Merge pull request #3098 from nspcc-dev/2951-getrawnotarypool

Implement `getrawnotaryrequest` and `getrawnotarytransaction` RPC extensions, close #2951.
This commit is contained in:
Roman Khimov 2023-08-31 22:05:51 +03:00 committed by GitHub
commit 0d30c834d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 695 additions and 91 deletions

View file

@ -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 to track the contract storage scheme using the specified past chain state. These
methods may be useful for debugging purposes. 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 This method can be used on P2P Notary enabled networks to submit new notary
payloads to be relayed from RPC to P2P. payloads to be relayed from RPC to P2P.

View file

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

View file

@ -637,26 +637,18 @@ func TestMempoolAddWithDataGetData(t *testing.T) {
balance: 100, balance: 100,
} }
mp := New(10, 1, false, nil) 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 // bad, insufficient deposit
r1 := &payload.P2PNotaryRequest{ r1 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0), MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: newTx(t, fs.balance+1), FallbackTransaction: mkTwoSignersTx(fs.balance+1, &nonce),
} }
require.ErrorIs(t, mp.Add(r1.FallbackTransaction, fs, r1), ErrInsufficientFunds) require.ErrorIs(t, mp.Add(r1.FallbackTransaction, fs, r1), ErrInsufficientFunds)
// good // good
r2 := &payload.P2PNotaryRequest{ r2 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0), MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: newTx(t, smallNetFee), FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
} }
require.NoError(t, mp.Add(r2.FallbackTransaction, fs, r2)) require.NoError(t, mp.Add(r2.FallbackTransaction, fs, r2))
require.True(t, mp.ContainsKey(r2.FallbackTransaction.Hash())) 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] // good, higher priority than r2. The resulting mp.verifiedTxes: [r3, r2]
r3 := &payload.P2PNotaryRequest{ r3 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0), MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: newTx(t, smallNetFee+1), FallbackTransaction: mkTwoSignersTx(smallNetFee+1, &nonce),
} }
require.NoError(t, mp.Add(r3.FallbackTransaction, fs, r3)) require.NoError(t, mp.Add(r3.FallbackTransaction, fs, r3))
require.True(t, mp.ContainsKey(r3.FallbackTransaction.Hash())) 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] // good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4]
r4 := &payload.P2PNotaryRequest{ r4 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0), MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: newTx(t, smallNetFee), FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
} }
require.NoError(t, mp.Add(r4.FallbackTransaction, fs, r4)) require.NoError(t, mp.Add(r4.FallbackTransaction, fs, r4))
require.True(t, mp.ContainsKey(r4.FallbackTransaction.Hash())) 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] // good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4, r5]
r5 := &payload.P2PNotaryRequest{ r5 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0), MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: newTx(t, smallNetFee), FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
} }
require.NoError(t, mp.Add(r5.FallbackTransaction, fs, r5)) require.NoError(t, mp.Add(r5.FallbackTransaction, fs, r5))
require.True(t, mp.ContainsKey(r5.FallbackTransaction.Hash())) require.True(t, mp.ContainsKey(r5.FallbackTransaction.Hash()))
@ -713,7 +705,7 @@ func TestMempoolAddWithDataGetData(t *testing.T) {
require.False(t, ok) require.False(t, ok)
// but getting nil data is OK. The resulting mp.verifiedTxes: [r3, r2, r4, r5, r6] // 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.NoError(t, mp.Add(r6, fs, nil))
require.True(t, mp.ContainsKey(r6.Hash())) require.True(t, mp.ContainsKey(r6.Hash()))
data, ok = mp.TryGetData(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 // getting data: item is in verifiedMap, but not in verifiedTxes
r7 := &payload.P2PNotaryRequest{ r7 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0), MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: newTx(t, smallNetFee), FallbackTransaction: mkTwoSignersTx(smallNetFee, &nonce),
} }
require.NoError(t, mp.Add(r7.FallbackTransaction, fs, r4)) require.NoError(t, mp.Add(r7.FallbackTransaction, fs, r4))
require.True(t, mp.ContainsKey(r7.FallbackTransaction.Hash())) require.True(t, mp.ContainsKey(r7.FallbackTransaction.Hash()))
r8 := &payload.P2PNotaryRequest{ r8 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0), MainTransaction: mkTwoSignersTx(0, &nonce),
FallbackTransaction: newTx(t, smallNetFee-1), FallbackTransaction: mkTwoSignersTx(smallNetFee-1, &nonce),
} }
require.NoError(t, mp.Add(r8.FallbackTransaction, fs, r4)) require.NoError(t, mp.Add(r8.FallbackTransaction, fs, r4))
require.True(t, mp.ContainsKey(r8.FallbackTransaction.Hash())) require.True(t, mp.ContainsKey(r8.FallbackTransaction.Hash()))
@ -737,3 +729,99 @@ func TestMempoolAddWithDataGetData(t *testing.T) {
_, ok = mp.TryGetData(r7.FallbackTransaction.Hash()) _, ok = mp.TryGetData(r7.FallbackTransaction.Hash())
require.False(t, ok) 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)
}

View file

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

View file

@ -96,6 +96,8 @@ Supported methods
Extensions: Extensions:
getblocksysfee getblocksysfee
getrawnotarypool
getrawnotarytransaction
submitnotaryrequest submitnotaryrequest
Unsupported methods Unsupported methods

View file

@ -1285,3 +1285,44 @@ func (c *Client) TerminateSession(sessionID uuid.UUID) (bool, error) {
return resp, nil 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
}

View file

@ -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 { type rpcClientErrorCase struct {

View file

@ -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) { func TestCalculateNotaryFee(t *testing.T) {
chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t)
defer chain.Close() defer chain.Close()

View file

@ -229,6 +229,8 @@ var rpcHandlers = map[string]func(*Server, params.Params) (any, *neorpc.Error){
"getpeers": (*Server).getPeers, "getpeers": (*Server).getPeers,
"getproof": (*Server).getProof, "getproof": (*Server).getProof,
"getrawmempool": (*Server).getRawMempool, "getrawmempool": (*Server).getRawMempool,
"getrawnotarypool": (*Server).getRawNotaryPool,
"getrawnotarytransaction": (*Server).getRawNotaryTransaction,
"getrawtransaction": (*Server).getrawtransaction, "getrawtransaction": (*Server).getrawtransaction,
"getstate": (*Server).getState, "getstate": (*Server).getState,
"getstateheight": (*Server).getStateHeight, "getstateheight": (*Server).getStateHeight,
@ -3090,3 +3092,54 @@ func (s *Server) Addresses() []string {
} }
return res 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
}

View file

@ -2275,8 +2275,11 @@ func TestSubmitOracle(t *testing.T) {
t.Run("Valid", runCase(t, false, 0, pubStr, `1`, txSigStr, msgSigStr)) t.Run("Valid", runCase(t, false, 0, pubStr, `1`, txSigStr, msgSigStr))
} }
func TestSubmitNotaryRequest(t *testing.T) { func TestNotaryRequestRPC(t *testing.T) {
rpc := `{"jsonrpc": "2.0", "id": 1, "method": "submitnotaryrequest", "params": %s}` 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) { t.Run("disabled P2PSigExtensions", func(t *testing.T) {
chain, rpcSrv, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) { chain, rpcSrv, httpSrv := initClearServerWithCustomConfig(t, func(c *config.Config) {
@ -2284,87 +2287,206 @@ func TestSubmitNotaryRequest(t *testing.T) {
}) })
defer chain.Close() defer chain.Close()
defer rpcSrv.Shutdown() defer rpcSrv.Shutdown()
req := fmt.Sprintf(rpc, "[]") t.Run("submitnotaryrequest", func(t *testing.T) {
body := doRPCCallOverHTTP(req, httpSrv.URL, t) body := doRPCCallOverHTTP(fmt.Sprintf(rpcSubmit, "[]"), httpSrv.URL, t)
checkErrGetResult(t, body, true, neorpc.InternalServerErrorCode) 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) chain, rpcSrv, httpSrv := initServerWithInMemoryChainAndServices(t, false, true, false)
defer chain.Close() defer chain.Close()
defer rpcSrv.Shutdown() 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) { return func(t *testing.T) {
ps := `[` + strings.Join(params, ",") + `]` ps := `[` + strings.Join(params, ",") + `]`
req := fmt.Sprintf(rpc, ps) req := fmt.Sprintf(rpcSubmit, ps)
body := doRPCCallOverHTTP(req, httpSrv.URL, t) body := doRPCCallOverHTTP(req, httpSrv.URL, t)
checkErrGetResult(t, body, fail, errCode) 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("getrawnotarypool", func(t *testing.T) {
t.Run("invalid request bytes", runCase(t, true, neorpc.InvalidParamsCode, `"not-a-request"`)) t.Run("empty pool", func(t *testing.T) {
t.Run("invalid request", func(t *testing.T) { body := doRPCCallOverHTTP(rpcPool, httpSrv.URL, t)
mainTx := &transaction.Transaction{ 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}}}, Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}},
Script: []byte{byte(opcode.RET)}, Script: []byte{byte(opcode.RET)},
ValidUntilBlock: 123, ValidUntilBlock: h + 100,
Signers: []transaction.Signer{{Account: util.Uint160{1, 5, 9}}}, Signers: []transaction.Signer{{Account: sender.GetScriptHash()}},
Scripts: []transaction.Witness{{ Scripts: []transaction.Witness{{
InvocationScript: []byte{1, 4, 7}, InvocationScript: []byte{1, 4, 7},
VerificationScript: []byte{3, 6, 9}, 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{ fallbackTx := &transaction.Transaction{
Script: []byte{byte(opcode.RET)}, Script: []byte{byte(opcode.RET)},
@ -2378,7 +2500,7 @@ func createValidNotaryRequest(chain *core.Blockchain, sender *keys.PrivateKey, n
Scripts: []transaction.Witness{ Scripts: []transaction.Witness{
{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...), VerificationScript: []byte{}}, {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{ fallbackTx.Scripts = append(fallbackTx.Scripts, transaction.Witness{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, sender.SignHashable(uint32(testchain.Network()), fallbackTx)...), InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, sender.SignHashable(uint32(testchain.Network()), fallbackTx)...),

View file

@ -144,7 +144,7 @@ func TestSubscriptions(t *testing.T) {
// We should manually add NotaryRequest to test notification. // We should manually add NotaryRequest to test notification.
sender := testchain.PrivateKeyByID(0) 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) require.NoError(t, err)
for { for {
resp := getNotification(t, respMsgs) resp := getNotification(t, respMsgs)
@ -390,7 +390,7 @@ func TestFilteredNotaryRequestSubscriptions(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
subID := callSubscribe(t, c, respMsgs, this.params) 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) require.NoError(t, err)
nonce++ nonce++