Skip to content

Commit

Permalink
state, rpc: add invocations to applicationlog
Browse files Browse the repository at this point in the history
Add setting to include smart contract invocations data to the applicationlog.
Original issue at neo-project/neo#3386

Signed-off-by: ixje <[email protected]>
  • Loading branch information
ixje authored and ixje committed Jan 24, 2025
1 parent e8b8c1a commit 4b2ee9a
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 21 deletions.
2 changes: 2 additions & 0 deletions docs/node-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ node-related settings described in the table below.
| SaveStorageBatch | `bool` | `false` | Enables storage batch saving before every persist. It is similar to StorageDump plugin for C# node. |
| SkipBlockVerification | `bool` | `false` | Allows to disable verification of received/processed blocks (including cryptographic checks). |
| StateRoot | [State Root Configuration](#State-Root-Configuration) | | State root module configuration. See the [State Root Configuration](#State-Root-Configuration) section for details. |
| SaveInvocations | `bool` | `false` | Determines if additional smart contract invocation details are stored. If enabled, the `getapplicationlog` RPC method will return a new field with invocation details for the transaction. See the [RPC](rpc.md#applicationlog-invocations) documentation for more information. |

### P2P Configuration

Expand Down Expand Up @@ -471,6 +472,7 @@ affect this:
- `GarbageCollectionPeriod` must be the same
- `KeepOnlyLatestState` must be the same
- `RemoveUntraceableBlocks` must be the same
- `SaveInvocations` must be the same

BotlDB is also known to be incompatible between machines with different
endianness. Nothing is known for LevelDB wrt this, so it's not recommended
Expand Down
56 changes: 56 additions & 0 deletions docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,62 @@ to various blockchain events (with simple event filtering) and receive them on
the client as JSON-RPC notifications. More details on that are written in the
[notifications specification](notifications.md).

#### `applicationlog` call invocations

The `SaveInvocations` configuration setting causes the RPC server to store smart contract
invocation details as part of the application logs. This feature is specifically useful to
capture information in the absence of `System.Runtime.Notify` calls for the given smart
contract method. Other use-cases are described in [this issue](https://github.com/neo-project/neo/issues/3386).

Example transaction on Testnet which interacts with the native PolicyContract:
```json
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"txid": "0xd6fe5f61d9cb34d6324db1be42c056d02ba1f1f6cd0bd3f3c6bb24faaaeef2a9",
"executions": [
{
"trigger": "Application",
"vmstate": "HALT",
"gasconsumed": "2028120",
"stack": [
{
"type": "Any"
}
],
"notifications": [],
"exception": null,
"invocations": [
{
"hash": "0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b",
"method": "setFeePerByte",
"arguments": {
"type": "Array",
"value": [
{
"type": "Integer",
"value": "100"
}
]
},
"argumentscount": 1,
"truncated": false
}
]
}
]
}
}
```

For security reasons the `arguments` field data may result in `null` if the count exceeds 2048.
In such case the `Truncated` field will be set to `true`.

The invocation records are presented in a flat structure in the order as how they were executed.
Note that invocation records for faulted transactions are kept and are present in the
applicationlog. This behaviour differs from notifications which are omitted for faulted transactions.

## Reference

* [JSON-RPC 2.0 Specification](http://www.jsonrpc.org/specification)
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/ledger_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type Ledger struct {
// SkipBlockVerification allows to disable verification of received
// blocks (including cryptographic checks).
SkipBlockVerification bool `yaml:"SkipBlockVerification"`
// SaveInvocations enables smart contract invocation data saving.
SaveInvocations bool `yaml:"SaveInvocations"`
}

// Blockchain is a set of settings for core.Blockchain to use, it includes protocol
Expand Down
6 changes: 6 additions & 0 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ func (bc *Blockchain) init() error {
KeepOnlyLatestState: bc.config.Ledger.KeepOnlyLatestState,
Magic: uint32(bc.config.Magic),
Value: version,
SaveInvocations: bc.config.SaveInvocations,

Check warning on line 420 in pkg/core/blockchain.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/blockchain.go#L420

Added line #L420 was not covered by tests
}
bc.dao.PutVersion(ver)
bc.dao.Version = ver
Expand Down Expand Up @@ -454,6 +455,10 @@ func (bc *Blockchain) init() error {
return fmt.Errorf("protocol configuration Magic mismatch (old=%v, new=%v)",
ver.Magic, bc.config.Magic)
}
if ver.SaveInvocations != bc.config.SaveInvocations {
return fmt.Errorf("SaveInvocations setting mismatch (old=%v, new=%v)",
ver.SaveInvocations, bc.config.SaveInvocations)
}

Check warning on line 461 in pkg/core/blockchain.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/blockchain.go#L461

Added line #L461 was not covered by tests
bc.dao.Version = ver
bc.persistent.Version = ver

Expand Down Expand Up @@ -1717,6 +1722,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
Stack: v.Estack().ToArray(),
Events: systemInterop.Notifications,
FaultException: faultException,
Invocations: systemInterop.InvocationCalls,

Check warning on line 1725 in pkg/core/blockchain.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/blockchain.go#L1725

Added line #L1725 was not covered by tests
},
}
appExecResults = append(appExecResults, aer)
Expand Down
6 changes: 6 additions & 0 deletions pkg/core/dao/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,13 +448,15 @@ type Version struct {
KeepOnlyLatestState bool
Magic uint32
Value string
SaveInvocations bool
}

const (
stateRootInHeaderBit = 1 << iota
p2pSigExtensionsBit
p2pStateExchangeExtensionsBit
keepOnlyLatestStateBit
saveInvocationsBit
)

// FromBytes decodes v from a byte-slice.
Expand Down Expand Up @@ -482,6 +484,7 @@ func (v *Version) FromBytes(data []byte) error {
v.P2PSigExtensions = data[i+2]&p2pSigExtensionsBit != 0
v.P2PStateExchangeExtensions = data[i+2]&p2pStateExchangeExtensionsBit != 0
v.KeepOnlyLatestState = data[i+2]&keepOnlyLatestStateBit != 0
v.SaveInvocations = data[i+2]&saveInvocationsBit != 0

m := i + 3
if len(data) == m+4 {
Expand All @@ -505,6 +508,9 @@ func (v *Version) Bytes() []byte {
if v.KeepOnlyLatestState {
mask |= keepOnlyLatestStateBit
}
if v.SaveInvocations {
mask |= saveInvocationsBit
}

Check warning on line 513 in pkg/core/dao/dao.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/dao/dao.go#L512-L513

Added lines #L512 - L513 were not covered by tests
res := append([]byte(v.Value), '\x00', byte(v.StoragePrefix), mask)
res = binary.LittleEndian.AppendUint32(res, v.Magic)
return res
Expand Down
33 changes: 18 additions & 15 deletions pkg/core/interop/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ type Context struct {
VM *vm.VM
Functions []Function
Invocations map[util.Uint160]int
InvocationCalls []state.ContractInvocation
cancelFuncs []context.CancelFunc
getContract func(*dao.Simple, util.Uint160) (*state.Contract, error)
baseExecFee int64
baseStorageFee int64
loadToken func(ic *Context, id int32) error
GetRandomCounter uint32
signers []transaction.Signer
SaveInvocations bool
}

// NewContext returns new interop context.
Expand All @@ -78,22 +80,23 @@ func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, bas
loadTokenFunc func(ic *Context, id int32) error,
block *block.Block, tx *transaction.Transaction, log *zap.Logger) *Context {
dao := d.GetPrivate()
cfg := bc.GetConfig().ProtocolConfiguration
cfg := bc.GetConfig()
return &Context{
Chain: bc,
Network: uint32(cfg.Magic),
Hardforks: cfg.Hardforks,
Natives: natives,
Trigger: trigger,
Block: block,
Tx: tx,
DAO: dao,
Log: log,
Invocations: make(map[util.Uint160]int),
getContract: getContract,
baseExecFee: baseExecFee,
baseStorageFee: baseStorageFee,
loadToken: loadTokenFunc,
Chain: bc,
Network: uint32(cfg.Magic),
Hardforks: cfg.Hardforks,
Natives: natives,
Trigger: trigger,
Block: block,
Tx: tx,
DAO: dao,
Log: log,
Invocations: make(map[util.Uint160]int),
getContract: getContract,
baseExecFee: baseExecFee,
baseStorageFee: baseStorageFee,
loadToken: loadTokenFunc,
SaveInvocations: cfg.SaveInvocations,
}
}

Expand Down
13 changes: 13 additions & 0 deletions pkg/core/interop/contract/call.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package contract

import (
"bytes"
"errors"
"fmt"
"math/big"
Expand Down Expand Up @@ -69,6 +70,18 @@ func Call(ic *interop.Context) error {
return fmt.Errorf("method not found: %s/%d", method, len(args))
}
hasReturn := md.ReturnType != smartcontract.VoidType

if ic.SaveInvocations {
var (
arrCount = len(args)
argBytes []byte
)
if argBytes, err = ic.DAO.GetItemCtx().Serialize(stackitem.NewArray(args), false); err != nil {
argBytes = nil
}
ci := state.NewContractInvocation(u, method, bytes.Clone(argBytes), uint32(arrCount))
ic.InvocationCalls = append(ic.InvocationCalls, *ci)

Check warning on line 83 in pkg/core/interop/contract/call.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/interop/contract/call.go#L75-L83

Added lines #L75 - L83 were not covered by tests
}
return callInternal(ic, cs, method, fs, hasReturn, args, true)
}

Expand Down
120 changes: 120 additions & 0 deletions pkg/core/state/contract_invocation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package state

import (
"encoding/json"
"fmt"

"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// ContractInvocation contains method call information.
// The Arguments field will be nil if serialization of the arguments exceeds the predefined limit
// of [stackitem.MaxSerialized] (for security reasons). In that case Truncated will be set to true.
type ContractInvocation struct {
Hash util.Uint160 `json:"contract"`
Method string `json:"method"`
// Arguments are the arguments as passed to the `args` parameter of System.Contract.Call
// for use in the RPC Server and RPC Client.
Arguments *stackitem.Array `json:"arguments"`
// argumentsBytes is the serialized arguments used at the interop level.
argumentsBytes []byte
ArgumentsCount uint32 `json:"argumentscount"`
Truncated bool `json:"truncated"`
}

// contractInvocationAux is an auxiliary struct for ContractInvocation JSON marshalling.
type contractInvocationAux struct {
Hash util.Uint160 `json:"hash"`
Method string `json:"method"`
Arguments json.RawMessage `json:"arguments,omitempty"`
ArgumentsCount uint32 `json:"argumentscount"`
Truncated bool `json:"truncated"`
}

// NewContractInvocation returns a new ContractInvocation.
func NewContractInvocation(hash util.Uint160, method string, argBytes []byte, argCnt uint32) *ContractInvocation {
return &ContractInvocation{
Hash: hash,
Method: method,
argumentsBytes: argBytes,
ArgumentsCount: argCnt,
Truncated: argBytes == nil,
}
}

// DecodeBinary implements the Serializable interface.
func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) {
ci.Hash.DecodeBinary(r)
ci.Method = r.ReadString()
ci.ArgumentsCount = r.ReadU32LE()
ci.Truncated = r.ReadBool()
if !ci.Truncated {
ci.argumentsBytes = r.ReadVarBytes()
}
}

// EncodeBinary implements the Serializable interface.
func (ci *ContractInvocation) EncodeBinary(w *io.BinWriter) {
ci.EncodeBinaryWithContext(w, stackitem.NewSerializationContext())
}

// EncodeBinaryWithContext is the same as EncodeBinary, but allows to efficiently reuse
// stack item serialization context.
func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem.SerializationContext) {
ci.Hash.EncodeBinary(w)
w.WriteString(ci.Method)
w.WriteU32LE(ci.ArgumentsCount)
w.WriteBool(ci.Truncated)
if !ci.Truncated {
w.WriteVarBytes(ci.argumentsBytes)
}
}

// MarshalJSON implements the json.Marshaler interface.
func (ci ContractInvocation) MarshalJSON() ([]byte, error) {
var item []byte
if ci.Arguments == nil && ci.argumentsBytes != nil {
si, err := stackitem.Deserialize(ci.argumentsBytes)
if err != nil {
return nil, err
}

Check warning on line 82 in pkg/core/state/contract_invocation.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/state/contract_invocation.go#L81-L82

Added lines #L81 - L82 were not covered by tests
item, err = stackitem.ToJSONWithTypes(si.(*stackitem.Array))
if err != nil {
item = nil
}

Check warning on line 86 in pkg/core/state/contract_invocation.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/state/contract_invocation.go#L85-L86

Added lines #L85 - L86 were not covered by tests
}
return json.Marshal(contractInvocationAux{
Hash: ci.Hash,
Method: ci.Method,
Arguments: item,
ArgumentsCount: ci.ArgumentsCount,
Truncated: ci.Truncated,
})
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (ci *ContractInvocation) UnmarshalJSON(data []byte) error {
aux := new(contractInvocationAux)
if err := json.Unmarshal(data, aux); err != nil {
return err
}

Check warning on line 102 in pkg/core/state/contract_invocation.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/state/contract_invocation.go#L101-L102

Added lines #L101 - L102 were not covered by tests
var args *stackitem.Array
if aux.Arguments != nil {
arguments, err := stackitem.FromJSONWithTypes(aux.Arguments)
if err != nil {
return err
}

Check warning on line 108 in pkg/core/state/contract_invocation.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/state/contract_invocation.go#L107-L108

Added lines #L107 - L108 were not covered by tests
if t := arguments.Type(); t != stackitem.ArrayT {
return fmt.Errorf("failed to convert invocation state of type %s to array", t.String())
}

Check warning on line 111 in pkg/core/state/contract_invocation.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/state/contract_invocation.go#L110-L111

Added lines #L110 - L111 were not covered by tests
args = arguments.(*stackitem.Array)
}
ci.Method = aux.Method
ci.Hash = aux.Hash
ci.ArgumentsCount = aux.ArgumentsCount
ci.Truncated = aux.Truncated
ci.Arguments = args
return nil
}
Loading

0 comments on commit 4b2ee9a

Please sign in to comment.