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

Block notifications #3805

Merged
merged 5 commits into from
Feb 11, 2025
Merged
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
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 @@
}
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

Check warning on line 982 in pkg/rpcclient/rpc.go

View check run for this annotation

Codecov / codecov/patch

pkg/rpcclient/rpc.go#L977-L982

Added lines #L977 - L982 were not covered by tests
}
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 @@
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 @@
"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 @@
}
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))
}

Check warning on line 3231 in pkg/services/rpcsrv/server.go

View check run for this annotation

Codecov / codecov/patch

pkg/services/rpcsrv/server.go#L3230-L3231

Added lines #L3230 - L3231 were not covered by tests
}

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")
}

Check warning on line 3244 in pkg/services/rpcsrv/server.go

View check run for this annotation

Codecov / codecov/patch

pkg/services/rpcsrv/server.go#L3243-L3244

Added lines #L3243 - L3244 were not covered by tests
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)...)

Check warning on line 3252 in pkg/services/rpcsrv/server.go

View check run for this annotation

Codecov / codecov/patch

pkg/services/rpcsrv/server.go#L3248-L3252

Added lines #L3248 - L3252 were not covered by tests
}

aers, err = s.chain.GetAppExecResults(block.Hash(), trigger.PostPersist)
if err != nil {
return nil, neorpc.NewInternalServerError("failed to get app exec results for postpersist")
}

Check warning on line 3258 in pkg/services/rpcsrv/server.go

View check run for this annotation

Codecov / codecov/patch

pkg/services/rpcsrv/server.go#L3257-L3258

Added lines #L3257 - L3258 were not covered by tests
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
Loading