Skip to content

Commit a1db45d

Browse files
authored
Merge pull request #3805 from nspcc-dev/block-notifications
2 parents 8d728b4 + 25e2d80 commit a1db45d

File tree

7 files changed

+213
-0
lines changed

7 files changed

+213
-0
lines changed

docs/rpc.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,15 @@ block. It can be removed in future versions, but at the moment you can use it
248248
to see how much GAS is burned with a particular block (because system fees are
249249
burned).
250250

251+
#### `getblocknotifications` call
252+
253+
This method returns notifications from a block organized by trigger type.
254+
Supports filtering by contract and event name (the same filter as provided
255+
for subscriptions to execution results, see [notifications specification](notifications.md).
256+
The resulting JSON is an object with three (if matched) field: "onpersist",
257+
"application" and "postpersist" containing arrays of notifications (same JSON
258+
as used in notification service) for the respective triggers.
259+
251260
#### Historic calls
252261

253262
A set of `*historic` extension methods provide the ability of interacting with

pkg/core/blockchain.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3168,3 +3168,8 @@ func (bc *Blockchain) GetStoragePrice() int64 {
31683168
}
31693169
return bc.contracts.Policy.GetStoragePriceInternal(bc.dao)
31703170
}
3171+
3172+
// GetTrimmedBlock returns a block with only the header and transaction hashes.
3173+
func (bc *Blockchain) GetTrimmedBlock(hash util.Uint256) (*block.Block, error) {
3174+
return bc.dao.GetBlock(hash)
3175+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package result
2+
3+
import (
4+
"github.com/nspcc-dev/neo-go/pkg/core/state"
5+
)
6+
7+
// BlockNotifications represents notifications from a block organized by
8+
// trigger type.
9+
type BlockNotifications struct {
10+
// Block-level execution _before_ any transactions.
11+
OnPersist []state.ContainedNotificationEvent `json:"onpersist,omitempty"`
12+
// Transaction execution.
13+
Application []state.ContainedNotificationEvent `json:"application,omitempty"`
14+
// Block-level execution _after_ all transactions.
15+
PostPersist []state.ContainedNotificationEvent `json:"postpersist,omitempty"`
16+
}

pkg/rpcclient/rpc.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,3 +972,12 @@ func (c *Client) GetRawNotaryPool() (*result.RawNotaryPool, error) {
972972
}
973973
return resp, nil
974974
}
975+
976+
// GetBlockNotifications returns notifications from a block organized by trigger type.
977+
func (c *Client) GetBlockNotifications(blockHash util.Uint256, filters ...*neorpc.NotificationFilter) (*result.BlockNotifications, error) {
978+
var resp = &result.BlockNotifications{}
979+
if err := c.performRequest("getblocknotifications", []any{blockHash.StringLE(), filters}, resp); err != nil {
980+
return nil, err
981+
}
982+
return resp, nil
983+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package rpcsrv
2+
3+
import (
4+
"github.com/nspcc-dev/neo-go/pkg/core/state"
5+
"github.com/nspcc-dev/neo-go/pkg/neorpc"
6+
"github.com/nspcc-dev/neo-go/pkg/neorpc/rpcevent"
7+
"github.com/nspcc-dev/neo-go/pkg/util"
8+
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
9+
)
10+
11+
// notificationEventComparator is a comparator for notification events.
12+
type notificationEventComparator struct {
13+
filter neorpc.SubscriptionFilter
14+
}
15+
16+
// EventID returns the event ID for the notification event comparator.
17+
func (s notificationEventComparator) EventID() neorpc.EventID {
18+
return neorpc.NotificationEventID
19+
}
20+
21+
// Filter returns the filter for the notification event comparator.
22+
func (c notificationEventComparator) Filter() neorpc.SubscriptionFilter {
23+
return c.filter
24+
}
25+
26+
// notificationEventContainer is a container for a notification event.
27+
type notificationEventContainer struct {
28+
ntf *state.ContainedNotificationEvent
29+
}
30+
31+
// EventID returns the event ID for the notification event container.
32+
func (c notificationEventContainer) EventID() neorpc.EventID {
33+
return neorpc.NotificationEventID
34+
}
35+
36+
// EventPayload returns the payload for the notification event container.
37+
func (c notificationEventContainer) EventPayload() any {
38+
return c.ntf
39+
}
40+
41+
func processAppExecResults(aers []state.AppExecResult, filter *neorpc.NotificationFilter) []state.ContainedNotificationEvent {
42+
var notifications []state.ContainedNotificationEvent
43+
for _, aer := range aers {
44+
if aer.VMState == vmstate.Halt {
45+
notifications = append(notifications, filterEvents(aer.Events, aer.Container, filter)...)
46+
}
47+
}
48+
return notifications
49+
}
50+
51+
func filterEvents(events []state.NotificationEvent, container util.Uint256, filter *neorpc.NotificationFilter) []state.ContainedNotificationEvent {
52+
var notifications []state.ContainedNotificationEvent
53+
for _, evt := range events {
54+
ntf := state.ContainedNotificationEvent{
55+
Container: container,
56+
NotificationEvent: evt,
57+
}
58+
if filter == nil || rpcevent.Matches(&notificationEventComparator{
59+
filter: *filter,
60+
}, &notificationEventContainer{ntf: &ntf}) {
61+
notifications = append(notifications, ntf)
62+
}
63+
}
64+
return notifications
65+
}

pkg/services/rpcsrv/server.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ type (
112112
VerifyWitness(util.Uint160, hash.Hashable, *transaction.Witness, int64) (int64, error)
113113
mempool.Feer // fee interface
114114
ContractStorageSeeker
115+
GetTrimmedBlock(hash util.Uint256) (*block.Block, error)
115116
}
116117

117118
// 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){
219220
"getblockhash": (*Server).getBlockHash,
220221
"getblockheader": (*Server).getBlockHeader,
221222
"getblockheadercount": (*Server).getBlockHeaderCount,
223+
"getblocknotifications": (*Server).getBlockNotifications,
222224
"getblocksysfee": (*Server).getBlockSysFee,
223225
"getcandidates": (*Server).getCandidates,
224226
"getcommittee": (*Server).getCommittee,
@@ -3202,3 +3204,59 @@ func (s *Server) getRawNotaryTransaction(reqParams params.Params) (any, *neorpc.
32023204
}
32033205
return tx.Bytes(), nil
32043206
}
3207+
3208+
// getBlockNotifications returns notifications from a specific block with optional filtering.
3209+
func (s *Server) getBlockNotifications(reqParams params.Params) (any, *neorpc.Error) {
3210+
param := reqParams.Value(0)
3211+
hash, respErr := s.blockHashFromParam(param)
3212+
if respErr != nil {
3213+
return nil, respErr
3214+
}
3215+
3216+
var filter *neorpc.NotificationFilter
3217+
if len(reqParams) > 1 {
3218+
var (
3219+
reader = bytes.NewBuffer([]byte(reqParams[1].RawMessage))
3220+
decoder = json.NewDecoder(reader)
3221+
)
3222+
decoder.DisallowUnknownFields()
3223+
filter = new(neorpc.NotificationFilter)
3224+
3225+
err := decoder.Decode(filter)
3226+
if err != nil {
3227+
return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("invalid filter: %s", err))
3228+
}
3229+
if err := filter.IsValid(); err != nil {
3230+
return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("invalid filter: %s", err))
3231+
}
3232+
}
3233+
3234+
block, err := s.chain.GetTrimmedBlock(hash)
3235+
if err != nil {
3236+
return nil, neorpc.ErrUnknownBlock
3237+
}
3238+
3239+
notifications := &result.BlockNotifications{}
3240+
3241+
aers, err := s.chain.GetAppExecResults(block.Hash(), trigger.OnPersist)
3242+
if err != nil {
3243+
return nil, neorpc.NewInternalServerError("failed to get app exec results for onpersist")
3244+
}
3245+
notifications.OnPersist = processAppExecResults([]state.AppExecResult{aers[0]}, filter)
3246+
3247+
for _, txHash := range block.Transactions {
3248+
aers, err := s.chain.GetAppExecResults(txHash.Hash(), trigger.Application)
3249+
if err != nil {
3250+
return nil, neorpc.NewInternalServerError("failed to get app exec results")
3251+
}
3252+
notifications.Application = append(notifications.Application, processAppExecResults(aers, filter)...)
3253+
}
3254+
3255+
aers, err = s.chain.GetAppExecResults(block.Hash(), trigger.PostPersist)
3256+
if err != nil {
3257+
return nil, neorpc.NewInternalServerError("failed to get app exec results for postpersist")
3258+
}
3259+
notifications.PostPersist = processAppExecResults([]state.AppExecResult{aers[0]}, filter)
3260+
3261+
return notifications, nil
3262+
}

pkg/services/rpcsrv/server_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/nspcc-dev/neo-go/pkg/core/block"
3232
"github.com/nspcc-dev/neo-go/pkg/core/fee"
3333
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
34+
"github.com/nspcc-dev/neo-go/pkg/core/native/nativehashes"
3435
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
3536
"github.com/nspcc-dev/neo-go/pkg/core/state"
3637
"github.com/nspcc-dev/neo-go/pkg/core/storage/dboper"
@@ -2274,6 +2275,56 @@ var rpcTestCases = map[string][]rpcTestCase{
22742275
errCode: neorpc.InvalidParamsCode,
22752276
},
22762277
},
2278+
"getblocknotifications": {
2279+
{
2280+
name: "positive",
2281+
params: `["` + genesisBlockHash + `"]`,
2282+
result: func(e *executor) any { return &result.BlockNotifications{} },
2283+
check: func(t *testing.T, e *executor, acc any) {
2284+
res, ok := acc.(*result.BlockNotifications)
2285+
require.True(t, ok)
2286+
require.NotNil(t, res)
2287+
},
2288+
},
2289+
{
2290+
name: "positive with filter",
2291+
params: `["` + genesisBlockHash + `", {"contract":"` + nativehashes.NeoToken.StringLE() + `", "name":"Transfer"}]`,
2292+
result: func(e *executor) any { return &result.BlockNotifications{} },
2293+
check: func(t *testing.T, e *executor, acc any) {
2294+
res, ok := acc.(*result.BlockNotifications)
2295+
require.True(t, ok)
2296+
require.NotNil(t, res)
2297+
for _, ne := range res.Application {
2298+
require.Equal(t, nativehashes.NeoToken, ne.ScriptHash)
2299+
require.Equal(t, "Transfer", ne.Name)
2300+
}
2301+
},
2302+
},
2303+
{
2304+
name: "invalid hash",
2305+
params: `["invalid"]`,
2306+
fail: true,
2307+
errCode: neorpc.InvalidParamsCode,
2308+
},
2309+
{
2310+
name: "unknown block",
2311+
params: `["` + util.Uint256{}.StringLE() + `"]`,
2312+
fail: true,
2313+
errCode: neorpc.ErrUnknownBlockCode,
2314+
},
2315+
{
2316+
name: "invalid filter",
2317+
params: `["` + genesisBlockHash + `", {"contract":"invalid"}]`,
2318+
fail: true,
2319+
errCode: neorpc.InvalidParamsCode,
2320+
},
2321+
{
2322+
name: "filter with unknown fields",
2323+
params: `["` + genesisBlockHash + `", {"invalid":"something"}]`,
2324+
fail: true,
2325+
errCode: neorpc.InvalidParamsCode,
2326+
},
2327+
},
22772328
}
22782329

22792330
func TestRPC(t *testing.T) {

0 commit comments

Comments
 (0)