From d8a0d132b50e36e083c0b06c2cd7c492d8215cde Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Fri, 28 Jun 2024 22:10:41 +0200 Subject: [PATCH 01/16] Store invocations in application log --- pkg/core/blockchain.go | 1 + pkg/core/dao/dao_test.go | 19 +++++- pkg/core/interop/context.go | 1 + pkg/core/interop/contract/call.go | 6 ++ pkg/core/state/notification_event.go | 96 ++++++++++++++++++++++++++-- 5 files changed, 114 insertions(+), 9 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index fcfc1eec02..099f04ae57 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1717,6 +1717,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error Stack: v.Estack().ToArray(), Events: systemInterop.Notifications, FaultException: faultException, + Invocations: systemInterop.InvocationCalls, }, } appExecResults = append(appExecResults, aer) diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index 2652c422c8..483e1a36a7 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -180,8 +180,23 @@ func TestStoreAsTransaction(t *testing.T) { Container: hash, Execution: state.Execution{ Trigger: trigger.Application, - Events: []state.NotificationEvent{}, - Stack: []stackitem.Item{}, + Events: []state.NotificationEvent{ + { + ScriptHash: util.Uint160{}, + Name: "fakeTransferEvent", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewBool(false), + }), + }, + }, + Stack: []stackitem.Item{}, + Invocations: []state.ContractInvocation{{ + Hash: util.Uint160{}, + Method: "fakeMethodCall", + Params: stackitem.NewArray([]stackitem.Item{ + stackitem.NewBool(false), + }), + }}, }, } err := dao.StoreAsTransaction(tx, 0, aer) diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index d9ff394263..67f7718d75 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -63,6 +63,7 @@ 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 diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 60f8ac9c57..ceca829312 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -69,6 +69,12 @@ func Call(ic *interop.Context) error { return fmt.Errorf("method not found: %s/%d", method, len(args)) } hasReturn := md.ReturnType != smartcontract.VoidType + + ic.InvocationCalls = append(ic.InvocationCalls, state.ContractInvocation{ + Hash: u, + Method: method, + Params: stackitem.NewArray(args), + }) return callInternal(ic, cs, method, fs, hasReturn, args, true) } diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index d30f231a9d..b0eed0c92f 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" @@ -20,6 +19,74 @@ type NotificationEvent struct { Item *stackitem.Array `json:"state"` } +type ContractInvocation struct { + Hash util.Uint160 `json:"contract_hash"` + Method string `json:"method"` + Params *stackitem.Array `json:"parameters"` +} + +func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { + ci.Hash.DecodeBinary(r) + ci.Method = r.ReadString() + params := stackitem.DecodeBinary(r) + if r.Err != nil { + return + } + arr, ok := params.Value().([]stackitem.Item) + if !ok { + r.Err = errors.New("Array or Struct expected") + return + } + ci.Params = stackitem.NewArray(arr) +} + +func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem.SerializationContext) { + ci.Hash.EncodeBinary(w) + w.WriteString(ci.Method) + b, err := sc.Serialize(ci.Params, false) + if err != nil { + w.Err = err + return + } + w.WriteBytes(b) +} + +// MarshalJSON implements the json.Marshaler interface. +func (ci ContractInvocation) MarshalJSON() ([]byte, error) { + item, err := stackitem.ToJSONWithTypes(ci.Params) + if err != nil { + item = []byte(fmt.Sprintf(`"error: %v"`, err)) + } + return json.Marshal(ContractInvocationAux{ + Hash: ci.Hash, + Method: ci.Method, + Params: item, + }) +} + +// 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.Params) + 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.Params = params.(*stackitem.Array) + ci.Method = aux.Method + ci.Hash = aux.Hash + 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 { @@ -95,6 +162,10 @@ 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) + } } // DecodeBinary implements the Serializable interface. @@ -120,6 +191,7 @@ func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { aer.Stack = arr r.ReadArray(&aer.Events) aer.FaultException = r.ReadString() + r.ReadArray(&aer.Invocations) } // notificationEventAux is an auxiliary struct for NotificationEvent JSON marshalling. @@ -209,16 +281,24 @@ type Execution struct { Stack []stackitem.Item Events []NotificationEvent FaultException string + Invocations []ContractInvocation +} + +type ContractInvocationAux struct { + Hash util.Uint160 `json:"contract_hash"` + Method string `json:"method"` + Params json.RawMessage `json:"parameters"` } // executionAux represents an auxiliary struct for Execution JSON marshalling. type executionAux struct { - Trigger string `json:"trigger"` - VMState string `json:"vmstate"` - GasConsumed int64 `json:"gasconsumed,string"` - Stack json.RawMessage `json:"stack"` - Events []NotificationEvent `json:"notifications"` - FaultException *string `json:"exception"` + Trigger string `json:"trigger"` + VMState string `json:"vmstate"` + GasConsumed int64 `json:"gasconsumed,string"` + Stack json.RawMessage `json:"stack"` + Events []NotificationEvent `json:"notifications"` + FaultException *string `json:"exception"` + Invocations []ContractInvocation `json:"invocations"` } // MarshalJSON implements the json.Marshaler interface. @@ -246,6 +326,7 @@ func (e Execution) MarshalJSON() ([]byte, error) { Stack: st, Events: e.Events, FaultException: exception, + Invocations: e.Invocations, }) } @@ -287,6 +368,7 @@ func (e *Execution) UnmarshalJSON(data []byte) error { if aux.FaultException != nil { e.FaultException = *aux.FaultException } + e.Invocations = aux.Invocations return nil } From 04c71f52bcef59d5316364686e8c348b1abdb3d4 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Mon, 2 Sep 2024 13:29:55 +0200 Subject: [PATCH 02/16] rename params to arguments --- pkg/core/dao/dao_test.go | 2 +- pkg/core/interop/contract/call.go | 6 ++--- pkg/core/state/notification_event.go | 35 ++++++++++++++-------------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index 483e1a36a7..f57b4d3649 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -193,7 +193,7 @@ func TestStoreAsTransaction(t *testing.T) { Invocations: []state.ContractInvocation{{ Hash: util.Uint160{}, Method: "fakeMethodCall", - Params: stackitem.NewArray([]stackitem.Item{ + Arguments: stackitem.NewArray([]stackitem.Item{ stackitem.NewBool(false), }), }}, diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index ceca829312..6c6bdc64a4 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -71,9 +71,9 @@ func Call(ic *interop.Context) error { hasReturn := md.ReturnType != smartcontract.VoidType ic.InvocationCalls = append(ic.InvocationCalls, state.ContractInvocation{ - Hash: u, - Method: method, - Params: stackitem.NewArray(args), + Hash: u, + Method: method, + Arguments: stackitem.NewArray(args), }) return callInternal(ic, cs, method, fs, hasReturn, args, true) } diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index b0eed0c92f..9fdd38b2e8 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" @@ -20,30 +21,30 @@ type NotificationEvent struct { } type ContractInvocation struct { - Hash util.Uint160 `json:"contract_hash"` - Method string `json:"method"` - Params *stackitem.Array `json:"parameters"` + Hash util.Uint160 `json:"contract_hash"` + Method string `json:"method"` + Arguments *stackitem.Array `json:"arguments"` } func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { ci.Hash.DecodeBinary(r) ci.Method = r.ReadString() - params := stackitem.DecodeBinary(r) + args := stackitem.DecodeBinary(r) if r.Err != nil { return } - arr, ok := params.Value().([]stackitem.Item) + arr, ok := args.Value().([]stackitem.Item) if !ok { - r.Err = errors.New("Array or Struct expected") + r.Err = errors.New("array or Struct expected") return } - ci.Params = stackitem.NewArray(arr) + ci.Arguments = stackitem.NewArray(arr) } func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem.SerializationContext) { ci.Hash.EncodeBinary(w) w.WriteString(ci.Method) - b, err := sc.Serialize(ci.Params, false) + b, err := sc.Serialize(ci.Arguments, false) if err != nil { w.Err = err return @@ -53,14 +54,14 @@ func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stack // MarshalJSON implements the json.Marshaler interface. func (ci ContractInvocation) MarshalJSON() ([]byte, error) { - item, err := stackitem.ToJSONWithTypes(ci.Params) + 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, - Params: item, + Hash: ci.Hash, + Method: ci.Method, + Arguments: item, }) } @@ -70,14 +71,14 @@ func (ci *ContractInvocation) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, aux); err != nil { return err } - params, err := stackitem.FromJSONWithTypes(aux.Params) + 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.Params = params.(*stackitem.Array) + ci.Arguments = params.(*stackitem.Array) ci.Method = aux.Method ci.Hash = aux.Hash return nil @@ -285,9 +286,9 @@ type Execution struct { } type ContractInvocationAux struct { - Hash util.Uint160 `json:"contract_hash"` - Method string `json:"method"` - Params json.RawMessage `json:"parameters"` + Hash util.Uint160 `json:"contract_hash"` + Method string `json:"method"` + Arguments json.RawMessage `json:"arguments"` } // executionAux represents an auxiliary struct for Execution JSON marshalling. From dfd5c9bb68c681694e1f1c198afbf955a2ee90a0 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Thu, 31 Oct 2024 10:03:33 +0100 Subject: [PATCH 03/16] make invocation saving configurable, safely handle args --- pkg/config/ledger_config.go | 2 ++ pkg/core/dao/dao_test.go | 2 ++ pkg/core/interop/contract/call.go | 22 +++++++++++++++----- pkg/core/state/notification_event.go | 30 +++++++++++++++++++--------- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/pkg/config/ledger_config.go b/pkg/config/ledger_config.go index 529a0d0176..f62f0e43fd 100644 --- a/pkg/config/ledger_config.go +++ b/pkg/config/ledger_config.go @@ -19,6 +19,8 @@ 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 bool `yaml:"SaveInvocations"` } // Blockchain is a set of settings for core.Blockchain to use, it includes protocol diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index f57b4d3649..acc88c9ed7 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -196,6 +196,8 @@ func TestStoreAsTransaction(t *testing.T) { Arguments: stackitem.NewArray([]stackitem.Item{ stackitem.NewBool(false), }), + ArgumentsCount: 1, + IsValid: true, }}, }, } diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 6c6bdc64a4..bc91010fc3 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -70,11 +70,23 @@ func Call(ic *interop.Context) error { } hasReturn := md.ReturnType != smartcontract.VoidType - ic.InvocationCalls = append(ic.InvocationCalls, state.ContractInvocation{ - Hash: u, - Method: method, - Arguments: stackitem.NewArray(args), - }) + if ic.Chain.GetConfig().Ledger.SaveInvocations { + arr := stackitem.NewArray(args) + arrCount := len(args) + valid := true + if _, err = ic.DAO.GetItemCtx().Serialize(arr, false); err != nil { + arr = stackitem.NewArray([]stackitem.Item{}) + valid = false + } + + ic.InvocationCalls = append(ic.InvocationCalls, state.ContractInvocation{ + Hash: u, + Method: method, + Arguments: arr, + ArgumentsCount: uint32(arrCount), + IsValid: valid, + }) + } return callInternal(ic, cs, method, fs, hasReturn, args, true) } diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 9fdd38b2e8..17619de68e 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -21,9 +21,11 @@ type NotificationEvent struct { } type ContractInvocation struct { - Hash util.Uint160 `json:"contract_hash"` - Method string `json:"method"` - Arguments *stackitem.Array `json:"arguments"` + Hash util.Uint160 `json:"contract_hash"` + Method string `json:"method"` + Arguments *stackitem.Array `json:"arguments"` + ArgumentsCount uint32 `json:"arguments_count"` + IsValid bool `json:"is_valid"` } func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { @@ -39,6 +41,8 @@ func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { return } ci.Arguments = stackitem.NewArray(arr) + ci.ArgumentsCount = r.ReadU32LE() + ci.IsValid = r.ReadBool() } func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem.SerializationContext) { @@ -50,6 +54,8 @@ func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stack return } w.WriteBytes(b) + w.WriteU32LE(ci.ArgumentsCount) + w.WriteBool(ci.IsValid) } // MarshalJSON implements the json.Marshaler interface. @@ -59,9 +65,11 @@ func (ci ContractInvocation) MarshalJSON() ([]byte, error) { item = []byte(fmt.Sprintf(`"error: %v"`, err)) } return json.Marshal(ContractInvocationAux{ - Hash: ci.Hash, - Method: ci.Method, - Arguments: item, + Hash: ci.Hash, + Method: ci.Method, + Arguments: item, + ArgumentsCount: ci.ArgumentsCount, + IsValid: ci.IsValid, }) } @@ -81,6 +89,8 @@ func (ci *ContractInvocation) UnmarshalJSON(data []byte) error { ci.Arguments = params.(*stackitem.Array) ci.Method = aux.Method ci.Hash = aux.Hash + ci.ArgumentsCount = aux.ArgumentsCount + ci.IsValid = aux.IsValid return nil } @@ -286,9 +296,11 @@ type Execution struct { } type ContractInvocationAux struct { - Hash util.Uint160 `json:"contract_hash"` - Method string `json:"method"` - Arguments json.RawMessage `json:"arguments"` + Hash util.Uint160 `json:"contract_hash"` + Method string `json:"method"` + Arguments json.RawMessage `json:"arguments"` + ArgumentsCount uint32 `json:"arguments_count"` + IsValid bool `json:"is_valid"` } // executionAux represents an auxiliary struct for Execution JSON marshalling. From cd7caf519a60e032f3b3a38fd4ba9e9596c0ecec Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Thu, 31 Oct 2024 10:25:13 +0100 Subject: [PATCH 04/16] re-use serialized arguments for storing --- pkg/core/interop/contract/call.go | 4 +++- pkg/core/state/notification_event.go | 20 ++++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index bc91010fc3..be1bbfdea4 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -74,7 +74,8 @@ func Call(ic *interop.Context) error { arr := stackitem.NewArray(args) arrCount := len(args) valid := true - if _, err = ic.DAO.GetItemCtx().Serialize(arr, false); err != nil { + argBytes := []byte{} + if argBytes, err = ic.DAO.GetItemCtx().Serialize(arr, false); err != nil { arr = stackitem.NewArray([]stackitem.Item{}) valid = false } @@ -83,6 +84,7 @@ func Call(ic *interop.Context) error { Hash: u, Method: method, Arguments: arr, + ArgumentsBytes: argBytes, ArgumentsCount: uint32(arrCount), IsValid: valid, }) diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 17619de68e..0d0394a4d9 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -24,6 +24,7 @@ type ContractInvocation struct { Hash util.Uint160 `json:"contract_hash"` Method string `json:"method"` Arguments *stackitem.Array `json:"arguments"` + ArgumentsBytes []byte `json:"arguments_bytes"` ArgumentsCount uint32 `json:"arguments_count"` IsValid bool `json:"is_valid"` } @@ -31,16 +32,12 @@ type ContractInvocation struct { func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { ci.Hash.DecodeBinary(r) ci.Method = r.ReadString() - args := stackitem.DecodeBinary(r) - if r.Err != nil { - return - } - arr, ok := args.Value().([]stackitem.Item) - if !ok { - r.Err = errors.New("array or Struct expected") + ci.ArgumentsBytes = r.ReadVarBytes() + si, err := stackitem.Deserialize(ci.ArgumentsBytes) + if err != nil { return } - ci.Arguments = stackitem.NewArray(arr) + ci.Arguments = si.(*stackitem.Array) ci.ArgumentsCount = r.ReadU32LE() ci.IsValid = r.ReadBool() } @@ -48,12 +45,7 @@ func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem.SerializationContext) { ci.Hash.EncodeBinary(w) w.WriteString(ci.Method) - b, err := sc.Serialize(ci.Arguments, false) - if err != nil { - w.Err = err - return - } - w.WriteBytes(b) + w.WriteVarBytes(ci.ArgumentsBytes) w.WriteU32LE(ci.ArgumentsCount) w.WriteBool(ci.IsValid) } From e7063a20a0376e5774c57627f36dc22f0dbc4efe Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Wed, 20 Nov 2024 15:15:28 +0100 Subject: [PATCH 05/16] feedback --- pkg/core/dao/dao_test.go | 2 +- pkg/core/interop/context.go | 30 +++++++++++++++------------- pkg/core/interop/contract/call.go | 19 +++++++++--------- pkg/core/state/notification_event.go | 29 ++++++++++++++++----------- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index acc88c9ed7..4ae08f4e15 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -197,7 +197,7 @@ func TestStoreAsTransaction(t *testing.T) { stackitem.NewBool(false), }), ArgumentsCount: 1, - IsValid: true, + Truncated: false, }}, }, } diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index 67f7718d75..08be26d20e 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -71,6 +71,7 @@ type Context struct { loadToken func(ic *Context, id int32) error GetRandomCounter uint32 signers []transaction.Signer + SaveInvocations bool } // NewContext returns new interop context. @@ -81,20 +82,21 @@ func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, bas dao := d.GetPrivate() cfg := bc.GetConfig().ProtocolConfiguration 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: bc.GetConfig().SaveInvocations, } } diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index be1bbfdea4..1cbabf5f32 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -70,23 +70,22 @@ func Call(ic *interop.Context) error { } hasReturn := md.ReturnType != smartcontract.VoidType - if ic.Chain.GetConfig().Ledger.SaveInvocations { - arr := stackitem.NewArray(args) - arrCount := len(args) - valid := true - argBytes := []byte{} - if argBytes, err = ic.DAO.GetItemCtx().Serialize(arr, false); err != nil { - arr = stackitem.NewArray([]stackitem.Item{}) - valid = false + if ic.SaveInvocations { + var ( + arrCount = len(args) + truncated = false + argBytes []byte + ) + 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, - Arguments: arr, ArgumentsBytes: argBytes, ArgumentsCount: uint32(arrCount), - IsValid: valid, + Truncated: truncated, }) } return callInternal(ic, cs, method, fs, hasReturn, args, true) diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 0d0394a4d9..72c1a8dc81 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -20,13 +20,16 @@ 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:"contract_hash"` + Hash util.Uint160 `json:"contracthash"` Method string `json:"method"` Arguments *stackitem.Array `json:"arguments"` - ArgumentsBytes []byte `json:"arguments_bytes"` - ArgumentsCount uint32 `json:"arguments_count"` - IsValid bool `json:"is_valid"` + ArgumentsBytes []byte + ArgumentsCount uint32 `json:"argumentscount"` + Truncated bool `json:"truncated"` } func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { @@ -39,7 +42,7 @@ func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { } ci.Arguments = si.(*stackitem.Array) ci.ArgumentsCount = r.ReadU32LE() - ci.IsValid = r.ReadBool() + ci.Truncated = r.ReadBool() } func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem.SerializationContext) { @@ -47,7 +50,7 @@ func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stack w.WriteString(ci.Method) w.WriteVarBytes(ci.ArgumentsBytes) w.WriteU32LE(ci.ArgumentsCount) - w.WriteBool(ci.IsValid) + w.WriteBool(ci.Truncated) } // MarshalJSON implements the json.Marshaler interface. @@ -61,7 +64,7 @@ func (ci ContractInvocation) MarshalJSON() ([]byte, error) { Method: ci.Method, Arguments: item, ArgumentsCount: ci.ArgumentsCount, - IsValid: ci.IsValid, + Truncated: ci.Truncated, }) } @@ -82,7 +85,7 @@ func (ci *ContractInvocation) UnmarshalJSON(data []byte) error { ci.Method = aux.Method ci.Hash = aux.Hash ci.ArgumentsCount = aux.ArgumentsCount - ci.IsValid = aux.IsValid + ci.Truncated = aux.Truncated return nil } @@ -194,7 +197,9 @@ func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { aer.Stack = arr r.ReadArray(&aer.Events) aer.FaultException = r.ReadString() - r.ReadArray(&aer.Invocations) + if r.Len() > 0 { + r.ReadArray(&aer.Invocations) + } } // notificationEventAux is an auxiliary struct for NotificationEvent JSON marshalling. @@ -288,11 +293,11 @@ type Execution struct { } type ContractInvocationAux struct { - Hash util.Uint160 `json:"contract_hash"` + Hash util.Uint160 `json:"contracthash"` Method string `json:"method"` Arguments json.RawMessage `json:"arguments"` - ArgumentsCount uint32 `json:"arguments_count"` - IsValid bool `json:"is_valid"` + ArgumentsCount uint32 `json:"argumentscount"` + Truncated bool `json:"truncated"` } // executionAux represents an auxiliary struct for Execution JSON marshalling. 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 06/16] 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"` From bc4cb3d351299227aeeb919b8edc4c8bfc3d722c Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Wed, 22 Jan 2025 11:32:40 +0100 Subject: [PATCH 07/16] process feedback --- docs/rpc.md | 111 ++++++++++++++------- pkg/core/interop/contract/call.go | 1 + pkg/core/state/contract_invocation.go | 42 +++++--- pkg/core/state/contract_invocation_test.go | 68 +++++++++++++ 4 files changed, 170 insertions(+), 52 deletions(-) create mode 100644 pkg/core/state/contract_invocation_test.go diff --git a/docs/rpc.md b/docs/rpc.md index 07aa784daf..df9a6996da 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -356,49 +356,88 @@ 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 +#### Applicationlog call 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). +The `SaveInvocations` configuration setting causes the RPC server to stores 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: ```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 - } - ] +{ + "id": 1, + "jsonrpc": "2.0", + "result": { + "txid": "0x7f40e2252fe791b89d60c2cdd9419de7984e5fab6d941d3cf3a8d0c96135c535", + "executions": [ + { + "trigger": "Application", + "vmstate": "HALT", + "gasconsumed": "9977780", + "stack": [], + "notifications": [ + { + "contract": "0xd2a4cff31913016155e38e474a2c06d08be276cf", + "eventname": "Transfer", + "state": { + "type": "Array", + "value": [ + { + "type": "ByteString", + "value": "krOcd6pg8ptXwXPO2Rfxf9Mhpus=" + }, + { + "type": "ByteString", + "value": "AZelPVEEY0csq+FRLl/HJ9cW+Qs=" + }, + { + "type": "Integer", + "value": "1000000000000" + } + ] + } + } + ], + "exception": null, + "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`. +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. diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index f6c6be38f1..8bcbd2cfed 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -78,6 +78,7 @@ func Call(ic *interop.Context) error { ) if argBytes, err = ic.DAO.GetItemCtx().Serialize(stackitem.NewArray(args), false); err != nil { truncated = true + argBytes = nil } ci := state.NewContractInvocation(u, method, argBytes, uint32(arrCount), truncated) ic.InvocationCalls = append(ic.InvocationCalls, *ci) diff --git a/pkg/core/state/contract_invocation.go b/pkg/core/state/contract_invocation.go index d84b2f4ab4..2f4687cef7 100644 --- a/pkg/core/state/contract_invocation.go +++ b/pkg/core/state/contract_invocation.go @@ -39,7 +39,9 @@ type ContractInvocation struct { func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { ci.Hash.DecodeBinary(r) ci.Method = r.ReadString() - ci.argumentsBytes = r.ReadVarBytes() + if b := r.ReadVarBytes(); len(b) > 0 { + ci.argumentsBytes = b + } ci.ArgumentsCount = r.ReadU32LE() ci.Truncated = r.ReadBool() } @@ -61,13 +63,16 @@ func (ci *ContractInvocation) EncodeBinaryWithContext(w *io.BinWriter, sc *stack // 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)) + var item []byte + if ci.argumentsBytes != nil { + si, err := stackitem.Deserialize(ci.argumentsBytes) + if err != nil { + return nil, err + } + item, err = stackitem.ToJSONWithTypes(si.(*stackitem.Array)) + if err != nil { + item = nil + } } return json.Marshal(contractInvocationAux{ Hash: ci.Hash, @@ -84,18 +89,23 @@ func (ci *ContractInvocation) UnmarshalJSON(data []byte) error { 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()) + var argsBytes []byte + if aux.Arguments != nil { + arguments, err := stackitem.FromJSONWithTypes(aux.Arguments) + if err != nil { + return err + } + if t := arguments.Type(); t != stackitem.ArrayT { + return fmt.Errorf("failed to convert invocation state of type %s to array", t.String()) + } + args := arguments.(*stackitem.Array) + argsBytes, _ = stackitem.NewSerializationContext().Serialize(args, false) } - ci.Arguments = params.(*stackitem.Array) ci.Method = aux.Method ci.Hash = aux.Hash ci.ArgumentsCount = aux.ArgumentsCount ci.Truncated = aux.Truncated + ci.argumentsBytes = argsBytes return nil } @@ -103,7 +113,7 @@ func (ci *ContractInvocation) UnmarshalJSON(data []byte) error { type contractInvocationAux struct { Hash util.Uint160 `json:"hash"` Method string `json:"method"` - Arguments json.RawMessage `json:"arguments"` + Arguments json.RawMessage `json:"arguments,omitempty"` ArgumentsCount uint32 `json:"argumentscount"` Truncated bool `json:"truncated"` } diff --git a/pkg/core/state/contract_invocation_test.go b/pkg/core/state/contract_invocation_test.go new file mode 100644 index 0000000000..5ffeb96f0c --- /dev/null +++ b/pkg/core/state/contract_invocation_test.go @@ -0,0 +1,68 @@ +package state + +import ( + "testing" + + json "github.com/nspcc-dev/go-ordered-json" + "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" + "github.com/stretchr/testify/require" +) + +func TestContractInvocation_JSON_InvalidArguments(t *testing.T) { + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, false) + + out, err := json.Marshal(&ci) + require.NoError(t, err) + + var ci2 ContractInvocation + err = json.Unmarshal(out, &ci2) + require.NoError(t, err) + require.Equal(t, ci, &ci2) +} + +func TestContractInvocation_JSON_ValidArguments(t *testing.T) { + si := stackitem.NewArray([]stackitem.Item{stackitem.NewBool(false)}) + argBytes, err := stackitem.NewSerializationContext().Serialize(si, false) + require.NoError(t, err) + + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1, false) + out, err := json.Marshal(&ci) + require.NoError(t, err) + + var ci2 ContractInvocation + err = json.Unmarshal(out, &ci2) + require.NoError(t, err) + require.Equal(t, ci, &ci2) +} + +func TestContractInvocation_EncodeDecode_InvalidArguments(t *testing.T) { + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, false) + + w := io.NewBufBinWriter() + ci.EncodeBinary(w.BinWriter) + require.NoError(t, w.Err) + + ciReader := io.NewBinReaderFromBuf(w.Bytes()) + ci2 := ContractInvocation{} + ci2.DecodeBinary(ciReader) + require.Equal(t, ci, &ci2) +} + +func TestContractInvocation_EncodeDecode_ValidArguments(t *testing.T) { + si := stackitem.NewArray([]stackitem.Item{stackitem.NewBool(false)}) + argBytes, err := stackitem.NewSerializationContext().Serialize(si, false) + require.NoError(t, err) + + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1, false) + + w := io.NewBufBinWriter() + ci.EncodeBinary(w.BinWriter) + require.NoError(t, w.Err) + + ciReader := io.NewBinReaderFromBuf(w.Bytes()) + ci2 := ContractInvocation{} + ci2.DecodeBinary(ciReader) + require.Equal(t, ci, &ci2) +} From 5ffb57db8d99ad338a30c0d0472a6df1e60860ed Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Wed, 22 Jan 2025 13:35:46 +0100 Subject: [PATCH 08/16] add better RPC example --- docs/rpc.md | 49 ++++++++++--------------------------------------- 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/docs/rpc.md b/docs/rpc.md index df9a6996da..f1be9d8739 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -363,68 +363,39 @@ invocation details as part of the application logs. This feature is specifically 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: +Example transaction on Testnet which interacts with the native PolicyContract: ```json { "id": 1, "jsonrpc": "2.0", "result": { - "txid": "0x7f40e2252fe791b89d60c2cdd9419de7984e5fab6d941d3cf3a8d0c96135c535", + "txid": "0xd6fe5f61d9cb34d6324db1be42c056d02ba1f1f6cd0bd3f3c6bb24faaaeef2a9", "executions": [ { "trigger": "Application", "vmstate": "HALT", - "gasconsumed": "9977780", - "stack": [], - "notifications": [ + "gasconsumed": "2028120", + "stack": [ { - "contract": "0xd2a4cff31913016155e38e474a2c06d08be276cf", - "eventname": "Transfer", - "state": { - "type": "Array", - "value": [ - { - "type": "ByteString", - "value": "krOcd6pg8ptXwXPO2Rfxf9Mhpus=" - }, - { - "type": "ByteString", - "value": "AZelPVEEY0csq+FRLl/HJ9cW+Qs=" - }, - { - "type": "Integer", - "value": "1000000000000" - } - ] - } + "type": "Any" } ], + "notifications": [], "exception": null, "invocations": [ { - "hash": "0xd2a4cff31913016155e38e474a2c06d08be276cf", - "method": "transfer", + "hash": "0xcc5e4edd9f5f8dba8bb65734541df7a1c081c67b", + "method": "setFeePerByte", "arguments": { "type": "Array", "value": [ - { - "type": "ByteString", - "value": "krOcd6pg8ptXwXPO2Rfxf9Mhpus=" - }, - { - "type": "ByteString", - "value": "AZelPVEEY0csq+FRLl/HJ9cW+Qs=" - }, { "type": "Integer", - "value": "1000000000000" - }, - { - "type": "Any" + "value": "100" } ] }, - "argumentscount": 4, + "argumentscount": 1, "truncated": false } ] From 90c18b7d6e6dcb1dff9fc09846236357a5e7f923 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Wed, 22 Jan 2025 13:39:00 +0100 Subject: [PATCH 09/16] preserve argBytes --- pkg/core/interop/contract/call.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 8bcbd2cfed..5243e8eb5a 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -76,7 +76,7 @@ func Call(ic *interop.Context) error { truncated = false argBytes []byte ) - if argBytes, err = ic.DAO.GetItemCtx().Serialize(stackitem.NewArray(args), false); err != nil { + if argBytes, err = stackitem.NewSerializationContext().Serialize(stackitem.NewArray(args), false); err != nil { truncated = true argBytes = nil } From d1d09210d1d34c867b07726e03a46693a5ec1bf5 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Wed, 22 Jan 2025 14:20:17 +0100 Subject: [PATCH 10/16] use VMState as marker --- pkg/core/state/notification_event.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 3f926a6ea8..9100910d10 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -78,6 +78,10 @@ func (aer *AppExecResult) EncodeBinary(w *io.BinWriter) { func (aer *AppExecResult) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem.SerializationContext) { w.WriteBytes(aer.Container[:]) w.WriteB(byte(aer.Trigger)) + invocLen := len(aer.Invocations) + if invocLen > 0 { + aer.VMState |= 0x80 + } w.WriteB(byte(aer.VMState)) w.WriteU64LE(uint64(aer.GasConsumed)) // Stack items are expected to be marshaled one by one. @@ -95,7 +99,7 @@ func (aer *AppExecResult) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem aer.Events[i].EncodeBinaryWithContext(w, sc) } w.WriteVarBytes([]byte(aer.FaultException)) - if invocLen := len(aer.Invocations); invocLen > 0 { + if invocLen > 0 { w.WriteVarUint(uint64(invocLen)) for i := range aer.Invocations { aer.Invocations[i].EncodeBinaryWithContext(w, sc) @@ -126,7 +130,7 @@ func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { aer.Stack = arr r.ReadArray(&aer.Events) aer.FaultException = r.ReadString() - if r.Len() > 0 { + if aer.VMState&0x80 != 0 { r.ReadArray(&aer.Invocations) } } From 25e93ecf838d2137302a91a9a988367fb0119906 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Wed, 22 Jan 2025 19:55:10 +0100 Subject: [PATCH 11/16] process feedback --- docs/rpc.md | 4 ++-- pkg/core/dao/dao_test.go | 17 +++-------------- pkg/core/interop/contract/call.go | 2 +- pkg/core/state/contract_invocation.go | 2 +- pkg/core/state/notification_event.go | 10 ++++++++-- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/docs/rpc.md b/docs/rpc.md index f1be9d8739..0c5f8c9870 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -356,9 +356,9 @@ 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 +#### `applicationlog` call invocations -The `SaveInvocations` configuration setting causes the RPC server to stores smart contract +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). diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index 4d7b8d7640..2652c422c8 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -176,26 +176,15 @@ 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{ Trigger: trigger.Application, - Events: []state.NotificationEvent{ - { - ScriptHash: util.Uint160{}, - Name: "fakeTransferEvent", - Item: si, - }, - }, - Stack: []stackitem.Item{}, - Invocations: []state.ContractInvocation{*ci}, + Events: []state.NotificationEvent{}, + Stack: []stackitem.Item{}, }, } - 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/contract/call.go b/pkg/core/interop/contract/call.go index 5243e8eb5a..d0e61e2f6d 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -73,7 +73,7 @@ func Call(ic *interop.Context) error { if ic.SaveInvocations { var ( arrCount = len(args) - truncated = false + truncated bool argBytes []byte ) if argBytes, err = stackitem.NewSerializationContext().Serialize(stackitem.NewArray(args), false); err != nil { diff --git a/pkg/core/state/contract_invocation.go b/pkg/core/state/contract_invocation.go index 2f4687cef7..bff5cfafff 100644 --- a/pkg/core/state/contract_invocation.go +++ b/pkg/core/state/contract_invocation.go @@ -9,7 +9,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) -// NewContractInvocation return a new ContractInvocation. +// NewContractInvocation returns a new ContractInvocation. func NewContractInvocation(hash util.Uint160, method string, argBytes []byte, argCnt uint32, truncated bool) *ContractInvocation { return &ContractInvocation{ Hash: hash, diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 9100910d10..0bdd8ed42e 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -12,6 +12,11 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" ) +const ( + MostSignificantBit = 0x80 + ClearMostSignificantBitMask = MostSignificantBit ^ 0xFF +) + // NotificationEvent is a tuple of the scripthash that has emitted the Item as a // notification and the item itself. type NotificationEvent struct { @@ -80,7 +85,7 @@ func (aer *AppExecResult) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem w.WriteB(byte(aer.Trigger)) invocLen := len(aer.Invocations) if invocLen > 0 { - aer.VMState |= 0x80 + aer.VMState |= MostSignificantBit } w.WriteB(byte(aer.VMState)) w.WriteU64LE(uint64(aer.GasConsumed)) @@ -130,8 +135,9 @@ func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { aer.Stack = arr r.ReadArray(&aer.Events) aer.FaultException = r.ReadString() - if aer.VMState&0x80 != 0 { + if aer.VMState&MostSignificantBit != 0 { r.ReadArray(&aer.Invocations) + aer.VMState &= ClearMostSignificantBitMask } } From 72caf56418429124e61a161afc48124b76aaefd4 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Thu, 23 Jan 2025 10:54:04 +0100 Subject: [PATCH 12/16] move code around --- pkg/core/state/contract_invocation.go | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/core/state/contract_invocation.go b/pkg/core/state/contract_invocation.go index bff5cfafff..444eee1b3e 100644 --- a/pkg/core/state/contract_invocation.go +++ b/pkg/core/state/contract_invocation.go @@ -9,17 +9,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) -// NewContractInvocation returns 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. @@ -35,6 +24,26 @@ type ContractInvocation struct { 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, truncated bool) *ContractInvocation { + return &ContractInvocation{ + Hash: hash, + Method: method, + argumentsBytes: argBytes, + ArgumentsCount: argCnt, + Truncated: truncated, + } +} + // DecodeBinary implements the Serializable interface. func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { ci.Hash.DecodeBinary(r) @@ -108,12 +117,3 @@ func (ci *ContractInvocation) UnmarshalJSON(data []byte) error { ci.argumentsBytes = argsBytes 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,omitempty"` - ArgumentsCount uint32 `json:"argumentscount"` - Truncated bool `json:"truncated"` -} From 4876e7056e4dfd87d9d0cca65da7aa597350fd0f Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Thu, 23 Jan 2025 10:54:52 +0100 Subject: [PATCH 13/16] document bit masks consts --- pkg/core/state/notification_event.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/core/state/notification_event.go b/pkg/core/state/notification_event.go index 0bdd8ed42e..00ed0986db 100644 --- a/pkg/core/state/notification_event.go +++ b/pkg/core/state/notification_event.go @@ -13,8 +13,10 @@ import ( ) const ( - MostSignificantBit = 0x80 - ClearMostSignificantBitMask = MostSignificantBit ^ 0xFF + // mostSignificantBit has the most significant bit of a uint8 set. + mostSignificantBit = 0x80 + // clearMostSignificantBitMask has the most significant bit of a uint8 not set. + clearMostSignificantBitMask = mostSignificantBit ^ 0xFF ) // NotificationEvent is a tuple of the scripthash that has emitted the Item as a @@ -85,7 +87,7 @@ func (aer *AppExecResult) EncodeBinaryWithContext(w *io.BinWriter, sc *stackitem w.WriteB(byte(aer.Trigger)) invocLen := len(aer.Invocations) if invocLen > 0 { - aer.VMState |= MostSignificantBit + aer.VMState |= mostSignificantBit } w.WriteB(byte(aer.VMState)) w.WriteU64LE(uint64(aer.GasConsumed)) @@ -135,9 +137,9 @@ func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { aer.Stack = arr r.ReadArray(&aer.Events) aer.FaultException = r.ReadString() - if aer.VMState&MostSignificantBit != 0 { + if aer.VMState&mostSignificantBit != 0 { r.ReadArray(&aer.Invocations) - aer.VMState &= ClearMostSignificantBitMask + aer.VMState &= clearMostSignificantBitMask } } From 036e6cb4e274fc2e5d50228606d25ebd06f7b3a5 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Thu, 23 Jan 2025 11:25:23 +0100 Subject: [PATCH 14/16] update encode/decode binary routines, merge binary tests --- pkg/core/state/contract_invocation.go | 10 +++-- pkg/core/state/contract_invocation_test.go | 43 +++++++--------------- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/pkg/core/state/contract_invocation.go b/pkg/core/state/contract_invocation.go index 444eee1b3e..61ca2add02 100644 --- a/pkg/core/state/contract_invocation.go +++ b/pkg/core/state/contract_invocation.go @@ -48,11 +48,11 @@ func NewContractInvocation(hash util.Uint160, method string, argBytes []byte, ar func (ci *ContractInvocation) DecodeBinary(r *io.BinReader) { ci.Hash.DecodeBinary(r) ci.Method = r.ReadString() - if b := r.ReadVarBytes(); len(b) > 0 { - ci.argumentsBytes = b - } ci.ArgumentsCount = r.ReadU32LE() ci.Truncated = r.ReadBool() + if !ci.Truncated { + ci.argumentsBytes = r.ReadVarBytes() + } } // EncodeBinary implements the Serializable interface. @@ -65,9 +65,11 @@ func (ci *ContractInvocation) EncodeBinary(w *io.BinWriter) { 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) + if !ci.Truncated { + w.WriteVarBytes(ci.argumentsBytes) + } } // MarshalJSON implements the json.Marshaler interface. diff --git a/pkg/core/state/contract_invocation_test.go b/pkg/core/state/contract_invocation_test.go index 5ffeb96f0c..e4cf177f8e 100644 --- a/pkg/core/state/contract_invocation_test.go +++ b/pkg/core/state/contract_invocation_test.go @@ -4,7 +4,7 @@ import ( "testing" json "github.com/nspcc-dev/go-ordered-json" - "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" @@ -37,32 +37,17 @@ func TestContractInvocation_JSON_ValidArguments(t *testing.T) { require.Equal(t, ci, &ci2) } -func TestContractInvocation_EncodeDecode_InvalidArguments(t *testing.T) { - ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, false) - - w := io.NewBufBinWriter() - ci.EncodeBinary(w.BinWriter) - require.NoError(t, w.Err) - - ciReader := io.NewBinReaderFromBuf(w.Bytes()) - ci2 := ContractInvocation{} - ci2.DecodeBinary(ciReader) - require.Equal(t, ci, &ci2) -} - -func TestContractInvocation_EncodeDecode_ValidArguments(t *testing.T) { - si := stackitem.NewArray([]stackitem.Item{stackitem.NewBool(false)}) - argBytes, err := stackitem.NewSerializationContext().Serialize(si, false) - require.NoError(t, err) - - ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1, false) - - w := io.NewBufBinWriter() - ci.EncodeBinary(w.BinWriter) - require.NoError(t, w.Err) - - ciReader := io.NewBinReaderFromBuf(w.Bytes()) - ci2 := ContractInvocation{} - ci2.DecodeBinary(ciReader) - require.Equal(t, ci, &ci2) +func TestContractInvocation_EncodeDecodeBinary(t *testing.T) { + t.Run("truncated", func(t *testing.T) { + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, true) + testserdes.EncodeDecodeBinary(t, ci, new(ContractInvocation)) + }) + t.Run("truncated", func(t *testing.T) { + si := stackitem.NewArray([]stackitem.Item{stackitem.NewBool(false)}) + argBytes, err := stackitem.NewSerializationContext().Serialize(si, false) + require.NoError(t, err) + + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1, false) + testserdes.EncodeDecodeBinary(t, ci, new(ContractInvocation)) + }) } From 7b83ff3558a0a91cfc6b7652c40a029fb2bec490 Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Thu, 23 Jan 2025 11:50:22 +0100 Subject: [PATCH 15/16] update marshal/unmarshal routines, merge marshalling tests --- pkg/core/state/contract_invocation.go | 7 ++-- pkg/core/state/contract_invocation_test.go | 48 +++++++++++----------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/pkg/core/state/contract_invocation.go b/pkg/core/state/contract_invocation.go index 61ca2add02..ba7a5c675a 100644 --- a/pkg/core/state/contract_invocation.go +++ b/pkg/core/state/contract_invocation.go @@ -100,7 +100,7 @@ func (ci *ContractInvocation) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, aux); err != nil { return err } - var argsBytes []byte + var args *stackitem.Array if aux.Arguments != nil { arguments, err := stackitem.FromJSONWithTypes(aux.Arguments) if err != nil { @@ -109,13 +109,12 @@ func (ci *ContractInvocation) UnmarshalJSON(data []byte) error { if t := arguments.Type(); t != stackitem.ArrayT { return fmt.Errorf("failed to convert invocation state of type %s to array", t.String()) } - args := arguments.(*stackitem.Array) - argsBytes, _ = stackitem.NewSerializationContext().Serialize(args, false) + args = arguments.(*stackitem.Array) } ci.Method = aux.Method ci.Hash = aux.Hash ci.ArgumentsCount = aux.ArgumentsCount ci.Truncated = aux.Truncated - ci.argumentsBytes = argsBytes + ci.Arguments = args return nil } diff --git a/pkg/core/state/contract_invocation_test.go b/pkg/core/state/contract_invocation_test.go index e4cf177f8e..cac921a971 100644 --- a/pkg/core/state/contract_invocation_test.go +++ b/pkg/core/state/contract_invocation_test.go @@ -10,31 +10,29 @@ import ( "github.com/stretchr/testify/require" ) -func TestContractInvocation_JSON_InvalidArguments(t *testing.T) { - ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, false) - - out, err := json.Marshal(&ci) - require.NoError(t, err) - - var ci2 ContractInvocation - err = json.Unmarshal(out, &ci2) - require.NoError(t, err) - require.Equal(t, ci, &ci2) -} - -func TestContractInvocation_JSON_ValidArguments(t *testing.T) { - si := stackitem.NewArray([]stackitem.Item{stackitem.NewBool(false)}) - argBytes, err := stackitem.NewSerializationContext().Serialize(si, false) - require.NoError(t, err) - - ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1, false) - out, err := json.Marshal(&ci) - require.NoError(t, err) +func TestContractInvocation_MarshalUnmarshalJSON(t *testing.T) { + t.Run("truncated", func(t *testing.T) { + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, true) + testserdes.MarshalUnmarshalJSON(t, ci, new(ContractInvocation)) + }) + t.Run("not truncated", func(t *testing.T) { + si := stackitem.NewArray([]stackitem.Item{stackitem.NewBool(false)}) + argBytes, err := stackitem.NewSerializationContext().Serialize(si, false) + require.NoError(t, err) - var ci2 ContractInvocation - err = json.Unmarshal(out, &ci2) - require.NoError(t, err) - require.Equal(t, ci, &ci2) + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1, false) + // Marshal and Unmarshal are asymmetric, test manually + out, err := json.Marshal(&ci) + require.NoError(t, err) + var ci2 ContractInvocation + err = json.Unmarshal(out, &ci2) + require.NoError(t, err) + require.Equal(t, ci.Hash, ci2.Hash) + require.Equal(t, ci.Method, ci2.Method) + require.Equal(t, ci.Truncated, ci2.Truncated) + require.Equal(t, ci.ArgumentsCount, ci2.ArgumentsCount) + require.Equal(t, si, ci2.Arguments) + }) } func TestContractInvocation_EncodeDecodeBinary(t *testing.T) { @@ -42,7 +40,7 @@ func TestContractInvocation_EncodeDecodeBinary(t *testing.T) { ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, true) testserdes.EncodeDecodeBinary(t, ci, new(ContractInvocation)) }) - t.Run("truncated", func(t *testing.T) { + t.Run("not truncated", func(t *testing.T) { si := stackitem.NewArray([]stackitem.Item{stackitem.NewBool(false)}) argBytes, err := stackitem.NewSerializationContext().Serialize(si, false) require.NoError(t, err) From fa29376590b2957bb9ce4d40e9c825344ba427ce Mon Sep 17 00:00:00 2001 From: Erik van den Brink Date: Thu, 23 Jan 2025 11:52:08 +0100 Subject: [PATCH 16/16] simplify NewContractInvocation --- pkg/core/interop/contract/call.go | 8 +++----- pkg/core/state/contract_invocation.go | 4 ++-- pkg/core/state/contract_invocation_test.go | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index d0e61e2f6d..7fa2bf97a8 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -72,15 +72,13 @@ func Call(ic *interop.Context) error { if ic.SaveInvocations { var ( - arrCount = len(args) - truncated bool - argBytes []byte + arrCount = len(args) + argBytes []byte ) if argBytes, err = stackitem.NewSerializationContext().Serialize(stackitem.NewArray(args), false); err != nil { - truncated = true argBytes = nil } - ci := state.NewContractInvocation(u, method, argBytes, uint32(arrCount), truncated) + ci := state.NewContractInvocation(u, method, argBytes, uint32(arrCount)) 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 index ba7a5c675a..6adacbeb6c 100644 --- a/pkg/core/state/contract_invocation.go +++ b/pkg/core/state/contract_invocation.go @@ -34,13 +34,13 @@ type contractInvocationAux struct { } // NewContractInvocation returns a new ContractInvocation. -func NewContractInvocation(hash util.Uint160, method string, argBytes []byte, argCnt uint32, truncated bool) *ContractInvocation { +func NewContractInvocation(hash util.Uint160, method string, argBytes []byte, argCnt uint32) *ContractInvocation { return &ContractInvocation{ Hash: hash, Method: method, argumentsBytes: argBytes, ArgumentsCount: argCnt, - Truncated: truncated, + Truncated: argBytes == nil, } } diff --git a/pkg/core/state/contract_invocation_test.go b/pkg/core/state/contract_invocation_test.go index cac921a971..eed8da3701 100644 --- a/pkg/core/state/contract_invocation_test.go +++ b/pkg/core/state/contract_invocation_test.go @@ -12,7 +12,7 @@ import ( func TestContractInvocation_MarshalUnmarshalJSON(t *testing.T) { t.Run("truncated", func(t *testing.T) { - ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, true) + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1) testserdes.MarshalUnmarshalJSON(t, ci, new(ContractInvocation)) }) t.Run("not truncated", func(t *testing.T) { @@ -20,7 +20,7 @@ func TestContractInvocation_MarshalUnmarshalJSON(t *testing.T) { argBytes, err := stackitem.NewSerializationContext().Serialize(si, false) require.NoError(t, err) - ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1, false) + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1) // Marshal and Unmarshal are asymmetric, test manually out, err := json.Marshal(&ci) require.NoError(t, err) @@ -37,7 +37,7 @@ func TestContractInvocation_MarshalUnmarshalJSON(t *testing.T) { func TestContractInvocation_EncodeDecodeBinary(t *testing.T) { t.Run("truncated", func(t *testing.T) { - ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, true) + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1) testserdes.EncodeDecodeBinary(t, ci, new(ContractInvocation)) }) t.Run("not truncated", func(t *testing.T) { @@ -45,7 +45,7 @@ func TestContractInvocation_EncodeDecodeBinary(t *testing.T) { argBytes, err := stackitem.NewSerializationContext().Serialize(si, false) require.NoError(t, err) - ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1, false) + ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", argBytes, 1) testserdes.EncodeDecodeBinary(t, ci, new(ContractInvocation)) }) }