Skip to content

Commit b7087b9

Browse files
authored
rpc: expose missing nonces in future pool (#1973)
# Goal The goal of this PR is to expose missing nonces in future pool which causes problems when submitting a lot of transactions from the same account. Closes #1974 ## Discussions - I had to remove node RPC test for events, since the setup fundamentally didn't work with the Pool and I added the removed test into e2e side. # Checklist - [X] Chain spec updated - [X] Custom RPC OR Runtime API added/changed? Updated js/api-augment. - [X] Tests added
1 parent a99aab4 commit b7087b9

File tree

9 files changed

+203
-132
lines changed

9 files changed

+203
-132
lines changed

e2e/miscellaneous/frequency.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import '@frequency-chain/api-augment';
2+
import assert from 'assert';
3+
import { DOLLARS, createAndFundKeypair, getBlockNumber, getNonce } from '../scaffolding/helpers';
4+
import { KeyringPair } from '@polkadot/keyring/types';
5+
import { Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
6+
import { getFundingSource } from '../scaffolding/funding';
7+
import { u8, Option } from '@polkadot/types';
8+
import { u8aToHex } from '@polkadot/util/u8a/toHex';
9+
10+
const fundingSource: KeyringPair = getFundingSource('frequency-misc');
11+
12+
describe('Frequency', function () {
13+
describe('setup', function () {
14+
let keypairA: KeyringPair;
15+
let keypairB: KeyringPair;
16+
17+
before(async function () {
18+
keypairA = await createAndFundKeypair(fundingSource, 100n * DOLLARS);
19+
keypairB = await createAndFundKeypair(fundingSource, 100n * DOLLARS);
20+
});
21+
22+
it('Get events successfully', async function () {
23+
const balance_pallet = new u8(ExtrinsicHelper.api.registry, 10);
24+
const transfer_event = new u8(ExtrinsicHelper.api.registry, 2);
25+
const dest_account = u8aToHex(keypairB.publicKey).slice(2);
26+
const beforeBlockNumber = await getBlockNumber();
27+
28+
const extrinsic = new Extrinsic(
29+
() => ExtrinsicHelper.api.tx.balances.transferKeepAlive(keypairB.address, 1n * DOLLARS),
30+
keypairA,
31+
ExtrinsicHelper.api.events.balances.Transfer
32+
);
33+
const { target } = await extrinsic.signAndSend();
34+
assert.notEqual(target, undefined, 'should have returned Transfer event');
35+
36+
const afterBlockNumber = await getBlockNumber();
37+
let found = false;
38+
39+
for (let i = beforeBlockNumber + 1; i <= afterBlockNumber; i++) {
40+
const block = await ExtrinsicHelper.apiPromise.rpc.chain.getBlockHash(i);
41+
const events = await ExtrinsicHelper.getFrequencyEvents(block);
42+
if (
43+
events.find(
44+
(e) => e.pallet.eq(balance_pallet) && e.event.eq(transfer_event) && e.data.toHex().includes(dest_account)
45+
)
46+
) {
47+
found = true;
48+
break;
49+
}
50+
}
51+
52+
assert(found, 'Could not find the desired event');
53+
});
54+
55+
it('Get missing nonce successfully', async function () {
56+
const nonce = await getNonce(keypairB);
57+
for (let i = 0; i < 10; i += 2) {
58+
const extrinsic = new Extrinsic(
59+
() => ExtrinsicHelper.api.tx.balances.transferKeepAlive(keypairA.address, 1n * DOLLARS),
60+
keypairB,
61+
ExtrinsicHelper.api.events.balances.Transfer
62+
);
63+
// intentionally we don't want an await here
64+
extrinsic.signAndSend(nonce + i);
65+
}
66+
// wait a little for all of the above transactions to get queued
67+
await new Promise((resolve) => setTimeout(resolve, 1000));
68+
const missingNonce = await ExtrinsicHelper.getMissingNonceValues(keypairB.publicKey);
69+
assert.equal(missingNonce.length, 4, 'Could not get missing nonce values');
70+
71+
// applying the missing nonce values to next transactions to unblock the stuck ones
72+
for (const missing of missingNonce) {
73+
const extrinsic = new Extrinsic(
74+
() => ExtrinsicHelper.api.tx.balances.transferKeepAlive(keypairA.address, 1n * DOLLARS),
75+
keypairB,
76+
ExtrinsicHelper.api.events.balances.Transfer
77+
);
78+
const { target } = await extrinsic.signAndSend(missing.toNumber());
79+
assert.notEqual(target, undefined, 'should have returned Transfer event');
80+
}
81+
});
82+
});
83+
});

e2e/scaffolding/extrinsicHelpers.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,20 @@ import { firstValueFrom, filter, map, pipe, tap } from 'rxjs';
99
import { getBlockNumber, getExistentialDeposit, log, Sr25519Signature } from './helpers';
1010
import autoNonce, { AutoNonce } from './autoNonce';
1111
import { connect, connectPromise } from './apiConnection';
12-
import { DispatchError, Event, SignedBlock } from '@polkadot/types/interfaces';
12+
import { DispatchError, Event, Index, SignedBlock } from '@polkadot/types/interfaces';
1313
import { IsEvent } from '@polkadot/types/metadata/decorate/types';
1414
import {
1515
HandleResponse,
1616
ItemizedStoragePageResponse,
1717
MessageSourceId,
1818
PaginatedStorageResponse,
1919
PresumptiveSuffixesResponse,
20+
RpcEvent,
2021
SchemaResponse,
2122
} from '@frequency-chain/api-augment/interfaces';
2223
import { u8aToHex } from '@polkadot/util/u8a/toHex';
2324
import { u8aWrapBytes } from '@polkadot/util';
24-
import type { Call } from '@polkadot/types/interfaces/runtime';
25+
import type { AccountId32, Call, H256 } from '@polkadot/types/interfaces/runtime';
2526
import { hasRelayChain } from './env';
2627

2728
export interface ReleaseSchedule {
@@ -728,6 +729,14 @@ export class ExtrinsicHelper {
728729
return ExtrinsicHelper.apiPromise.rpc.handles.validateHandle(base_handle);
729730
}
730731

732+
public static getFrequencyEvents(at: H256 | string): Promise<Vec<RpcEvent>> {
733+
return ExtrinsicHelper.apiPromise.rpc.frequency.getEvents(at);
734+
}
735+
736+
public static getMissingNonceValues(accountId: AccountId32 | string | Uint8Array): Promise<Vec<Index>> {
737+
return ExtrinsicHelper.apiPromise.rpc.frequency.getMissingNonceValues(accountId);
738+
}
739+
731740
public static addOnChainMessage(keys: KeyringPair, schemaId: any, payload: string) {
732741
return new Extrinsic(
733742
() => ExtrinsicHelper.api.tx.messages.addOnchainMessage(null, schemaId, payload),

e2e/scaffolding/funding.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const fundingSources = [
2525
'msa-key-management',
2626
'stateful-storage-handle-paginated',
2727
'stateful-storage-handle-itemized',
28+
'frequency-misc',
2829
] as const;
2930

3031
// Get the correct key for this Funding Source

js/api-augment/definitions/frequency.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ export default {
1010
],
1111
type: 'Vec<RpcEvent>',
1212
},
13+
getMissingNonceValues: {
14+
description: 'Get missing nonce values for an account',
15+
params: [
16+
{
17+
name: 'account',
18+
type: 'AccountId32',
19+
},
20+
],
21+
type: 'Vec<Index>',
22+
},
1323
},
1424
types: {
1525
RpcEvent: {

node/service/src/rpc/frequency_rpc.rs

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,130 @@ use common_primitives::rpc::RpcEvent;
1111
use jsonrpsee::{
1212
core::{async_trait, RpcResult},
1313
proc_macros::rpc,
14+
types::error::{CallError, ErrorObject},
1415
};
16+
use parity_scale_codec::{Codec, Decode, Encode};
17+
use sc_transaction_pool_api::{InPoolTransaction, TransactionPool};
1518
use sp_api::ProvideRuntimeApi;
16-
use sp_runtime::traits::Block as BlockT;
19+
use sp_blockchain::HeaderBackend;
20+
use sp_runtime::traits::{AtLeast32Bit, Block as BlockT, One};
1721
use std::sync::Arc;
22+
use substrate_frame_rpc_system::AccountNonceApi;
1823
use system_runtime_api::AdditionalRuntimeApi;
1924

25+
/// This is an upper limit to restrict the number of returned nonce holes to eliminate a potential
26+
/// attack vector
27+
const MAX_RETURNED_MISSING_NONCE_SIZE: usize = 1000;
2028
/// Frequency MSA Custom RPC API
2129
#[rpc(client, server)]
22-
pub trait FrequencyRpcApi<B: BlockT> {
30+
pub trait FrequencyRpcApi<B: BlockT, AccountId, Nonce> {
2331
/// gets the events for a block hash
2432
#[method(name = "frequency_getEvents")]
2533
fn get_events(&self, at: B::Hash) -> RpcResult<Vec<RpcEvent>>;
34+
/// returns a list of missing nonce values from Future transaction pool.
35+
#[method(name = "frequency_getMissingNonceValues")]
36+
fn get_missing_nonce_values(&self, account: AccountId) -> RpcResult<Vec<Nonce>>;
2637
}
2738

2839
/// The client handler for the API used by Frequency Service RPC with `jsonrpsee`
29-
pub struct FrequencyRpcHandler<C, M> {
40+
pub struct FrequencyRpcHandler<P: TransactionPool, C, M> {
3041
client: Arc<C>,
42+
pool: Arc<P>,
3143
_marker: std::marker::PhantomData<M>,
3244
}
3345

34-
impl<C, M> FrequencyRpcHandler<C, M> {
46+
impl<P: TransactionPool, C, M> FrequencyRpcHandler<P, C, M> {
3547
/// Create new instance with the given reference to the client.
36-
pub fn new(client: Arc<C>) -> Self {
37-
Self { client, _marker: Default::default() }
48+
pub fn new(client: Arc<C>, pool: Arc<P>) -> Self {
49+
Self { client, pool, _marker: Default::default() }
3850
}
3951
}
4052

4153
#[async_trait]
42-
impl<C, Block> FrequencyRpcApiServer<Block> for FrequencyRpcHandler<C, Block>
54+
impl<P, C, Block, AccountId, Nonce> FrequencyRpcApiServer<Block, AccountId, Nonce>
55+
for FrequencyRpcHandler<P, C, Block>
4356
where
4457
Block: BlockT,
58+
C: HeaderBackend<Block>,
4559
C: Send + Sync + 'static,
4660
C: ProvideRuntimeApi<Block>,
4761
C::Api: AdditionalRuntimeApi<Block>,
62+
C::Api: AccountNonceApi<Block, AccountId, Nonce>,
63+
P: TransactionPool + 'static,
64+
AccountId: Clone + Codec,
65+
Nonce: Clone + Encode + Decode + AtLeast32Bit + 'static,
4866
{
4967
fn get_events(&self, at: <Block as BlockT>::Hash) -> RpcResult<Vec<RpcEvent>> {
5068
let api = self.client.runtime_api();
5169
return map_rpc_result(api.get_events(at))
5270
}
71+
72+
fn get_missing_nonce_values(&self, account: AccountId) -> RpcResult<Vec<Nonce>> {
73+
let api = self.client.runtime_api();
74+
let best = self.client.info().best_hash;
75+
76+
let nonce = api.account_nonce(best, account.clone()).map_err(|e| {
77+
CallError::Custom(ErrorObject::owned(1, "Unable to query nonce.", Some(e.to_string())))
78+
})?;
79+
Ok(get_missing_nonces(&*self.pool, account, nonce))
80+
}
81+
}
82+
83+
/// Finds any missing nonce values inside Future pool and return them as result
84+
fn get_missing_nonces<P, AccountId, Nonce>(pool: &P, account: AccountId, nonce: Nonce) -> Vec<Nonce>
85+
where
86+
P: TransactionPool,
87+
AccountId: Clone + Encode,
88+
Nonce: Clone + Encode + Decode + AtLeast32Bit + 'static,
89+
{
90+
// Now we need to query the transaction pool
91+
// and find transactions originating from the same sender.
92+
// Since extrinsics are opaque to us, we look for them using
93+
// `provides` tag. And increment the nonce if we find a transaction
94+
// that matches the current one.
95+
let mut current_nonce = nonce.clone();
96+
let encoded_account = account.clone().encode();
97+
let mut current_tag = (account.clone(), nonce).encode();
98+
for tx in pool.ready() {
99+
// since transactions in `ready()` need to be ordered by nonce
100+
// it's fine to continue with current iterator.
101+
if tx.provides().get(0) == Some(&current_tag) {
102+
current_nonce += One::one();
103+
current_tag = (account.clone(), current_nonce.clone()).encode();
104+
}
105+
}
106+
107+
let mut result = vec![];
108+
let mut my_in_future: Vec<_> = pool
109+
.futures()
110+
.into_iter()
111+
.filter_map(|x| match x.provides().get(0) {
112+
// filtering transactions by account
113+
Some(tag) if tag.starts_with(&encoded_account) => {
114+
if let Ok(nonce) = Nonce::decode(&mut &tag[encoded_account.len()..]) {
115+
return Some(nonce)
116+
}
117+
None
118+
},
119+
_ => None,
120+
})
121+
.collect();
122+
my_in_future.sort();
123+
124+
for future_nonce in my_in_future {
125+
while current_nonce < future_nonce {
126+
result.push(current_nonce.clone());
127+
current_nonce += One::one();
128+
129+
// short circuit if we reached the limit
130+
if result.len() == MAX_RETURNED_MISSING_NONCE_SIZE {
131+
return result
132+
}
133+
}
134+
135+
// progress the current_nonce
136+
current_nonce += One::one();
137+
}
138+
139+
result
53140
}

node/service/src/rpc/mod.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata};
2020

2121
mod frequency_rpc;
2222

23-
#[cfg(test)]
24-
mod tests;
25-
2623
/// A type representing all RPC extensions.
2724
pub type RpcExtension = jsonrpsee::RpcModule<()>;
2825

@@ -80,15 +77,15 @@ where
8077
let mut module = RpcExtension::new(());
8178
let FullDeps { client, pool, deny_unsafe, command_sink } = deps;
8279

83-
module.merge(System::new(client.clone(), pool, deny_unsafe).into_rpc())?;
80+
module.merge(System::new(client.clone(), pool.clone(), deny_unsafe).into_rpc())?;
8481
module.merge(TransactionPayment::new(client.clone()).into_rpc())?;
8582
module.merge(MessagesHandler::new(client.clone()).into_rpc())?;
8683
module.merge(SchemasHandler::new(client.clone()).into_rpc())?;
8784
module.merge(MsaHandler::new(client.clone(), offchain).into_rpc())?;
8885
module.merge(StatefulStorageHandler::new(client.clone()).into_rpc())?;
8986
module.merge(HandlesHandler::new(client.clone()).into_rpc())?;
9087
module.merge(CapacityPaymentHandler::new(client.clone()).into_rpc())?;
91-
module.merge(FrequencyRpcHandler::new(client).into_rpc())?;
88+
module.merge(FrequencyRpcHandler::new(client, pool).into_rpc())?;
9289
if let Some(command_sink) = command_sink {
9390
module.merge(
9491
// We provide the rpc handler with the sending end of the channel to allow the rpc

node/service/src/rpc/tests/mod.rs

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)