diff --git a/docs/rpc.md b/docs/rpc.md index 2fd0777a6..068c55c5e 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -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. diff --git a/pkg/core/mempool/mem_pool.go b/pkg/core/mempool/mem_pool.go index 2fbce047b..b03c1f750 100644 --- a/pkg/core/mempool/mem_pool.go +++ b/pkg/core/mempool/mem_pool.go @@ -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 + } + } +} diff --git a/pkg/core/mempool/mem_pool_test.go b/pkg/core/mempool/mem_pool_test.go index d1d247dc9..9bcf43cba 100644 --- a/pkg/core/mempool/mem_pool_test.go +++ b/pkg/core/mempool/mem_pool_test.go @@ -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())) @@ -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())) @@ -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())) @@ -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())) @@ -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()) @@ -722,14 +714,14 @@ 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())) @@ -737,3 +729,99 @@ func TestMempoolAddWithDataGetData(t *testing.T) { _, 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) +} diff --git a/pkg/neorpc/result/raw_notary_pool.go b/pkg/neorpc/result/raw_notary_pool.go new file mode 100644 index 000000000..09bd8902f --- /dev/null +++ b/pkg/neorpc/result/raw_notary_pool.go @@ -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 +// transaction hashes. +type RawNotaryPool struct { + Hashes map[util.Uint256][]util.Uint256 +} + +// rawNotaryPoolAux is an auxiliary struct for RawNotaryPool JSON marshalling. +type rawNotaryPoolAux struct { + Hashes map[string][]util.Uint256 `json:"hashes,omitempty"` +} + +// MarshalJSON implements the json.Marshaler interface. +func (p RawNotaryPool) MarshalJSON() ([]byte, error) { + 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 { + var aux rawNotaryPoolAux + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + 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 + } + p.Hashes[hashMain] = fallbacks + } + return nil +} diff --git a/pkg/rpcclient/doc.go b/pkg/rpcclient/doc.go index 8871a5371..ef2e1d198 100644 --- a/pkg/rpcclient/doc.go +++ b/pkg/rpcclient/doc.go @@ -96,6 +96,8 @@ Supported methods Extensions: getblocksysfee + getrawnotarypool + getrawnotarytransaction submitnotaryrequest Unsupported methods diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index 0a7215558..c6a66df38 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -1285,3 +1285,44 @@ func (c *Client) TerminateSession(sessionID uuid.UUID) (bool, error) { 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. + 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) { + resp := &result.RawNotaryPool{} + if err := c.performRequest("getrawnotarypool", nil, resp); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/rpcclient/rpc_test.go b/pkg/rpcclient/rpc_test.go index 5562a21ac..0c0b51a23 100644 --- a/pkg/rpcclient/rpc_test.go +++ b/pkg/rpcclient/rpc_test.go @@ -1375,6 +1375,89 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ }, }, }, + "getrawnotarytransaction": { + { + name: "positive", + invoke: func(c *Client) (any, error) { + hash, err := util.Uint256DecodeStringLE("ad1c2875de823a54188949490e2d68580fd070fcc5ff409609f478d23d12355f") + if err != nil { + panic(err) + } + return c.GetRawNotaryTransaction(hash) + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":"AAMAAAAAAAAAAAAAAAAAAAAAAAAAewAAAAHunqIsJ+NL0BSPxBCOCPdOj1BIsgABIgEBQAEDAQQHAwMGCQ=="}`, + result: func(c *Client) any { + return &transaction.Transaction{} + }, + check: func(t *testing.T, c *Client, uns any) { + res, ok := uns.(*transaction.Transaction) + require.True(t, ok) + assert.NotNil(t, res) + expectHash, err := util.Uint256DecodeStringLE("ad1c2875de823a54188949490e2d68580fd070fcc5ff409609f478d23d12355f") + require.NoError(t, err) + assert.Equal(t, expectHash, res.Hash()) + }, + }, + { + name: "positive verbose", + invoke: func(c *Client) (any, error) { + hash, err := util.Uint256DecodeStringLE("ad1c2875de823a54188949490e2d68580fd070fcc5ff409609f478d23d12355f") + if err != nil { + panic(err) + } + return c.GetRawNotaryTransactionVerbose(hash) + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"hash":"0xad1c2875de823a54188949490e2d68580fd070fcc5ff409609f478d23d12355f","size":61,"version":0,"nonce":3,"sender":"Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn","sysfee":"0","netfee":"0","validuntilblock":123,"attributes":[{"nkeys":1,"type":"NotaryAssisted"}],"signers":[{"account":"0xb248508f4ef7088e10c48f14d04be3272ca29eee","scopes":"None"}],"script":"QA==","witnesses":[{"invocation":"AQQH","verification":"AwYJ"}]}}`, + result: func(c *Client) any { + return &transaction.Transaction{} + }, + check: func(t *testing.T, c *Client, uns any) { + res, ok := uns.(*transaction.Transaction) + require.True(t, ok) + assert.NotNil(t, res) + expectHash, err := util.Uint256DecodeStringLE("ad1c2875de823a54188949490e2d68580fd070fcc5ff409609f478d23d12355f") + require.NoError(t, err) + assert.Equal(t, expectHash, res.Hash()) + }, + }, + }, + "getrawnotarypool": { + { + name: "empty pool", + invoke: func(c *Client) (any, error) { + return c.GetRawNotaryPool() + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{}}`, + result: func(c *Client) any { + return &result.RawNotaryPool{ + Hashes: map[util.Uint256][]util.Uint256{}, + } + }, + }, + { + name: "nonempty pool", + invoke: func(c *Client) (any, error) { + return c.GetRawNotaryPool() + }, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"hashes":{"0xd86b5346e9bbe6dba845cc4192fa716535a3d05c4f2084431edc99dc3862a299":["0xbb0b2f1d5539dd776637f00e5011d97921a1400d3a63c02977a38446180c6d7c"]}}}`, + result: func(c *Client) any { + return &result.RawNotaryPool{ + Hashes: map[util.Uint256][]util.Uint256{}, + } + }, + check: func(t *testing.T, c *Client, uns any) { + res, ok := uns.(*result.RawNotaryPool) + require.True(t, ok) + mainHash, err := util.Uint256DecodeStringLE("d86b5346e9bbe6dba845cc4192fa716535a3d05c4f2084431edc99dc3862a299") + require.NoError(t, err, "can't decode `mainHash` result hash") + fallbackHash, err := util.Uint256DecodeStringLE("bb0b2f1d5539dd776637f00e5011d97921a1400d3a63c02977a38446180c6d7c") + require.NoError(t, err, "can't decode `fallbackHash` result hash") + fallbacks, ok := res.Hashes[mainHash] + require.True(t, ok) + assert.Equal(t, fallbacks[0], fallbackHash) + }, + }, + }, } type rpcClientErrorCase struct { diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index 0e3b0dd6d..7beb69f8b 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -1135,6 +1135,137 @@ func TestSignAndPushP2PNotaryRequest(t *testing.T) { }) } +func TestGetRawNotaryPoolAndTransaction(t *testing.T) { + var ( + mainHash1, fallbackHash1, mainHash2, fallbackHash2 util.Uint256 + tx1, tx2 *transaction.Transaction + ) + + chain, rpcSrv, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false) + defer chain.Close() + defer rpcSrv.Shutdown() + + c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) + require.NoError(t, err) + t.Run("getrawnotarypool", func(t *testing.T) { + t.Run("empty pool", func(t *testing.T) { + np, err := c.GetRawNotaryPool() + require.NoError(t, err) + require.Equal(t, 0, len(np.Hashes)) + }) + + sender := testchain.PrivateKeyByID(0) // owner of the deposit in testchain + acc := wallet.NewAccountFromPrivateKey(sender) + + comm, err := c.GetCommittee() + require.NoError(t, err) + + multiAcc := &wallet.Account{} + *multiAcc = *acc + require.NoError(t, multiAcc.ConvertMultisig(smartcontract.GetMajorityHonestNodeCount(len(comm)), comm)) + + nact, err := notary.NewActor(c, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: multiAcc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: multiAcc, + }}, acc) + require.NoError(t, err) + neoW := neo.New(nact) + // Send the 1st notary request + tx1, err = neoW.SetRegisterPriceTransaction(1_0000_0000) + require.NoError(t, err) + mainHash1, fallbackHash1, _, err = nact.Notarize(tx1, err) + require.NoError(t, err) + + checkTxInPool := func(t *testing.T, mainHash, fallbackHash util.Uint256, res *result.RawNotaryPool) { + actFallbacks, ok := res.Hashes[mainHash] + require.Equal(t, true, ok) + require.Equal(t, 1, len(actFallbacks)) + require.Equal(t, fallbackHash, actFallbacks[0]) + } + t.Run("nonempty pool", func(t *testing.T) { + actNotaryPool, err := c.GetRawNotaryPool() + require.NoError(t, err) + require.Equal(t, 1, len(actNotaryPool.Hashes)) + checkTxInPool(t, mainHash1, fallbackHash1, actNotaryPool) + }) + + // Send the 2nd notary request + tx2, err = neoW.SetRegisterPriceTransaction(2_0000_0000) + require.NoError(t, err) + mainHash2, fallbackHash2, _, err = nact.Notarize(tx2, err) + require.NoError(t, err) + + t.Run("pool with 2", func(t *testing.T) { + actNotaryPool, err := c.GetRawNotaryPool() + require.NoError(t, err) + require.Equal(t, 2, len(actNotaryPool.Hashes)) + checkTxInPool(t, mainHash1, fallbackHash1, actNotaryPool) + checkTxInPool(t, mainHash2, fallbackHash2, actNotaryPool) + }) + }) + t.Run("getrawnotarytransaction", func(t *testing.T) { + t.Run("client GetRawNotaryTransaction", func(t *testing.T) { + t.Run("unknown transaction", func(t *testing.T) { + _, err := c.GetRawNotaryTransaction(util.Uint256{0, 0, 0}) + require.Error(t, err) + require.ErrorIs(t, err, neorpc.ErrUnknownTransaction) + }) + _ = tx1.Size() + _ = tx2.Size() + // RPC server returns empty scripts in transaction.Witness, + // thus here the nil-value was changed to empty value. + if tx1.Scripts[1].InvocationScript == nil && tx1.Scripts[1].VerificationScript == nil { + tx1.Scripts[1] = transaction.Witness{ + InvocationScript: []byte{}, + VerificationScript: []byte{}, + } + } + if tx2.Scripts[1].InvocationScript == nil && tx2.Scripts[1].VerificationScript == nil { + tx2.Scripts[1] = transaction.Witness{ + InvocationScript: []byte{}, + VerificationScript: []byte{}, + } + } + t.Run("transactions from pool", func(t *testing.T) { + mainTx1, err := c.GetRawNotaryTransaction(mainHash1) + require.NoError(t, err) + require.Equal(t, tx1, mainTx1) + _, err = c.GetRawNotaryTransaction(fallbackHash1) + require.NoError(t, err) + + mainTx2, err := c.GetRawNotaryTransaction(mainHash2) + require.NoError(t, err) + require.Equal(t, tx2, mainTx2) + _, err = c.GetRawNotaryTransaction(fallbackHash2) + require.NoError(t, err) + }) + }) + t.Run("client GetRawNotaryTransactionVerbose", func(t *testing.T) { + t.Run("unknown transaction", func(t *testing.T) { + _, err := c.GetRawNotaryTransactionVerbose(util.Uint256{0, 0, 0}) + require.Error(t, err) + require.ErrorIs(t, err, neorpc.ErrUnknownTransaction) + }) + t.Run("transactions from pool", func(t *testing.T) { + mainTx1, err := c.GetRawNotaryTransactionVerbose(mainHash1) + require.NoError(t, err) + require.Equal(t, tx1, mainTx1) + _, err = c.GetRawNotaryTransactionVerbose(fallbackHash1) + require.NoError(t, err) + + mainTx2, err := c.GetRawNotaryTransactionVerbose(mainHash2) + require.NoError(t, err) + require.Equal(t, tx2, mainTx2) + _, err = c.GetRawNotaryTransactionVerbose(fallbackHash2) + require.NoError(t, err) + }) + }) + }) +} + func TestCalculateNotaryFee(t *testing.T) { chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) defer chain.Close() diff --git a/pkg/services/rpcsrv/server.go b/pkg/services/rpcsrv/server.go index c6fe8d936..5e66e0e3a 100644 --- a/pkg/services/rpcsrv/server.go +++ b/pkg/services/rpcsrv/server.go @@ -229,6 +229,8 @@ var rpcHandlers = map[string]func(*Server, params.Params) (any, *neorpc.Error){ "getpeers": (*Server).getPeers, "getproof": (*Server).getProof, "getrawmempool": (*Server).getRawMempool, + "getrawnotarypool": (*Server).getRawNotaryPool, + "getrawnotarytransaction": (*Server).getRawNotaryTransaction, "getrawtransaction": (*Server).getrawtransaction, "getstate": (*Server).getState, "getstateheight": (*Server).getStateHeight, @@ -3090,3 +3092,54 @@ func (s *Server) Addresses() []string { } return res } + +func (s *Server) getRawNotaryPool(_ params.Params) (any, *neorpc.Error) { + if !s.chain.P2PSigExtensionsEnabled() { + return nil, neorpc.NewInternalServerError("P2PSignatureExtensions are disabled") + } + nrp := s.coreServer.GetNotaryPool() + res := &result.RawNotaryPool{Hashes: make(map[util.Uint256][]util.Uint256)} + nrp.IterateVerifiedTransactions(func(tx *transaction.Transaction, data any) bool { + if data != nil { + d := data.(*payload.P2PNotaryRequest) + mainHash := d.MainTransaction.Hash() + fallbackHash := d.FallbackTransaction.Hash() + res.Hashes[mainHash] = append(res.Hashes[mainHash], fallbackHash) + } + return true + }) + return res, nil +} + +func (s *Server) getRawNotaryTransaction(reqParams params.Params) (any, *neorpc.Error) { + if !s.chain.P2PSigExtensionsEnabled() { + return nil, neorpc.NewInternalServerError("P2PSignatureExtensions are disabled") + } + + txHash, err := reqParams.Value(0).GetUint256() + if err != nil { + return nil, neorpc.ErrInvalidParams + } + nrp := s.coreServer.GetNotaryPool() + // Try to find fallback transaction. + tx, ok := nrp.TryGetValue(txHash) + if !ok { + // Try to find main transaction. + nrp.IterateVerifiedTransactions(func(t *transaction.Transaction, data any) bool { + if data != nil && data.(*payload.P2PNotaryRequest).MainTransaction.Hash().Equals(txHash) { + tx = data.(*payload.P2PNotaryRequest).MainTransaction + return false + } + return true + }) + // The transaction was not found. + if tx == nil { + return nil, neorpc.ErrUnknownTransaction + } + } + + if v, _ := reqParams.Value(1).GetBoolean(); v { + return tx, nil + } + return tx.Bytes(), nil +} diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index 6fb1222f4..da27c69ce 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -2275,8 +2275,11 @@ func TestSubmitOracle(t *testing.T) { t.Run("Valid", runCase(t, false, 0, pubStr, `1`, txSigStr, msgSigStr)) } -func TestSubmitNotaryRequest(t *testing.T) { - rpc := `{"jsonrpc": "2.0", "id": 1, "method": "submitnotaryrequest", "params": %s}` +func TestNotaryRequestRPC(t *testing.T) { + var notaryRequest1, notaryRequest2 *payload.P2PNotaryRequest + rpcSubmit := `{"jsonrpc": "2.0", "id": 1, "method": "submitnotaryrequest", "params": %s}` + rpcPool := `{"jsonrpc": "2.0", "id": 1, "method": "getrawnotarypool", "params": []}` + rpcTx := `{"jsonrpc": "2.0", "id": 1, "method": "getrawnotarytransaction", "params": ["%s", %d]}` t.Run("disabled P2PSigExtensions", func(t *testing.T) { chain, rpcSrv, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) { @@ -2284,87 +2287,206 @@ func TestSubmitNotaryRequest(t *testing.T) { }) defer chain.Close() defer rpcSrv.Shutdown() - req := fmt.Sprintf(rpc, "[]") - body := doRPCCallOverHTTP(req, httpSrv.URL, t) - checkErrGetResult(t, body, true, neorpc.InternalServerErrorCode) + t.Run("submitnotaryrequest", func(t *testing.T) { + body := doRPCCallOverHTTP(fmt.Sprintf(rpcSubmit, "[]"), httpSrv.URL, t) + checkErrGetResult(t, body, true, neorpc.InternalServerErrorCode) + }) + t.Run("getrawnotarypool", func(t *testing.T) { + body := doRPCCallOverHTTP(rpcPool, httpSrv.URL, t) + checkErrGetResult(t, body, true, neorpc.InternalServerErrorCode) + }) + t.Run("getrawnotarytransaction", func(t *testing.T) { + body := doRPCCallOverHTTP(fmt.Sprintf(rpcTx, " ", 1), httpSrv.URL, t) + checkErrGetResult(t, body, true, neorpc.InternalServerErrorCode) + }) }) chain, rpcSrv, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false) defer chain.Close() defer rpcSrv.Shutdown() - runCase := func(t *testing.T, fail bool, errCode int64, params ...string) func(t *testing.T) { + submitNotaryRequest := func(t *testing.T, fail bool, errCode int64, params ...string) func(t *testing.T) { return func(t *testing.T) { ps := `[` + strings.Join(params, ",") + `]` - req := fmt.Sprintf(rpc, ps) + req := fmt.Sprintf(rpcSubmit, ps) body := doRPCCallOverHTTP(req, httpSrv.URL, t) checkErrGetResult(t, body, fail, errCode) } } - t.Run("missing request", runCase(t, true, neorpc.InvalidParamsCode)) - t.Run("not a base64", runCase(t, true, neorpc.InvalidParamsCode, `"not-a-base64$"`)) - t.Run("invalid request bytes", runCase(t, true, neorpc.InvalidParamsCode, `"not-a-request"`)) - t.Run("invalid request", func(t *testing.T) { - mainTx := &transaction.Transaction{ + + t.Run("getrawnotarypool", func(t *testing.T) { + t.Run("empty pool", func(t *testing.T) { + body := doRPCCallOverHTTP(rpcPool, httpSrv.URL, t) + res := checkErrGetResult(t, body, false, 0) + actual := new(result.RawNotaryPool) + require.NoError(t, json.Unmarshal(res, actual)) + require.Equal(t, 0, len(actual.Hashes)) + }) + + sender := testchain.PrivateKeyByID(0) // owner of the deposit in testchain + notaryRequest1 = createValidNotaryRequest(chain, sender, 1, 2_0000_0000, nil) + nrBytes, err := notaryRequest1.Bytes() + require.NoError(t, err) + str := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(nrBytes)) + submitNotaryRequest(t, false, 0, str)(t) + + t.Run("nonempty pool", func(t *testing.T) { + //get notary pool & check tx hashes + body := doRPCCallOverHTTP(rpcPool, httpSrv.URL, t) + res := checkErrGetResult(t, body, false, 0) + actual := new(result.RawNotaryPool) + require.NoError(t, json.Unmarshal(res, actual)) + require.Equal(t, 1, len(actual.Hashes)) + for actMain, actFallbacks := range actual.Hashes { + require.Equal(t, notaryRequest1.MainTransaction.Hash(), actMain) + require.Equal(t, 1, len(actFallbacks)) + require.Equal(t, notaryRequest1.FallbackTransaction.Hash(), actFallbacks[0]) + } + }) + + notaryRequest2 = createValidNotaryRequest(chain, sender, 2, 3_0000_0000, notaryRequest1.MainTransaction) + nrBytes2, err := notaryRequest2.Bytes() + require.NoError(t, err) + str2 := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(nrBytes2)) + submitNotaryRequest(t, false, 0, str2)(t) + + t.Run("pool with 2", func(t *testing.T) { + //get notary pool & check tx hashes + body := doRPCCallOverHTTP(rpcPool, httpSrv.URL, t) + res := checkErrGetResult(t, body, false, 0) + actual := new(result.RawNotaryPool) + require.NoError(t, json.Unmarshal(res, actual)) + require.Equal(t, 1, len(actual.Hashes)) + for actMain, actFallbacks := range actual.Hashes { + require.Equal(t, notaryRequest1.MainTransaction.Hash(), actMain) + require.Equal(t, 2, len(actFallbacks)) + // The second fallback transaction has higher priority, so it's first in the slice. + require.Equal(t, notaryRequest1.FallbackTransaction.Hash(), actFallbacks[1]) + require.Equal(t, notaryRequest2.FallbackTransaction.Hash(), actFallbacks[0]) + } + }) + }) + + t.Run("submitnotaryrequest", func(t *testing.T) { + t.Run("missing request", submitNotaryRequest(t, true, neorpc.InvalidParamsCode)) + t.Run("not a base64", submitNotaryRequest(t, true, neorpc.InvalidParamsCode, `"not-a-base64$"`)) + t.Run("invalid request bytes", submitNotaryRequest(t, true, neorpc.InvalidParamsCode, `"not-a-request"`)) + t.Run("invalid request", func(t *testing.T) { + mainTx := &transaction.Transaction{ + Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}}, + Script: []byte{byte(opcode.RET)}, + ValidUntilBlock: 123, + Signers: []transaction.Signer{{Account: util.Uint160{1, 5, 9}}}, + Scripts: []transaction.Witness{{ + InvocationScript: []byte{1, 4, 7}, + VerificationScript: []byte{3, 6, 9}, + }}, + } + fallbackTx := &transaction.Transaction{ + Script: []byte{byte(opcode.RET)}, + ValidUntilBlock: 123, + Attributes: []transaction.Attribute{ + {Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}, + {Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}}, + {Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}, + }, + Signers: []transaction.Signer{{Account: util.Uint160{1, 4, 7}}, {Account: util.Uint160{9, 8, 7}}}, + Scripts: []transaction.Witness{ + {InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...), VerificationScript: make([]byte, 0)}, + {InvocationScript: []byte{1, 2, 3}, VerificationScript: []byte{1, 2, 3}}}, + } + p := &payload.P2PNotaryRequest{ + MainTransaction: mainTx, + FallbackTransaction: fallbackTx, + Witness: transaction.Witness{ + InvocationScript: []byte{1, 2, 3}, + VerificationScript: []byte{7, 8, 9}, + }, + } + nrBytes, err := p.Bytes() + require.NoError(t, err) + str := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(nrBytes)) + submitNotaryRequest(t, true, neorpc.ErrVerificationFailedCode, str)(t) + }) + t.Run("valid request", func(t *testing.T) { + sender := testchain.PrivateKeyByID(0) // owner of the deposit in testchain + notaryRequest1 = createValidNotaryRequest(chain, sender, 3, 2_0000_0000, nil) + nrBytes, err := notaryRequest1.Bytes() + require.NoError(t, err) + str := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(nrBytes)) + submitNotaryRequest(t, false, 0, str)(t) + }) + }) + + t.Run("getrawnotarytransaction", func(t *testing.T) { + t.Run("invalid param", func(t *testing.T) { + req := fmt.Sprintf(rpcTx, "invalid", 1) + body := doRPCCallOverHTTP(req, httpSrv.URL, t) + checkErrGetResult(t, body, true, neorpc.InvalidParamsCode) + }) + t.Run("unknown transaction", func(t *testing.T) { + req := fmt.Sprintf(rpcTx, (util.Uint256{0, 0, 0}).StringLE(), 1) + body := doRPCCallOverHTTP(req, httpSrv.URL, t) + checkErrGetResult(t, body, true, neorpc.ErrUnknownTransactionCode) + }) + + checkGetTxVerbose := func(t *testing.T, tx *transaction.Transaction) { + req := fmt.Sprintf(rpcTx, tx.Hash().StringLE(), 1) + body := doRPCCallOverHTTP(req, httpSrv.URL, t) + res := checkErrGetResult(t, body, false, 0) + actual := new(transaction.Transaction) + require.NoError(t, json.Unmarshal(res, actual)) + _ = tx.Size() + require.Equal(t, tx, actual) + } + t.Run("mainTx verbose", func(t *testing.T) { + checkGetTxVerbose(t, notaryRequest1.MainTransaction) + }) + t.Run("fallbackTx verbose", func(t *testing.T) { + checkGetTxVerbose(t, notaryRequest1.FallbackTransaction) + checkGetTxVerbose(t, notaryRequest2.FallbackTransaction) + }) + + checkGetTxBytes := func(t *testing.T, tx *transaction.Transaction) { + req := fmt.Sprintf(rpcTx, tx.Hash().StringLE(), 0) + body := doRPCCallOverHTTP(req, httpSrv.URL, t) + res := checkErrGetResult(t, body, false, 0) + + var s string + err := json.Unmarshal(res, &s) + require.NoErrorf(t, err, "could not parse response: %s", res) + txBin, err := testserdes.EncodeBinary(tx) + require.NoError(t, err) + expected := base64.StdEncoding.EncodeToString(txBin) + assert.Equal(t, expected, s) + } + t.Run("mainTx bytes", func(t *testing.T) { + checkGetTxBytes(t, notaryRequest1.MainTransaction) + }) + t.Run("fallbackTx bytes", func(t *testing.T) { + checkGetTxBytes(t, notaryRequest1.FallbackTransaction) + checkGetTxBytes(t, notaryRequest2.FallbackTransaction) + }) + }) +} + +// createValidNotaryRequest creates and signs P2PNotaryRequest payload which can +// pass verification. It uses the provided mainTx if it's a nonempty structure. +func createValidNotaryRequest(chain *core.Blockchain, sender *keys.PrivateKey, nonce uint32, networkFee int64, mainTx *transaction.Transaction) *payload.P2PNotaryRequest { + h := chain.BlockHeight() + // If mainTx is nil, then generate it. + if mainTx == nil { + mainTx = &transaction.Transaction{ + Nonce: nonce, Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}}, Script: []byte{byte(opcode.RET)}, - ValidUntilBlock: 123, - Signers: []transaction.Signer{{Account: util.Uint160{1, 5, 9}}}, + ValidUntilBlock: h + 100, + Signers: []transaction.Signer{{Account: sender.GetScriptHash()}}, Scripts: []transaction.Witness{{ InvocationScript: []byte{1, 4, 7}, VerificationScript: []byte{3, 6, 9}, }}, } - fallbackTx := &transaction.Transaction{ - Script: []byte{byte(opcode.RET)}, - ValidUntilBlock: 123, - Attributes: []transaction.Attribute{ - {Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}, - {Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}}, - {Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}, - }, - Signers: []transaction.Signer{{Account: util.Uint160{1, 4, 7}}, {Account: util.Uint160{9, 8, 7}}}, - Scripts: []transaction.Witness{ - {InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...), VerificationScript: make([]byte, 0)}, - {InvocationScript: []byte{1, 2, 3}, VerificationScript: []byte{1, 2, 3}}}, - } - p := &payload.P2PNotaryRequest{ - MainTransaction: mainTx, - FallbackTransaction: fallbackTx, - Witness: transaction.Witness{ - InvocationScript: []byte{1, 2, 3}, - VerificationScript: []byte{7, 8, 9}, - }, - } - bytes, err := p.Bytes() - require.NoError(t, err) - str := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(bytes)) - runCase(t, true, neorpc.ErrVerificationFailedCode, str)(t) - }) - t.Run("valid request", func(t *testing.T) { - sender := testchain.PrivateKeyByID(0) // owner of the deposit in testchain - p := createValidNotaryRequest(chain, sender, 1) - bytes, err := p.Bytes() - require.NoError(t, err) - str := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(bytes)) - runCase(t, false, 0, str)(t) - }) -} - -// createValidNotaryRequest creates and signs P2PNotaryRequest payload which can -// pass verification. -func createValidNotaryRequest(chain *core.Blockchain, sender *keys.PrivateKey, nonce uint32) *payload.P2PNotaryRequest { - h := chain.BlockHeight() - mainTx := &transaction.Transaction{ - Nonce: nonce, - Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}}, - Script: []byte{byte(opcode.RET)}, - ValidUntilBlock: h + 100, - Signers: []transaction.Signer{{Account: sender.GetScriptHash()}}, - Scripts: []transaction.Witness{{ - InvocationScript: []byte{1, 4, 7}, - VerificationScript: []byte{3, 6, 9}, - }}, } fallbackTx := &transaction.Transaction{ Script: []byte{byte(opcode.RET)}, @@ -2378,7 +2500,7 @@ func createValidNotaryRequest(chain *core.Blockchain, sender *keys.PrivateKey, n Scripts: []transaction.Witness{ {InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...), VerificationScript: []byte{}}, }, - NetworkFee: 2_0000_0000, + NetworkFee: networkFee, } fallbackTx.Scripts = append(fallbackTx.Scripts, transaction.Witness{ InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, sender.SignHashable(uint32(testchain.Network()), fallbackTx)...), diff --git a/pkg/services/rpcsrv/subscription_test.go b/pkg/services/rpcsrv/subscription_test.go index 52ff0c6fb..c3b17fa54 100644 --- a/pkg/services/rpcsrv/subscription_test.go +++ b/pkg/services/rpcsrv/subscription_test.go @@ -144,7 +144,7 @@ func TestSubscriptions(t *testing.T) { // We should manually add NotaryRequest to test notification. sender := testchain.PrivateKeyByID(0) - err := rpcSrv.coreServer.RelayP2PNotaryRequest(createValidNotaryRequest(chain, sender, 1)) + err := rpcSrv.coreServer.RelayP2PNotaryRequest(createValidNotaryRequest(chain, sender, 1, 2_0000_0000, nil)) require.NoError(t, err) for { resp := getNotification(t, respMsgs) @@ -390,7 +390,7 @@ func TestFilteredNotaryRequestSubscriptions(t *testing.T) { t.Run(name, func(t *testing.T) { subID := callSubscribe(t, c, respMsgs, this.params) - err := rpcSrv.coreServer.RelayP2PNotaryRequest(createValidNotaryRequest(chain, priv0, nonce)) + err := rpcSrv.coreServer.RelayP2PNotaryRequest(createValidNotaryRequest(chain, priv0, nonce, 2_0000_0000, nil)) require.NoError(t, err) nonce++