diff --git a/pkg/rpcclient/doc.go b/pkg/rpcclient/doc.go index 8871a53718..ef2e1d198e 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 0a72155588..c6a66df387 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 5562a21ac7..c496dbe0a1 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 0e3b0dd6d9..18c8048ab4 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -1135,6 +1135,127 @@ 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) + + checkTxInActPool := func(t *testing.T, mainHash, fallbackHash util.Uint256, actNotaryPool *result.RawNotaryPool) { + actFallbacks, ok := actNotaryPool.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)) + checkTxInActPool(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)) + checkTxInActPool(t, mainHash1, fallbackHash1, actNotaryPool) + checkTxInActPool(t, mainHash2, fallbackHash2, actNotaryPool) + }) + }) + t.Run("getrawnotarytransaction", func(t *testing.T) { + compareTx := func(t *testing.T, expectedTx, actualTx *transaction.Transaction) func(t *testing.T) { + return func(t *testing.T) { + require.Equal(t, expectedTx.Hash(), actualTx.Hash()) + require.Equal(t, expectedTx.Size(), actualTx.Size()) + } + } + 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) + }) + t.Run("transactions from pool", func(t *testing.T) { + mainTx1, err := c.GetRawNotaryTransaction(mainHash1) + require.NoError(t, err) + compareTx(t, tx1, mainTx1) + _, err = c.GetRawNotaryTransaction(fallbackHash1) + require.NoError(t, err) + + mainTx2, err := c.GetRawNotaryTransaction(mainHash2) + require.NoError(t, err) + compareTx(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) + compareTx(t, tx1, mainTx1) + _, err = c.GetRawNotaryTransactionVerbose(fallbackHash1) + require.NoError(t, err) + + mainTx2, err := c.GetRawNotaryTransactionVerbose(mainHash2) + require.NoError(t, err) + compareTx(t, tx2, mainTx2) + _, err = c.GetRawNotaryTransactionVerbose(fallbackHash2) + require.NoError(t, err) + }) + }) + }) +} + func TestCalculateNotaryFee(t *testing.T) { chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) defer chain.Close()