Merge pull request #3805 from nspcc-dev/block-notifications

This commit is contained in:
Roman Khimov 2025-02-11 16:28:23 +03:00 committed by GitHub
commit a1db45d668
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 213 additions and 0 deletions

View file

@ -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

View file

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

View file

@ -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"`
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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) {