Skip to content

Commit

Permalink
Get block notifications API
Browse files Browse the repository at this point in the history
  • Loading branch information
lock9 committed Jan 20, 2025
1 parent 16a6a59 commit cb76197
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ 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.

#### Historic calls

A set of `*historic` extension methods provide the ability of interacting with
Expand Down
108 changes: 108 additions & 0 deletions pkg/core/block/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ type auxBlockIn struct {
Transactions []json.RawMessage `json:"tx"`
}

type TrimmedBlock struct {
Header
TxHashes []util.Uint256
}

type auxTrimmedBlockOut struct {
TxHashes []util.Uint256 `json:"tx"`
}

// auxTrimmedBlockIn é usado para JSON i/o.
type auxTrimmedBlockIn struct {
TxHashes []json.RawMessage `json:"tx"`
}

// ComputeMerkleRoot computes Merkle tree root hash based on actual block's data.
func (b *Block) ComputeMerkleRoot() util.Uint256 {
hashes := make([]util.Uint256, len(b.Transactions))
Expand Down Expand Up @@ -241,3 +255,97 @@ func (b *Block) ToStackItem() stackitem.Item {

return stackitem.NewArray(items)
}

// NewTrimmedBlockFromReader creates a block with only the header and transaction hashes
func NewTrimmedBlockFromReader(stateRootEnabled bool, br *io.BinReader) (*TrimmedBlock, error) {
block := &TrimmedBlock{
Header: Header{
StateRootEnabled: stateRootEnabled,
},
}

block.Header.DecodeBinary(br)
lenHashes := br.ReadVarUint()
if lenHashes > MaxTransactionsPerBlock {
return nil, ErrMaxContentsPerBlock
}
if lenHashes > 0 {
block.TxHashes = make([]util.Uint256, lenHashes)
for i := range lenHashes {
block.TxHashes[i].DecodeBinary(br)
}
}

return block, br.Err
}

func (b TrimmedBlock) MarshalJSON() ([]byte, error) {
abo := auxTrimmedBlockOut{
TxHashes: b.TxHashes,
}

if abo.TxHashes == nil {
abo.TxHashes = []util.Uint256{}
}
auxb, err := json.Marshal(abo)
if err != nil {
return nil, err
}
baseBytes, err := json.Marshal(b.Header)
if err != nil {
return nil, err
}

// Does as the 'normal' block does
if baseBytes[len(baseBytes)-1] != '}' || auxb[0] != '{' {
return nil, errors.New("can't merge internal jsons")
}
baseBytes[len(baseBytes)-1] = ','
baseBytes = append(baseBytes, auxb[1:]...)
return baseBytes, nil
}

// UnmarshalJSON implementa a interface json.Unmarshaler.
func (b *TrimmedBlock) UnmarshalJSON(data []byte) error {
// Similar ao Block normal, faz unmarshalling separado para Header e hashes
auxb := new(auxTrimmedBlockIn)
err := json.Unmarshal(data, auxb)
if err != nil {
return err
}
err = json.Unmarshal(data, &b.Header)
if err != nil {
return err
}
if len(auxb.TxHashes) != 0 {
b.TxHashes = make([]util.Uint256, len(auxb.TxHashes))
for i, hashBytes := range auxb.TxHashes {
err = json.Unmarshal(hashBytes, &b.TxHashes[i])
if err != nil {
return err
}
}
}
return nil
}

// ToStackItem converte TrimmedBlock para stackitem.Item.
func (b *TrimmedBlock) ToStackItem() stackitem.Item {
items := []stackitem.Item{
stackitem.NewByteArray(b.Hash().BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(b.Version))),
stackitem.NewByteArray(b.PrevHash.BytesBE()),
stackitem.NewByteArray(b.MerkleRoot.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(b.Timestamp))),
stackitem.NewBigInteger(new(big.Int).SetUint64(b.Nonce)),
stackitem.NewBigInteger(big.NewInt(int64(b.Index))),
stackitem.NewBigInteger(big.NewInt(int64(b.PrimaryIndex))),
stackitem.NewByteArray(b.NextConsensus.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(len(b.TxHashes)))),
}
if b.StateRootEnabled {
items = append(items, stackitem.NewByteArray(b.PrevStateRoot.BytesBE()))
}

return stackitem.NewArray(items)
}
41 changes: 41 additions & 0 deletions pkg/core/block/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,47 @@ func TestTrimmedBlock(t *testing.T) {
}
}

func TestNewTrimmedBlockFromReader(t *testing.T) {
block := getDecodedBlock(t, 1)

buf := io.NewBufBinWriter()
block.EncodeTrimmed(buf.BinWriter)
require.NoError(t, buf.Err)

r := io.NewBinReaderFromBuf(buf.Bytes())
trimmedBlock, err := NewTrimmedBlockFromReader(false, r)
require.NoError(t, err)

assert.Equal(t, block.Version, trimmedBlock.Version)
assert.Equal(t, block.PrevHash, trimmedBlock.PrevHash)
assert.Equal(t, block.MerkleRoot, trimmedBlock.MerkleRoot)
assert.Equal(t, block.Timestamp, trimmedBlock.Timestamp)
assert.Equal(t, block.Index, trimmedBlock.Index)
assert.Equal(t, block.NextConsensus, trimmedBlock.NextConsensus)

assert.Equal(t, block.Script, trimmedBlock.Script)
assert.Equal(t, len(block.Transactions), len(trimmedBlock.TxHashes))
for i := range block.Transactions {
assert.Equal(t, block.Transactions[i].Hash(), trimmedBlock.TxHashes[i])
}

data, err := json.Marshal(trimmedBlock)
require.NoError(t, err)

var decoded TrimmedBlock
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)

assert.Equal(t, trimmedBlock.Version, decoded.Version)
assert.Equal(t, trimmedBlock.PrevHash, decoded.PrevHash)
assert.Equal(t, trimmedBlock.MerkleRoot, decoded.MerkleRoot)
assert.Equal(t, trimmedBlock.Timestamp, decoded.Timestamp)
assert.Equal(t, trimmedBlock.Index, decoded.Index)
assert.Equal(t, trimmedBlock.NextConsensus, decoded.NextConsensus)
assert.Equal(t, trimmedBlock.Script, decoded.Script)
assert.Equal(t, trimmedBlock.TxHashes, decoded.TxHashes)
}

func newDumbBlock() *Block {
return &Block{
Header: Header{
Expand Down
5 changes: 5 additions & 0 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -3149,3 +3149,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.TrimmedBlock, error) {
return bc.dao.GetTrimmedBlock(hash)
}
15 changes: 15 additions & 0 deletions pkg/core/dao/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,21 @@ func (dao *Simple) getBlock(key []byte) (*block.Block, error) {
return block, nil
}

// GetTrimmedBlock returns a block with only the header and transaction hashes
func (dao *Simple) GetTrimmedBlock(hash util.Uint256) (*block.TrimmedBlock, error) {
key := dao.makeExecutableKey(hash)
b, err := dao.Store.Get(key)
if err != nil {
return nil, err
}

r := io.NewBinReaderFromBuf(b)
if r.ReadB() != storage.ExecBlock {
return nil, storage.ErrKeyNotFound
}
return block.NewTrimmedBlockFromReader(dao.Version.StateRootInHeader, r)
}

// Version represents the current dao version.
type Version struct {
StoragePrefix storage.KeyPrefix
Expand Down
50 changes: 50 additions & 0 deletions pkg/core/dao/dao_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -123,6 +124,55 @@ func TestPutGetBlock(t *testing.T) {
require.Error(t, err)
}

func TestGetTrimmedBlock(t *testing.T) {
dao := NewSimple(storage.NewMemoryStore(), false)
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
tx.Scripts = []transaction.Witness{{}}

b := &block.Block{
Header: block.Header{
Timestamp: 42,
Script: transaction.Witness{
VerificationScript: []byte{byte(opcode.PUSH1)},
InvocationScript: []byte{byte(opcode.NOP)},
},
},
Transactions: []*transaction.Transaction{tx},
}
hash := b.Hash()
appExecResult1 := &state.AppExecResult{
Container: hash,
Execution: state.Execution{
Trigger: trigger.OnPersist,
Events: []state.NotificationEvent{},
Stack: []stackitem.Item{},
},
}
err := dao.StoreAsBlock(b, appExecResult1, nil)
require.NoError(t, err)

trimmedBlock, err := dao.GetTrimmedBlock(hash)
require.NoError(t, err)
require.NotNil(t, trimmedBlock)

assert.Equal(t, b.Version, trimmedBlock.Version)
assert.Equal(t, b.PrevHash, trimmedBlock.PrevHash)
assert.Equal(t, b.MerkleRoot, trimmedBlock.MerkleRoot)
assert.Equal(t, b.Timestamp, trimmedBlock.Timestamp)
assert.Equal(t, b.Index, trimmedBlock.Index)
assert.Equal(t, b.NextConsensus, trimmedBlock.NextConsensus)
assert.Equal(t, b.Script, trimmedBlock.Script)

assert.Equal(t, len(b.Transactions), len(trimmedBlock.TxHashes))
for i := range b.Transactions {
assert.Equal(t, b.Transactions[i].Hash(), trimmedBlock.TxHashes[i])
}

_, err = dao.GetTrimmedBlock(util.Uint256{1, 2, 3})
require.Error(t, err)
}

func TestGetVersion_NoVersion(t *testing.T) {
dao := NewSimple(storage.NewMemoryStore(), false)
version, err := dao.GetVersion()
Expand Down
12 changes: 12 additions & 0 deletions pkg/neorpc/result/block_notifications.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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 {
PrePersistNotifications []state.ContainedNotificationEvent `json:"prepersist,omitempty"`
TxNotifications []state.ContainedNotificationEvent `json:"transactions,omitempty"`
PostPersistNotifications []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
}
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.TrimmedBlock, error)
}

// ContractStorageSeeker is the interface `findstorage*` handlers need to be able to
Expand Down Expand Up @@ -185,6 +186,14 @@ type (
// Item represents Iterator stackitem.
Item stackitem.Item
}

notificationComparatorFilter struct {
id neorpc.EventID
filter neorpc.SubscriptionFilter
}
notificationEventContainer struct {
ntf *state.ContainedNotificationEvent
}
)

const (
Expand Down Expand Up @@ -219,6 +228,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 +3212,51 @@ 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
}

block, err := s.chain.GetTrimmedBlock(hash)
if err != nil {
return nil, neorpc.ErrUnknownBlock
}

var filter *neorpc.NotificationFilter
if len(reqParams) > 1 {
filter = new(neorpc.NotificationFilter)
err := json.Unmarshal(reqParams[1].RawMessage, 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))
}
}

notifications := &result.BlockNotifications{}

aers, err := s.chain.GetAppExecResults(block.Hash(), trigger.OnPersist)
if err == nil && len(aers) > 0 {
notifications.PrePersistNotifications = processAppExecResults([]state.AppExecResult{aers[0]}, filter)
}

for _, txHash := range block.TxHashes {
aers, err := s.chain.GetAppExecResults(txHash, trigger.Application)
if err != nil {
return nil, neorpc.NewInternalServerError("failed to get app exec results")
}
notifications.TxNotifications = append(notifications.TxNotifications, processAppExecResults(aers, filter)...)
}

aers, err = s.chain.GetAppExecResults(block.Hash(), trigger.PostPersist)
if err == nil && len(aers) > 0 {
notifications.PostPersistNotifications = processAppExecResults([]state.AppExecResult{aers[0]}, filter)
}

return notifications, nil
}
Loading

0 comments on commit cb76197

Please sign in to comment.