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

Implement getrawnotaryrequest and getrawnotarytransaction RPC extensions #3098

Merged
merged 4 commits into from
Aug 31, 2023
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
22 changes: 21 additions & 1 deletion docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,27 @@ state has all its values got from MPT with the specified stateroot. This allows
to track the contract storage scheme using the specified past chain state. These
methods may be useful for debugging purposes.

#### `submitnotaryrequest` call
#### P2PNotary extensions

The following P2PNotary extensions can be used on P2P Notary enabled networks
only.

##### `getrawnotarypool` call

`getrawnotarypool` method provides the ability to retrieve the content of the
RPC node's notary pool (a map from main transaction hashes to the corresponding
fallback transaction hashes for currently processing P2PNotaryRequest payloads).
You can use the `getrawnotarytransaction` method to iterate through
the results of `getrawnotarypool`, retrieve main/fallback transactions,
check their contents and act accordingly.

##### `getrawnotarytransaction` call

The `getrawnotarytransaction` method takes a transaction hash and aims to locate
the corresponding transaction in the P2PNotaryRequest pool. It performs
this search across all the verified main and fallback transactions.

##### `submitnotaryrequest` call

This method can be used on P2P Notary enabled networks to submit new notary
payloads to be relayed from RPC to P2P.
Expand Down
16 changes: 16 additions & 0 deletions pkg/core/mempool/mem_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,19 @@ func (mp *Pool) removeConflictsOf(tx *transaction.Transaction) {
}
}
}

// IterateVerifiedTransactions iterates through verified transactions and invokes
// function `cont`. Iterations continue while the function `cont` returns true.
// Function `cont` is executed within a read-locked memory pool,
// thus IterateVerifiedTransactions will block any write mempool operation,
// use it with care. Do not modify transaction or data via `cont`.
func (mp *Pool) IterateVerifiedTransactions(cont func(tx *transaction.Transaction, data any) bool) {
mp.lock.RLock()
defer mp.lock.RUnlock()

for i := range mp.verifiedTxes {
if !cont(mp.verifiedTxes[i].txn, mp.verifiedTxes[i].data) {
return
}
}
}
134 changes: 111 additions & 23 deletions pkg/core/mempool/mem_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,26 +637,18 @@ func TestMempoolAddWithDataGetData(t *testing.T) {
balance: 100,
}
mp := New(10, 1, false, nil)
newTx := func(t *testing.T, netFee int64) *transaction.Transaction {
tx := transaction.New([]byte{byte(opcode.RET)}, 0)
tx.Signers = []transaction.Signer{{}, {}}
tx.NetworkFee = netFee
nonce++
tx.Nonce = nonce
return tx
}

// bad, insufficient deposit
r1 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, fs.balance+1),
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(fs.balance+1, &nonce),
}
require.ErrorIs(t, mp.Add(r1.FallbackTransaction, fs, r1), ErrInsufficientFunds)

// good
r2 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee),
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
}
require.NoError(t, mp.Add(r2.FallbackTransaction, fs, r2))
require.True(t, mp.ContainsKey(r2.FallbackTransaction.Hash()))
Expand All @@ -669,8 +661,8 @@ func TestMempoolAddWithDataGetData(t *testing.T) {

// good, higher priority than r2. The resulting mp.verifiedTxes: [r3, r2]
r3 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee+1),
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee+1, &nonce),
}
require.NoError(t, mp.Add(r3.FallbackTransaction, fs, r3))
require.True(t, mp.ContainsKey(r3.FallbackTransaction.Hash()))
Expand All @@ -680,8 +672,8 @@ func TestMempoolAddWithDataGetData(t *testing.T) {

// good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4]
r4 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee),
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
}
require.NoError(t, mp.Add(r4.FallbackTransaction, fs, r4))
require.True(t, mp.ContainsKey(r4.FallbackTransaction.Hash()))
Expand All @@ -691,8 +683,8 @@ func TestMempoolAddWithDataGetData(t *testing.T) {

// good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4, r5]
r5 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee),
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
}
require.NoError(t, mp.Add(r5.FallbackTransaction, fs, r5))
require.True(t, mp.ContainsKey(r5.FallbackTransaction.Hash()))
Expand All @@ -713,7 +705,7 @@ func TestMempoolAddWithDataGetData(t *testing.T) {
require.False(t, ok)

// but getting nil data is OK. The resulting mp.verifiedTxes: [r3, r2, r4, r5, r6]
r6 := newTx(t, smallNetFee)
r6 := mkTwoSignersTx(smallNetFee, &nonce)
require.NoError(t, mp.Add(r6, fs, nil))
require.True(t, mp.ContainsKey(r6.Hash()))
data, ok = mp.TryGetData(r6.Hash())
Expand All @@ -722,18 +714,114 @@ func TestMempoolAddWithDataGetData(t *testing.T) {

// getting data: item is in verifiedMap, but not in verifiedTxes
r7 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee),
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
}
require.NoError(t, mp.Add(r7.FallbackTransaction, fs, r4))
require.True(t, mp.ContainsKey(r7.FallbackTransaction.Hash()))
r8 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee-1),
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee-1, &nonce),
}
require.NoError(t, mp.Add(r8.FallbackTransaction, fs, r4))
require.True(t, mp.ContainsKey(r8.FallbackTransaction.Hash()))
mp.verifiedTxes = append(mp.verifiedTxes[:len(mp.verifiedTxes)-2], mp.verifiedTxes[len(mp.verifiedTxes)-1])
_, ok = mp.TryGetData(r7.FallbackTransaction.Hash())
require.False(t, ok)
}

func mkTwoSignersTx(netFee int64, nonce *uint32) *transaction.Transaction {
tx := transaction.New([]byte{byte(opcode.RET)}, 0)
tx.Signers = []transaction.Signer{{}, {}}
tx.NetworkFee = netFee
*nonce++
tx.Nonce = *nonce
return tx
}

func TestMempoolIterateVerifiedTransactions(t *testing.T) {
var (
smallNetFee int64 = 3
nonce uint32
r1, r2, r3, r4, r5 *payload.P2PNotaryRequest
)
fs := &FeerStub{
feePerByte: 0,
p2pSigExt: true,
blockHeight: 5,
balance: 100,
}
mp := New(10, 1, false, nil)

checkRequestsOrder := func(orderedRequests []*payload.P2PNotaryRequest) {
var pooledRequests []*payload.P2PNotaryRequest
mp.IterateVerifiedTransactions(func(tx *transaction.Transaction, data any) bool {
d := data.(*payload.P2PNotaryRequest)
pooledRequests = append(pooledRequests, d)
return true
})
require.Equal(t, orderedRequests, pooledRequests)
}

r1 = &payload.P2PNotaryRequest{
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
}
require.NoError(t, mp.Add(r1.FallbackTransaction, fs, r1))
checkRequestsOrder([]*payload.P2PNotaryRequest{r1})

// r2 has higher priority than r1. The resulting mp.verifiedTxes: [r2, r1]
r2 = &payload.P2PNotaryRequest{
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee+1, &nonce),
}
require.NoError(t, mp.Add(r2.FallbackTransaction, fs, r2))
checkRequestsOrder([]*payload.P2PNotaryRequest{r2, r1})

// r3 has the same priority as r1. The resulting mp.verifiedTxes: [r2, r1, r3]
r3 = &payload.P2PNotaryRequest{
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
}
require.NoError(t, mp.Add(r3.FallbackTransaction, fs, r3))
checkRequestsOrder([]*payload.P2PNotaryRequest{r2, r1, r3})

// r4 has the same priority as r1. The resulting mp.verifiedTxes: [r2, r1, r3, r4]
r4 = &payload.P2PNotaryRequest{
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
}
require.NoError(t, mp.Add(r4.FallbackTransaction, fs, r4))
checkRequestsOrder([]*payload.P2PNotaryRequest{r2, r1, r3, r4})

checkPooledRequest := func(t *testing.T, r *payload.P2PNotaryRequest, isPooled bool) {
cont := true
notaryRequest := &payload.P2PNotaryRequest{}
mp.IterateVerifiedTransactions(func(tx *transaction.Transaction, data any) bool {
if data != nil {
notaryRequest = data.(*payload.P2PNotaryRequest)
if notaryRequest.MainTransaction.Hash() == r.MainTransaction.Hash() {
cont = false
}
}
return cont
})

if isPooled {
require.Equal(t, false, cont)
require.Equal(t, r, notaryRequest)
} else {
require.Equal(t, true, cont)
}
}
checkPooledRequest(t, r1, true)
checkPooledRequest(t, r2, true)
checkPooledRequest(t, r3, true)
checkPooledRequest(t, r4, true)

r5 = &payload.P2PNotaryRequest{
MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
}
checkPooledRequest(t, r5, false)
}
48 changes: 48 additions & 0 deletions pkg/neorpc/result/raw_notary_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package result

import (
"encoding/json"
"strings"

"github.com/nspcc-dev/neo-go/pkg/util"
)

// RawNotaryPool represents a result of `getrawnotarypool` RPC call.
// The structure consist of `Hashes`. `Hashes` field is a map, where key is
// the hash of the main transaction and value is a slice of related fallback
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
// transaction hashes.
type RawNotaryPool struct {
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
Hashes map[util.Uint256][]util.Uint256
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
}

// rawNotaryPoolAux is an auxiliary struct for RawNotaryPool JSON marshalling.
type rawNotaryPoolAux struct {
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
Hashes map[string][]util.Uint256 `json:"hashes,omitempty"`
}

// MarshalJSON implements the json.Marshaler interface.
func (p RawNotaryPool) MarshalJSON() ([]byte, error) {
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
var aux rawNotaryPoolAux
aux.Hashes = make(map[string][]util.Uint256, len(p.Hashes))
for main, fallbacks := range p.Hashes {
aux.Hashes["0x"+main.StringLE()] = fallbacks
}
return json.Marshal(aux)
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (p *RawNotaryPool) UnmarshalJSON(data []byte) error {
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
var aux rawNotaryPoolAux
if err := json.Unmarshal(data, &aux); err != nil {
return err
}

Check warning on line 38 in pkg/neorpc/result/raw_notary_pool.go

View check run for this annotation

Codecov / codecov/patch

pkg/neorpc/result/raw_notary_pool.go#L37-L38

Added lines #L37 - L38 were not covered by tests
p.Hashes = make(map[util.Uint256][]util.Uint256, len(aux.Hashes))
for main, fallbacks := range aux.Hashes {
hashMain, err := util.Uint256DecodeStringLE(strings.TrimPrefix(main, "0x"))
if err != nil {
return err
}

Check warning on line 44 in pkg/neorpc/result/raw_notary_pool.go

View check run for this annotation

Codecov / codecov/patch

pkg/neorpc/result/raw_notary_pool.go#L43-L44

Added lines #L43 - L44 were not covered by tests
p.Hashes[hashMain] = fallbacks
}
return nil
}
2 changes: 2 additions & 0 deletions pkg/rpcclient/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ Supported methods
Extensions:

getblocksysfee
getrawnotarypool
getrawnotarytransaction
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
submitnotaryrequest

Unsupported methods
Expand Down
41 changes: 41 additions & 0 deletions pkg/rpcclient/rpc.go
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -1285,3 +1285,44 @@

return resp, nil
}

// GetRawNotaryTransaction returns main or fallback transaction from the
// RPC node's notary request pool.
func (c *Client) GetRawNotaryTransaction(hash util.Uint256) (*transaction.Transaction, error) {
var (
params = []any{hash.StringLE()}
resp []byte
err error
)
if err = c.performRequest("getrawnotarytransaction", params, &resp); err != nil {
return nil, err
}
return transaction.NewTransactionFromBytes(resp)
}

// GetRawNotaryTransactionVerbose returns main or fallback transaction from the
// RPC node's notary request pool.
// NOTE: to get transaction.ID and transaction.Size, use t.Hash() and
// io.GetVarSize(t) respectively.
func (c *Client) GetRawNotaryTransactionVerbose(hash util.Uint256) (*transaction.Transaction, error) {
var (
params = []any{hash.StringLE(), 1} // 1 for verbose.
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
resp = &transaction.Transaction{}
err error
)
if err = c.performRequest("getrawnotarytransaction", params, resp); err != nil {
return nil, err
}
return resp, nil
}

// GetRawNotaryPool returns hashes of main P2PNotaryRequest transactions that
// are currently in the RPC node's notary request pool with the corresponding
// hashes of fallback transactions.
func (c *Client) GetRawNotaryPool() (*result.RawNotaryPool, error) {
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
resp := &result.RawNotaryPool{}
if err := c.performRequest("getrawnotarypool", nil, resp); err != nil {
return nil, err
}

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

View check run for this annotation

Codecov / codecov/patch

pkg/rpcclient/rpc.go#L1325-L1326

Added lines #L1325 - L1326 were not covered by tests
return resp, nil
}
Loading
Loading