Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

*: add invocations to applicationlog #3569

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/node-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ node-related settings described in the table below.
| SaveStorageBatch | `bool` | `false` | Enables storage batch saving before every persist. It is similar to StorageDump plugin for C# node. |
| SkipBlockVerification | `bool` | `false` | Allows to disable verification of received/processed blocks (including cryptographic checks). |
| StateRoot | [State Root Configuration](#State-Root-Configuration) | | State root module configuration. See the [State Root Configuration](#State-Root-Configuration) section for details. |
| SaveInvocations | `bool` | `false` | Determines if additional smart contract invocation details are stored. If enabled, the `getapplicationlog` RPC method will return a new field with invocation details for the transaction. See the [RPC](rpc.md#applicationlog-invocations) documentation for more information. |

### P2P Configuration

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

BotlDB is also known to be incompatible between machines with different
endianness. Nothing is known for LevelDB wrt this, so it's not recommended
Expand Down
46 changes: 46 additions & 0 deletions docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with "getapplicationlog call" to follow the style of other RPC call extension headers?


The `SaveInvocations` node configuration setting stores smart contract invocation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/node/RPC server

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/stores/makes the node to store

or something like this.

details into the application logs under the `invocations` key. This feature is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/into the application logs under the invocations key/as a part of application log

The applog DB key is a pure internal thing, and I'd say it's not related to external user.

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": [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's attach the full response for getapplicationlog call and extend Example: description a bit, otherwise it's not clear what is the source of this JSON.

{
"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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, describe the exact size constraint that is used to filter out large notifications. Otherwise users don't have any other way to learn about this constraint other than code-level check.

`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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a couple of words about ordering and flat structure of invocations entries.


## Reference

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

// Blockchain is a set of settings for core.Blockchain to use, it includes protocol
Expand Down
6 changes: 6 additions & 0 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ func (bc *Blockchain) init() error {
KeepOnlyLatestState: bc.config.Ledger.KeepOnlyLatestState,
Magic: uint32(bc.config.Magic),
Value: version,
SaveInvocations: bc.config.SaveInvocations,
}
bc.dao.PutVersion(ver)
bc.dao.Version = ver
Expand Down Expand Up @@ -454,6 +455,10 @@ func (bc *Blockchain) init() error {
return fmt.Errorf("protocol configuration Magic mismatch (old=%v, new=%v)",
ver.Magic, bc.config.Magic)
}
if ver.SaveInvocations != bc.config.SaveInvocations {
return fmt.Errorf("SaveInvocations setting mismatch (old=%v, new=%v)",
ver.SaveInvocations, bc.config.SaveInvocations)
}
bc.dao.Version = ver
bc.persistent.Version = ver

Expand Down Expand Up @@ -1717,6 +1722,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
Stack: v.Estack().ToArray(),
Events: systemInterop.Notifications,
FaultException: faultException,
Invocations: systemInterop.InvocationCalls,
},
}
appExecResults = append(appExecResults, aer)
Expand Down
6 changes: 6 additions & 0 deletions pkg/core/dao/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,13 +448,15 @@ type Version struct {
KeepOnlyLatestState bool
Magic uint32
Value string
SaveInvocations bool
}

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

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

m := i + 3
if len(data) == m+4 {
Expand All @@ -505,6 +508,9 @@ func (v *Version) Bytes() []byte {
if v.KeepOnlyLatestState {
mask |= keepOnlyLatestStateBit
}
if v.SaveInvocations {
mask |= saveInvocationsBit
}
res := append([]byte(v.Value), '\x00', byte(v.StoragePrefix), mask)
res = binary.LittleEndian.AppendUint32(res, v.Magic)
return res
Expand Down
17 changes: 14 additions & 3 deletions pkg/core/dao/dao_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,26 @@ 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{},
Stack: []stackitem.Item{},
Events: []state.NotificationEvent{
{
ScriptHash: util.Uint160{},
Name: "fakeTransferEvent",
Item: si,
},
},
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)
Expand Down
33 changes: 18 additions & 15 deletions pkg/core/interop/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ type Context struct {
VM *vm.VM
Functions []Function
Invocations map[util.Uint160]int
InvocationCalls []state.ContractInvocation
ixje marked this conversation as resolved.
Show resolved Hide resolved
cancelFuncs []context.CancelFunc
getContract func(*dao.Simple, util.Uint160) (*state.Contract, error)
baseExecFee int64
baseStorageFee int64
loadToken func(ic *Context, id int32) error
GetRandomCounter uint32
signers []transaction.Signer
SaveInvocations bool
}

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

Expand Down
13 changes: 13 additions & 0 deletions pkg/core/interop/contract/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ func Call(ic *interop.Context) error {
return fmt.Errorf("method not found: %s/%d", method, len(args))
}
hasReturn := md.ReturnType != smartcontract.VoidType

if ic.SaveInvocations {
var (
arrCount = len(args)
truncated = false
argBytes []byte
)
if argBytes, err = ic.DAO.GetItemCtx().Serialize(stackitem.NewArray(args), false); err != nil {
truncated = true
}
ci := state.NewContractInvocation(u, method, argBytes, uint32(arrCount), truncated)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argBytes can't be used if err is not nil, because it contains half-serialized data in this case. Explicitly set argBytes to nil in case of non-nil err.

ic.InvocationCalls = append(ic.InvocationCalls, *ci)
}
return callInternal(ic, cs, method, fs, hasReturn, args, true)
}

Expand Down
109 changes: 109 additions & 0 deletions pkg/core/state/contract_invocation.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argumentsBytes may be empty, then deserialization won't work for it. Add a test for storing/retrieving invocation that contains too large number of arguments.

}
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to handle the case of invalid type ([]byte(fmt.Sprintf("error: %v", err)) set in the marshaller). Try to unmarshal aux.Arguments into string and continue execution flow if unmarshalling is successful (exactly like for state.Execution unmarshalling. Let's add Encode -> Decode -> marshal JSON -> unmarshal JSON unittest for ConstructIvocation structure for the case when argumentsBytes is nil.

}
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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use omitempty tag?

ArgumentsCount uint32 `json:"argumentscount"`
Truncated bool `json:"truncated"`
}
Loading
Loading