Skip to content

Commit

Permalink
Merge pull request #3805 from nspcc-dev/block-notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
roman-khimov authored Feb 11, 2025
2 parents 8d728b4 + 25e2d80 commit a1db45d
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 0 deletions.
9 changes: 9 additions & 0 deletions docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
16 changes: 16 additions & 0 deletions pkg/neorpc/result/block_notifications.go
Original file line number Diff line number Diff line change
@@ -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"`
}
9 changes: 9 additions & 0 deletions pkg/rpcclient/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
65 changes: 65 additions & 0 deletions pkg/services/rpcsrv/notification_comparator.go
Original file line number Diff line number Diff line change
@@ -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(&notificationEventComparator{
filter: *filter,
}, &notificationEventContainer{ntf: &ntf}) {
notifications = append(notifications, ntf)
}
}
return notifications
}
58 changes: 58 additions & 0 deletions pkg/services/rpcsrv/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
51 changes: 51 additions & 0 deletions pkg/services/rpcsrv/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit a1db45d

Please sign in to comment.