Skip to content

Commit dee62f4

Browse files
committed
Get block notifications API
1 parent 16a6a59 commit dee62f4

File tree

11 files changed

+399
-0
lines changed

11 files changed

+399
-0
lines changed

docs/rpc.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,10 @@ 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. Supports filtering by contract and event name.
254+
251255
#### Historic calls
252256

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

pkg/core/block/block.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ type auxBlockIn struct {
5151
Transactions []json.RawMessage `json:"tx"`
5252
}
5353

54+
// TrimmedBlock é uma versão mais leve do Block que contém apenas os hashes das transações
55+
type TrimmedBlock struct {
56+
Header
57+
TxHashes []util.Uint256
58+
}
59+
60+
// auxTrimmedBlockOut é usado para JSON i/o.
61+
type auxTrimmedBlockOut struct {
62+
TxHashes []util.Uint256 `json:"tx"`
63+
}
64+
65+
// auxTrimmedBlockIn é usado para JSON i/o.
66+
type auxTrimmedBlockIn struct {
67+
TxHashes []json.RawMessage `json:"tx"`
68+
}
69+
5470
// ComputeMerkleRoot computes Merkle tree root hash based on actual block's data.
5571
func (b *Block) ComputeMerkleRoot() util.Uint256 {
5672
hashes := make([]util.Uint256, len(b.Transactions))
@@ -241,3 +257,97 @@ func (b *Block) ToStackItem() stackitem.Item {
241257

242258
return stackitem.NewArray(items)
243259
}
260+
261+
// NewTrimmedBlockFromReader creates a block with only the header and transaction hashes
262+
func NewTrimmedBlockFromReader(stateRootEnabled bool, br *io.BinReader) (*TrimmedBlock, error) {
263+
block := &TrimmedBlock{
264+
Header: Header{
265+
StateRootEnabled: stateRootEnabled,
266+
},
267+
}
268+
269+
block.Header.DecodeBinary(br)
270+
lenHashes := br.ReadVarUint()
271+
if lenHashes > MaxTransactionsPerBlock {
272+
return nil, ErrMaxContentsPerBlock
273+
}
274+
if lenHashes > 0 {
275+
block.TxHashes = make([]util.Uint256, lenHashes)
276+
for i := range lenHashes {
277+
block.TxHashes[i].DecodeBinary(br)
278+
}
279+
}
280+
281+
return block, br.Err
282+
}
283+
284+
func (b TrimmedBlock) MarshalJSON() ([]byte, error) {
285+
abo := auxTrimmedBlockOut{
286+
TxHashes: b.TxHashes,
287+
}
288+
289+
if abo.TxHashes == nil {
290+
abo.TxHashes = []util.Uint256{}
291+
}
292+
auxb, err := json.Marshal(abo)
293+
if err != nil {
294+
return nil, err
295+
}
296+
baseBytes, err := json.Marshal(b.Header)
297+
if err != nil {
298+
return nil, err
299+
}
300+
301+
// Does as the 'normal' block does
302+
if baseBytes[len(baseBytes)-1] != '}' || auxb[0] != '{' {
303+
return nil, errors.New("can't merge internal jsons")
304+
}
305+
baseBytes[len(baseBytes)-1] = ','
306+
baseBytes = append(baseBytes, auxb[1:]...)
307+
return baseBytes, nil
308+
}
309+
310+
// UnmarshalJSON implementa a interface json.Unmarshaler.
311+
func (b *TrimmedBlock) UnmarshalJSON(data []byte) error {
312+
// Similar ao Block normal, faz unmarshalling separado para Header e hashes
313+
auxb := new(auxTrimmedBlockIn)
314+
err := json.Unmarshal(data, auxb)
315+
if err != nil {
316+
return err
317+
}
318+
err = json.Unmarshal(data, &b.Header)
319+
if err != nil {
320+
return err
321+
}
322+
if len(auxb.TxHashes) != 0 {
323+
b.TxHashes = make([]util.Uint256, len(auxb.TxHashes))
324+
for i, hashBytes := range auxb.TxHashes {
325+
err = json.Unmarshal(hashBytes, &b.TxHashes[i])
326+
if err != nil {
327+
return err
328+
}
329+
}
330+
}
331+
return nil
332+
}
333+
334+
// ToStackItem converte TrimmedBlock para stackitem.Item.
335+
func (b *TrimmedBlock) ToStackItem() stackitem.Item {
336+
items := []stackitem.Item{
337+
stackitem.NewByteArray(b.Hash().BytesBE()),
338+
stackitem.NewBigInteger(big.NewInt(int64(b.Version))),
339+
stackitem.NewByteArray(b.PrevHash.BytesBE()),
340+
stackitem.NewByteArray(b.MerkleRoot.BytesBE()),
341+
stackitem.NewBigInteger(big.NewInt(int64(b.Timestamp))),
342+
stackitem.NewBigInteger(new(big.Int).SetUint64(b.Nonce)),
343+
stackitem.NewBigInteger(big.NewInt(int64(b.Index))),
344+
stackitem.NewBigInteger(big.NewInt(int64(b.PrimaryIndex))),
345+
stackitem.NewByteArray(b.NextConsensus.BytesBE()),
346+
stackitem.NewBigInteger(big.NewInt(int64(len(b.TxHashes)))),
347+
}
348+
if b.StateRootEnabled {
349+
items = append(items, stackitem.NewByteArray(b.PrevStateRoot.BytesBE()))
350+
}
351+
352+
return stackitem.NewArray(items)
353+
}

pkg/core/block/block_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,47 @@ func TestTrimmedBlock(t *testing.T) {
7979
}
8080
}
8181

82+
func TestNewTrimmedBlockFromReader(t *testing.T) {
83+
block := getDecodedBlock(t, 1)
84+
85+
buf := io.NewBufBinWriter()
86+
block.EncodeTrimmed(buf.BinWriter)
87+
require.NoError(t, buf.Err)
88+
89+
r := io.NewBinReaderFromBuf(buf.Bytes())
90+
trimmedBlock, err := NewTrimmedBlockFromReader(false, r)
91+
require.NoError(t, err)
92+
93+
assert.Equal(t, block.Version, trimmedBlock.Version)
94+
assert.Equal(t, block.PrevHash, trimmedBlock.PrevHash)
95+
assert.Equal(t, block.MerkleRoot, trimmedBlock.MerkleRoot)
96+
assert.Equal(t, block.Timestamp, trimmedBlock.Timestamp)
97+
assert.Equal(t, block.Index, trimmedBlock.Index)
98+
assert.Equal(t, block.NextConsensus, trimmedBlock.NextConsensus)
99+
100+
assert.Equal(t, block.Script, trimmedBlock.Script)
101+
assert.Equal(t, len(block.Transactions), len(trimmedBlock.TxHashes))
102+
for i := range block.Transactions {
103+
assert.Equal(t, block.Transactions[i].Hash(), trimmedBlock.TxHashes[i])
104+
}
105+
106+
data, err := json.Marshal(trimmedBlock)
107+
require.NoError(t, err)
108+
109+
var decoded TrimmedBlock
110+
err = json.Unmarshal(data, &decoded)
111+
require.NoError(t, err)
112+
113+
assert.Equal(t, trimmedBlock.Version, decoded.Version)
114+
assert.Equal(t, trimmedBlock.PrevHash, decoded.PrevHash)
115+
assert.Equal(t, trimmedBlock.MerkleRoot, decoded.MerkleRoot)
116+
assert.Equal(t, trimmedBlock.Timestamp, decoded.Timestamp)
117+
assert.Equal(t, trimmedBlock.Index, decoded.Index)
118+
assert.Equal(t, trimmedBlock.NextConsensus, decoded.NextConsensus)
119+
assert.Equal(t, trimmedBlock.Script, decoded.Script)
120+
assert.Equal(t, trimmedBlock.TxHashes, decoded.TxHashes)
121+
}
122+
82123
func newDumbBlock() *Block {
83124
return &Block{
84125
Header: Header{

pkg/core/blockchain.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3149,3 +3149,8 @@ func (bc *Blockchain) GetStoragePrice() int64 {
31493149
}
31503150
return bc.contracts.Policy.GetStoragePriceInternal(bc.dao)
31513151
}
3152+
3153+
// GetTrimmedBlock returns a block with only the header and transaction hashes
3154+
func (bc *Blockchain) GetTrimmedBlock(hash util.Uint256) (*block.TrimmedBlock, error) {
3155+
return bc.dao.GetTrimmedBlock(hash)
3156+
}

pkg/core/dao/dao.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,21 @@ func (dao *Simple) getBlock(key []byte) (*block.Block, error) {
439439
return block, nil
440440
}
441441

442+
// GetTrimmedBlock returns a block with only the header and transaction hashes
443+
func (dao *Simple) GetTrimmedBlock(hash util.Uint256) (*block.TrimmedBlock, error) {
444+
key := dao.makeExecutableKey(hash)
445+
b, err := dao.Store.Get(key)
446+
if err != nil {
447+
return nil, err
448+
}
449+
450+
r := io.NewBinReaderFromBuf(b)
451+
if r.ReadB() != storage.ExecBlock {
452+
return nil, storage.ErrKeyNotFound
453+
}
454+
return block.NewTrimmedBlockFromReader(dao.Version.StateRootInHeader, r)
455+
}
456+
442457
// Version represents the current dao version.
443458
type Version struct {
444459
StoragePrefix storage.KeyPrefix

pkg/core/dao/dao_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/nspcc-dev/neo-go/pkg/util"
1515
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
1616
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
17+
"github.com/stretchr/testify/assert"
1718
"github.com/stretchr/testify/require"
1819
)
1920

@@ -123,6 +124,55 @@ func TestPutGetBlock(t *testing.T) {
123124
require.Error(t, err)
124125
}
125126

127+
func TestGetTrimmedBlock(t *testing.T) {
128+
dao := NewSimple(storage.NewMemoryStore(), false)
129+
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
130+
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
131+
tx.Scripts = []transaction.Witness{{}}
132+
133+
b := &block.Block{
134+
Header: block.Header{
135+
Timestamp: 42,
136+
Script: transaction.Witness{
137+
VerificationScript: []byte{byte(opcode.PUSH1)},
138+
InvocationScript: []byte{byte(opcode.NOP)},
139+
},
140+
},
141+
Transactions: []*transaction.Transaction{tx},
142+
}
143+
hash := b.Hash()
144+
appExecResult1 := &state.AppExecResult{
145+
Container: hash,
146+
Execution: state.Execution{
147+
Trigger: trigger.OnPersist,
148+
Events: []state.NotificationEvent{},
149+
Stack: []stackitem.Item{},
150+
},
151+
}
152+
err := dao.StoreAsBlock(b, appExecResult1, nil)
153+
require.NoError(t, err)
154+
155+
trimmedBlock, err := dao.GetTrimmedBlock(hash)
156+
require.NoError(t, err)
157+
require.NotNil(t, trimmedBlock)
158+
159+
assert.Equal(t, b.Version, trimmedBlock.Version)
160+
assert.Equal(t, b.PrevHash, trimmedBlock.PrevHash)
161+
assert.Equal(t, b.MerkleRoot, trimmedBlock.MerkleRoot)
162+
assert.Equal(t, b.Timestamp, trimmedBlock.Timestamp)
163+
assert.Equal(t, b.Index, trimmedBlock.Index)
164+
assert.Equal(t, b.NextConsensus, trimmedBlock.NextConsensus)
165+
assert.Equal(t, b.Script, trimmedBlock.Script)
166+
167+
assert.Equal(t, len(b.Transactions), len(trimmedBlock.TxHashes))
168+
for i := range b.Transactions {
169+
assert.Equal(t, b.Transactions[i].Hash(), trimmedBlock.TxHashes[i])
170+
}
171+
172+
_, err = dao.GetTrimmedBlock(util.Uint256{1, 2, 3})
173+
require.Error(t, err)
174+
}
175+
126176
func TestGetVersion_NoVersion(t *testing.T) {
127177
dao := NewSimple(storage.NewMemoryStore(), false)
128178
version, err := dao.GetVersion()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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 trigger type.
8+
type BlockNotifications struct {
9+
PrePersistNotifications []state.ContainedNotificationEvent `json:"prepersist,omitempty"`
10+
TxNotifications []state.ContainedNotificationEvent `json:"transactions,omitempty"`
11+
PostPersistNotifications []state.ContainedNotificationEvent `json:"postpersist,omitempty"`
12+
}

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

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.TrimmedBlock, error)
115116
}
116117

117118
// ContractStorageSeeker is the interface `findstorage*` handlers need to be able to
@@ -185,6 +186,14 @@ type (
185186
// Item represents Iterator stackitem.
186187
Item stackitem.Item
187188
}
189+
190+
notificationComparatorFilter struct {
191+
id neorpc.EventID
192+
filter neorpc.SubscriptionFilter
193+
}
194+
notificationEventContainer struct {
195+
ntf *state.ContainedNotificationEvent
196+
}
188197
)
189198

190199
const (
@@ -219,6 +228,7 @@ var rpcHandlers = map[string]func(*Server, params.Params) (any, *neorpc.Error){
219228
"getblockhash": (*Server).getBlockHash,
220229
"getblockheader": (*Server).getBlockHeader,
221230
"getblockheadercount": (*Server).getBlockHeaderCount,
231+
"getblocknotifications": (*Server).getBlockNotifications,
222232
"getblocksysfee": (*Server).getBlockSysFee,
223233
"getcandidates": (*Server).getCandidates,
224234
"getcommittee": (*Server).getCommittee,
@@ -3202,3 +3212,51 @@ func (s *Server) getRawNotaryTransaction(reqParams params.Params) (any, *neorpc.
32023212
}
32033213
return tx.Bytes(), nil
32043214
}
3215+
3216+
// getBlockNotifications returns notifications from a specific block with optional filtering.
3217+
func (s *Server) getBlockNotifications(reqParams params.Params) (any, *neorpc.Error) {
3218+
param := reqParams.Value(0)
3219+
hash, respErr := s.blockHashFromParam(param)
3220+
if respErr != nil {
3221+
return nil, respErr
3222+
}
3223+
3224+
block, err := s.chain.GetTrimmedBlock(hash)
3225+
if err != nil {
3226+
return nil, neorpc.ErrUnknownBlock
3227+
}
3228+
3229+
var filter *neorpc.NotificationFilter
3230+
if len(reqParams) > 1 {
3231+
filter = new(neorpc.NotificationFilter)
3232+
err := json.Unmarshal(reqParams[1].RawMessage, filter)
3233+
if err != nil {
3234+
return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("invalid filter: %s", err))
3235+
}
3236+
if err := filter.IsValid(); err != nil {
3237+
return nil, neorpc.WrapErrorWithData(neorpc.ErrInvalidParams, fmt.Sprintf("invalid filter: %s", err))
3238+
}
3239+
}
3240+
3241+
notifications := &result.BlockNotifications{}
3242+
3243+
aers, err := s.chain.GetAppExecResults(block.Hash(), trigger.OnPersist)
3244+
if err == nil && len(aers) > 0 {
3245+
notifications.PrePersistNotifications = processAppExecResults([]state.AppExecResult{aers[0]}, filter)
3246+
}
3247+
3248+
for _, txHash := range block.TxHashes {
3249+
aers, err := s.chain.GetAppExecResults(txHash, trigger.Application)
3250+
if err != nil {
3251+
return nil, neorpc.NewInternalServerError("failed to get app exec results")
3252+
}
3253+
notifications.TxNotifications = append(notifications.TxNotifications, processAppExecResults(aers, filter)...)
3254+
}
3255+
3256+
aers, err = s.chain.GetAppExecResults(block.Hash(), trigger.PostPersist)
3257+
if err == nil && len(aers) > 0 {
3258+
notifications.PostPersistNotifications = processAppExecResults([]state.AppExecResult{aers[0]}, filter)
3259+
}
3260+
3261+
return notifications, nil
3262+
}

0 commit comments

Comments
 (0)