From ae3b481198403d88367aac9fd7988aff3481ed30 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Thu, 2 Jan 2025 11:51:10 +0100 Subject: [PATCH] process feedback --- docs/node-configuration.md | 2 + docs/rpc.md | 46 +++++++++++ pkg/config/ledger_config.go | 2 +- pkg/core/blockchain.go | 5 ++ pkg/core/dao/dao.go | 6 ++ pkg/core/dao/dao_test.go | 22 ++---- pkg/core/interop/context.go | 4 +- pkg/core/interop/contract/call.go | 10 +-- pkg/core/state/contract_invocation.go | 109 ++++++++++++++++++++++++++ pkg/core/state/notification_event.go | 89 ++------------------- 10 files changed, 186 insertions(+), 109 deletions(-) create mode 100644 pkg/core/state/contract_invocation.go diff --git a/docs/node-configuration.md b/docs/node-configuration.md index 162ef7909c..196d660cae 100644 --- a/docs/node-configuration.md +++ b/docs/node-configuration.md @@ -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 @@ -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 diff --git a/docs/rpc.md b/docs/rpc.md index d450c1b7b2..07aa784daf 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -356,6 +356,52 @@ 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 invocations + +The `SaveInvocations` node configuration setting stores smart contract invocation +details into the application logs under the `invocations` key. 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: +```json +"invocations": [ + { + "hash": "0xd2a4cff31913016155e38e474a2c06d08be276cf", + "method": "transfer", + "arguments": { + "type": "Array", + "value": [ + { + "type": "ByteString", + "value": "krOcd6pg8ptXwXPO2Rfxf9Mhpus=" + }, + { + "type": "ByteString", + "value": "AZelPVEEY0csq+FRLl/HJ9cW+Qs=" + }, + { + "type": "Integer", + "value": "1000000000000" + }, + { + "type": "Any" + } + ] + }, + "argumentscount": 4, + "truncated": false + } + ] +``` + +For security reasons the `arguments` field data may result in `null`. In such case the +`Truncated` field will be set to `true`. + +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) diff --git a/pkg/config/ledger_config.go b/pkg/config/ledger_config.go index f62f0e43fd..243f5de5b5 100644 --- a/pkg/config/ledger_config.go +++ b/pkg/config/ledger_config.go @@ -19,7 +19,7 @@ type Ledger struct { // SkipBlockVerification allows to disable verification of received // blocks (including cryptographic checks). SkipBlockVerification bool `yaml:"SkipBlockVerification"` - // SaveInvocations enables contract smart contract invocation data saving. + // SaveInvocations enables smart contract invocation data saving. SaveInvocations bool `yaml:"SaveInvocations"` } diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 099f04ae57..edd3833550 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -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, } bc.dao.PutVersion(ver) bc.dao.Version = ver @@ -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) + } bc.dao.Version = ver bc.persistent.Version = ver diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index 579d355482..0aa17d7d66 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -448,6 +448,7 @@ type Version struct { KeepOnlyLatestState bool Magic uint32 Value string + SaveInvocations bool } const ( @@ -455,6 +456,7 @@ const ( p2pSigExtensionsBit p2pStateExchangeExtensionsBit keepOnlyLatestStateBit + saveInvocationsBit ) // FromBytes decodes v from a byte-slice. @@ -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 { @@ -505,6 +508,9 @@ func (v *Version) Bytes() []byte { if v.KeepOnlyLatestState { mask |= keepOnlyLatestStateBit } + if v.SaveInvocations { + mask |= saveInvocationsBit + } res := append([]byte(v.Value), '\x00', byte(v.StoragePrefix), mask) res = binary.LittleEndian.AppendUint32(res, v.Magic) return res diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index 4ae08f4e15..4d7b8d7640 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -176,6 +176,10 @@ func TestStoreAsTransaction(t *testing.T) { tx.Signers = append(tx.Signers, transaction.Signer{}) tx.Scripts = append(tx.Scripts, transaction.Witness{}) hash := tx.Hash() + si := stackitem.NewArray([]stackitem.Item{stackitem.NewBool(false)}) + argBytes, err := dao.GetItemCtx().Serialize(si, false) + require.NoError(t, err) + ci := state.NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1, false) aer := &state.AppExecResult{ Container: hash, Execution: state.Execution{ @@ -184,24 +188,14 @@ func TestStoreAsTransaction(t *testing.T) { { ScriptHash: util.Uint160{}, Name: "fakeTransferEvent", - Item: stackitem.NewArray([]stackitem.Item{ - stackitem.NewBool(false), - }), + Item: si, }, }, - Stack: []stackitem.Item{}, - Invocations: []state.ContractInvocation{{ - Hash: util.Uint160{}, - Method: "fakeMethodCall", - Arguments: stackitem.NewArray([]stackitem.Item{ - stackitem.NewBool(false), - }), - ArgumentsCount: 1, - Truncated: false, - }}, + Stack: []stackitem.Item{}, + Invocations: []state.ContractInvocation{*ci}, }, } - err := dao.StoreAsTransaction(tx, 0, aer) + err = dao.StoreAsTransaction(tx, 0, aer) require.NoError(t, err) err = dao.HasTransaction(hash, nil, 0, 0) require.ErrorIs(t, err, ErrAlreadyExists) diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index 08be26d20e..6cbd6d4ac4 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -80,7 +80,7 @@ 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), @@ -96,7 +96,7 @@ func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, bas baseExecFee: baseExecFee, baseStorageFee: baseStorageFee, loadToken: loadTokenFunc, - SaveInvocations: bc.GetConfig().SaveInvocations, + SaveInvocations: cfg.SaveInvocations, } } diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 1cbabf5f32..f6c6be38f1 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -79,14 +79,8 @@ func Call(ic *interop.Context) error { if argBytes, err = ic.DAO.GetItemCtx().Serialize(stackitem.NewArray(args), false); err != nil { truncated = true } - - ic.InvocationCalls = append(ic.InvocationCalls, state.ContractInvocation{ - Hash: u, - Method: method, - ArgumentsBytes: argBytes, - ArgumentsCount: uint32(arrCount), - Truncated: truncated, - }) + ci := state.NewContractInvocation(u, method, argBytes, uint32(arrCount), truncated) + ic.InvocationCalls = append(ic.InvocationCalls, *ci) } return callInternal(ic, cs, method, fs, hasReturn, args, true) } diff --git a/pkg/core/state/contract_invocation.go b/pkg/core/state/contract_invocation.go new file mode 100644 index 0000000000..d84b2f4ab4 --- /dev/null +++ b/pkg/core/state/contract_invocation.go @@ -0,0 +1,109 @@ +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" +) + +// NewContractInvocation return a new ContractInvocation. +func NewContractInvocation(hash util.Uint160, method string, argBytes []byte, argCnt uint32, truncated bool) *ContractInvocation { + return &ContractInvocation{ + Hash: hash, + Method: method, + argumentsBytes: argBytes, + ArgumentsCount: argCnt, + Truncated: truncated, + } +} + +// 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"` +} + +// DecodeBinary implements the Serializable interface. +func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { + ci.Hash.DecodeBinary(r) + ci.Method = r.ReadString() + ci.argumentsBytes = r.ReadVarBytes() + ci.ArgumentsCount = r.ReadU32LE() + ci.Truncated = r.ReadBool() +} + +// 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.WriteVarBytes(ci.argumentsBytes) + w.WriteU32LE(ci.ArgumentsCount) + w.WriteBool(ci.Truncated) +} + +// MarshalJSON implements the json.Marshaler interface. +func (ci ContractInvocation) MarshalJSON() ([]byte, error) { + si, err := stackitem.Deserialize(ci.argumentsBytes) + if err != nil { + return nil, err + } + item, err := stackitem.ToJSONWithTypes(si.(*stackitem.Array)) + if err != nil { + item = []byte(fmt.Sprintf(`"error: %v"`, err)) + } + 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 + } + params, err := stackitem.FromJSONWithTypes(aux.Arguments) + if err != nil { + return err + } + if t := params.Type(); t != stackitem.ArrayT { + return fmt.Errorf("failed to convert invocation state of type %s to array", t.String()) + } + ci.Arguments = params.(*stackitem.Array) + ci.Method = aux.Method + ci.Hash = aux.Hash + ci.ArgumentsCount = aux.ArgumentsCount + ci.Truncated = aux.Truncated + return nil +} + +// 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"` + ArgumentsCount uint32 `json:"argumentscount"` + Truncated bool `json:"truncated"` +} diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 72c1a8dc81..3f926a6ea8 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -20,79 +20,6 @@ type NotificationEvent struct { Item *stackitem.Array `json:"state"` } -// ContractInvocation contains method call information. -// The Arguments field will be nil if serialization of the arguments exceeds a predefined limit -// (for security reasons). In that case Truncated will be set to true. -type ContractInvocation struct { - Hash util.Uint160 `json:"contracthash"` - Method string `json:"method"` - Arguments *stackitem.Array `json:"arguments"` - ArgumentsBytes []byte - ArgumentsCount uint32 `json:"argumentscount"` - Truncated bool `json:"truncated"` -} - -func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { - ci.Hash.DecodeBinary(r) - ci.Method = r.ReadString() - ci.ArgumentsBytes = r.ReadVarBytes() - si, err := stackitem.Deserialize(ci.ArgumentsBytes) - if err != nil { - return - } - ci.Arguments = si.(*stackitem.Array) - ci.ArgumentsCount = r.ReadU32LE() - ci.Truncated = r.ReadBool() -} - -func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem.SerializationContext) { - ci.Hash.EncodeBinary(w) - w.WriteString(ci.Method) - w.WriteVarBytes(ci.ArgumentsBytes) - w.WriteU32LE(ci.ArgumentsCount) - w.WriteBool(ci.Truncated) -} - -// MarshalJSON implements the json.Marshaler interface. -func (ci ContractInvocation) MarshalJSON() ([]byte, error) { - item, err := stackitem.ToJSONWithTypes(ci.Arguments) - if err != nil { - item = []byte(fmt.Sprintf(`"error: %v"`, err)) - } - 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 - } - params, err := stackitem.FromJSONWithTypes(aux.Arguments) - if err != nil { - return err - } - if t := params.Type(); t != stackitem.ArrayT { - return fmt.Errorf("failed to convert invocation state of type %s to array", t.String()) - } - ci.Arguments = params.(*stackitem.Array) - ci.Method = aux.Method - ci.Hash = aux.Hash - ci.ArgumentsCount = aux.ArgumentsCount - ci.Truncated = aux.Truncated - return nil -} - -func (ci *ContractInvocation) EncodeBinary(w *io.BinWriter) { - ci.EncodeBinaryWithContext(w, stackitem.NewSerializationContext()) -} - // AppExecResult represents the result of the script execution, gathering together // all resulting notifications, state, stack and other metadata. type AppExecResult struct { @@ -168,9 +95,11 @@ func (aer *AppExecResult) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem aer.Events[i].EncodeBinaryWithContext(w, sc) } w.WriteVarBytes([]byte(aer.FaultException)) - w.WriteVarUint(uint64(len(aer.Invocations))) - for i := range aer.Invocations { - aer.Invocations[i].EncodeBinaryWithContext(w, sc) + if invocLen := len(aer.Invocations); invocLen > 0 { + w.WriteVarUint(uint64(invocLen)) + for i := range aer.Invocations { + aer.Invocations[i].EncodeBinaryWithContext(w, sc) + } } } @@ -292,14 +221,6 @@ type Execution struct { Invocations []ContractInvocation } -type ContractInvocationAux struct { - Hash util.Uint160 `json:"contracthash"` - Method string `json:"method"` - Arguments json.RawMessage `json:"arguments"` - ArgumentsCount uint32 `json:"argumentscount"` - Truncated bool `json:"truncated"` -} - // executionAux represents an auxiliary struct for Execution JSON marshalling. type executionAux struct { Trigger string `json:"trigger"`