diff --git a/docs/rpc.md b/docs/rpc.md index 0c5f8c9870..3f4d373775 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -248,6 +248,15 @@ block. It can be removed in future versions, but at the moment you can use it to see how much GAS is burned with a particular block (because system fees are burned). +#### `getblocknotifications` call + +This method returns notifications from a block organized by trigger type. +Supports filtering by contract and event name (the same filter as provided +for subscriptions to execution results, see [notifications specification](notifications.md). +The resulting JSON is an object with three (if matched) field: "onpersist", +"application" and "postpersist" containing arrays of notifications (same JSON +as used in notification service) for the respective triggers. + #### Historic calls A set of `*historic` extension methods provide the ability of interacting with diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index babb69a392..0c6b897dd9 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -3168,3 +3168,8 @@ func (bc *Blockchain) GetStoragePrice() int64 { } return bc.contracts.Policy.GetStoragePriceInternal(bc.dao) } + +// GetTrimmedBlock returns a block with only the header and transaction hashes. +func (bc *Blockchain) GetTrimmedBlock(hash util.Uint256) (*block.Block, error) { + return bc.dao.GetBlock(hash) +} diff --git a/pkg/neorpc/result/block_notifications.go b/pkg/neorpc/result/block_notifications.go new file mode 100644 index 0000000000..44376ed7a7 --- /dev/null +++ b/pkg/neorpc/result/block_notifications.go @@ -0,0 +1,16 @@ +package result + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/state" +) + +// BlockNotifications represents notifications from a block organized by +// trigger type. +type BlockNotifications struct { + // Block-level execution _before_ any transactions. + OnPersist []state.ContainedNotificationEvent `json:"onpersist,omitempty"` + // Transaction execution. + Application []state.ContainedNotificationEvent `json:"application,omitempty"` + // Block-level execution _after_ all transactions. + PostPersist []state.ContainedNotificationEvent `json:"postpersist,omitempty"` +} diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index 2959c00f77..bfc03b3942 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -972,3 +972,12 @@ func (c *Client) GetRawNotaryPool() (*result.RawNotaryPool, error) { } return resp, nil } + +// GetBlockNotifications returns notifications from a block organized by trigger type. +func (c *Client) GetBlockNotifications(blockHash util.Uint256, filters ...*neorpc.NotificationFilter) (*result.BlockNotifications, error) { + var resp = &result.BlockNotifications{} + if err := c.performRequest("getblocknotifications", []any{blockHash.StringLE(), filters}, resp); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/services/rpcsrv/notification_comparator.go b/pkg/services/rpcsrv/notification_comparator.go new file mode 100644 index 0000000000..0660388bd2 --- /dev/null +++ b/pkg/services/rpcsrv/notification_comparator.go @@ -0,0 +1,65 @@ +package rpcsrv + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/neorpc/rpcevent" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" +) + +// notificationEventComparator is a comparator for notification events. +type notificationEventComparator struct { + filter neorpc.SubscriptionFilter +} + +// EventID returns the event ID for the notification event comparator. +func (s notificationEventComparator) EventID() neorpc.EventID { + return neorpc.NotificationEventID +} + +// Filter returns the filter for the notification event comparator. +func (c notificationEventComparator) Filter() neorpc.SubscriptionFilter { + return c.filter +} + +// notificationEventContainer is a container for a notification event. +type notificationEventContainer struct { + ntf *state.ContainedNotificationEvent +} + +// EventID returns the event ID for the notification event container. +func (c notificationEventContainer) EventID() neorpc.EventID { + return neorpc.NotificationEventID +} + +// EventPayload returns the payload for the notification event container. +func (c notificationEventContainer) EventPayload() any { + return c.ntf +} + +func processAppExecResults(aers []state.AppExecResult, filter *neorpc.NotificationFilter) []state.ContainedNotificationEvent { + var notifications []state.ContainedNotificationEvent + for _, aer := range aers { + if aer.VMState == vmstate.Halt { + notifications = append(notifications, filterEvents(aer.Events, aer.Container, filter)...) + } + } + return notifications +} + +func filterEvents(events []state.NotificationEvent, container util.Uint256, filter *neorpc.NotificationFilter) []state.ContainedNotificationEvent { + var notifications []state.ContainedNotificationEvent + for _, evt := range events { + ntf := state.ContainedNotificationEvent{ + Container: container, + NotificationEvent: evt, + } + if filter == nil || rpcevent.Matches(¬ificationEventComparator{ + filter: *filter, + }, ¬ificationEventContainer{ntf: &ntf}) { + notifications = append(notifications, ntf) + } + } + return notifications +} diff --git a/pkg/services/rpcsrv/server.go b/pkg/services/rpcsrv/server.go index 194ddf1624..b27502e0a2 100644 --- a/pkg/services/rpcsrv/server.go +++ b/pkg/services/rpcsrv/server.go @@ -112,6 +112,7 @@ type ( VerifyWitness(util.Uint160, hash.Hashable, *transaction.Witness, int64) (int64, error) mempool.Feer // fee interface ContractStorageSeeker + GetTrimmedBlock(hash util.Uint256) (*block.Block, error) } // ContractStorageSeeker is the interface `findstorage*` handlers need to be able to @@ -219,6 +220,7 @@ var rpcHandlers = map[string]func(*Server, params.Params) (any, *neorpc.Error){ "getblockhash": (*Server).getBlockHash, "getblockheader": (*Server).getBlockHeader, "getblockheadercount": (*Server).getBlockHeaderCount, + "getblocknotifications": (*Server).getBlockNotifications, "getblocksysfee": (*Server).getBlockSysFee, "getcandidates": (*Server).getCandidates, "getcommittee": (*Server).getCommittee, @@ -3202,3 +3204,59 @@ func (s *Server) getRawNotaryTransaction(reqParams params.Params) (any, *neorpc. } return tx.Bytes(), nil } + +// getBlockNotifications returns notifications from a specific block with optional filtering. +func (s *Server) getBlockNotifications(reqParams params.Params) (any, *neorpc.Error) { + param := reqParams.Value(0) + hash, respErr := s.blockHashFromParam(param) + if respErr != nil { + return nil, respErr + } + + var filter *neorpc.NotificationFilter + if len(reqParams) > 1 { + var ( + reader = bytes.NewBuffer([]byte(reqParams[1].RawMessage)) + decoder = json.NewDecoder(reader) + ) + decoder.DisallowUnknownFields() + filter = new(neorpc.NotificationFilter) + + err := decoder.Decode(filter) + if err != nil { + return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("invalid filter: %s", err)) + } + if err := filter.IsValid(); err != nil { + return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("invalid filter: %s", err)) + } + } + + block, err := s.chain.GetTrimmedBlock(hash) + if err != nil { + return nil, neorpc.ErrUnknownBlock + } + + notifications := &result.BlockNotifications{} + + aers, err := s.chain.GetAppExecResults(block.Hash(), trigger.OnPersist) + if err != nil { + return nil, neorpc.NewInternalServerError("failed to get app exec results for onpersist") + } + notifications.OnPersist = processAppExecResults([]state.AppExecResult{aers[0]}, filter) + + for _, txHash := range block.Transactions { + aers, err := s.chain.GetAppExecResults(txHash.Hash(), trigger.Application) + if err != nil { + return nil, neorpc.NewInternalServerError("failed to get app exec results") + } + notifications.Application = append(notifications.Application, processAppExecResults(aers, filter)...) + } + + aers, err = s.chain.GetAppExecResults(block.Hash(), trigger.PostPersist) + if err != nil { + return nil, neorpc.NewInternalServerError("failed to get app exec results for postpersist") + } + notifications.PostPersist = processAppExecResults([]state.AppExecResult{aers[0]}, filter) + + return notifications, nil +} diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index 36d30df45c..4b994c3153 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -31,6 +31,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativehashes" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage/dboper" @@ -2274,6 +2275,56 @@ var rpcTestCases = map[string][]rpcTestCase{ errCode: neorpc.InvalidParamsCode, }, }, + "getblocknotifications": { + { + name: "positive", + params: `["` + genesisBlockHash + `"]`, + result: func(e *executor) any { return &result.BlockNotifications{} }, + check: func(t *testing.T, e *executor, acc any) { + res, ok := acc.(*result.BlockNotifications) + require.True(t, ok) + require.NotNil(t, res) + }, + }, + { + name: "positive with filter", + params: `["` + genesisBlockHash + `", {"contract":"` + nativehashes.NeoToken.StringLE() + `", "name":"Transfer"}]`, + result: func(e *executor) any { return &result.BlockNotifications{} }, + check: func(t *testing.T, e *executor, acc any) { + res, ok := acc.(*result.BlockNotifications) + require.True(t, ok) + require.NotNil(t, res) + for _, ne := range res.Application { + require.Equal(t, nativehashes.NeoToken, ne.ScriptHash) + require.Equal(t, "Transfer", ne.Name) + } + }, + }, + { + name: "invalid hash", + params: `["invalid"]`, + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "unknown block", + params: `["` + util.Uint256{}.StringLE() + `"]`, + fail: true, + errCode: neorpc.ErrUnknownBlockCode, + }, + { + name: "invalid filter", + params: `["` + genesisBlockHash + `", {"contract":"invalid"}]`, + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + { + name: "filter with unknown fields", + params: `["` + genesisBlockHash + `", {"invalid":"something"}]`, + fail: true, + errCode: neorpc.InvalidParamsCode, + }, + }, } func TestRPC(t *testing.T) {