Skip to content

Commit 32c29eb

Browse files
authored
update nonce precheck logic (0.8) (#558)
nonce precheck logic is currently missing some logic and logs - add log to show nonce value to help better inform WRONG_NONCE errors observed - Update RESOURCE_NOT_FOUND error to allow for resource details - Add nonce precheck test cases for better coverage - break out account check from nonce check - add account check tests cases - simplify nonce precheck logic - clean up orphaned references observed - Add prefined import Signed-off-by: Nana Essilfie-Conduah <[email protected]>
1 parent 0091f24 commit 32c29eb

File tree

4 files changed

+109
-24
lines changed

4 files changed

+109
-24
lines changed

packages/relay/src/lib/errors/JsonRpcError.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ export const predefined = {
114114
code: -32010,
115115
message: 'Request timeout. Please try again.'
116116
}),
117-
'RESOURCE_NOT_FOUND': new JsonRpcError({
117+
'RESOURCE_NOT_FOUND': (message: string = '') => new JsonRpcError({
118118
name: 'Resource not found',
119119
code: -32001,
120-
message: 'Requested resource not found'
120+
message: `Requested resource not found. ${message}`
121121
}),
122122
'RANGE_TOO_LARGE': new JsonRpcError({
123123
name: 'Block range too large',

packages/relay/src/lib/eth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,7 @@ export class EthImpl implements Eth {
10691069
}
10701070
if (_.isNil(blockResponse) || blockResponse.hash === undefined) {
10711071
// block not found.
1072-
throw predefined.RESOURCE_NOT_FOUND;
1072+
throw predefined.RESOURCE_NOT_FOUND(`block '${blockNumberOrTag}'.`);
10731073
}
10741074

10751075
return blockResponse;

packages/relay/src/lib/precheck.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,36 +60,33 @@ export class Precheck {
6060
const parsedTx = Precheck.parseTxIfNeeded(transaction);
6161

6262
this.gasLimit(parsedTx, requestId);
63-
await this.nonce(parsedTx, requestId);
63+
const mirrorAccountInfo = await this.verifyAccount(parsedTx, requestId);
64+
await this.nonce(parsedTx, mirrorAccountInfo.ethereum_nonce, requestId);
6465
this.chainId(parsedTx, requestId);
6566
this.value(parsedTx);
6667
this.gasPrice(parsedTx, gasPrice, requestId);
6768
await this.balance(parsedTx, EthImpl.ethSendRawTransaction, requestId);
6869
}
6970

71+
async verifyAccount(tx: Transaction, requestId?: string) {
72+
// verify account
73+
const accountInfo = await this.mirrorNodeClient.getAccount(tx.from!, requestId);
74+
if (accountInfo == null) {
75+
this.logger.trace(`${requestId} Failed to retrieve address '${tx.from}' account details from mirror node on verify account precheck for sendRawTransaction(transaction=${JSON.stringify(tx)})`);
76+
throw predefined.RESOURCE_NOT_FOUND(`address '${tx.from}'.`);
77+
}
78+
79+
return accountInfo;
80+
}
81+
7082
/**
7183
* @param tx
7284
*/
73-
async nonce(tx: Transaction, requestId?: string) {
74-
const rsTx = await ethers.utils.resolveProperties({
75-
gasPrice: tx.gasPrice,
76-
gasLimit: tx.gasLimit,
77-
value: tx.value,
78-
nonce: tx.nonce,
79-
data: tx.data,
80-
chainId: tx.chainId,
81-
to: tx.to
82-
});
83-
const raw = ethers.utils.serializeTransaction(rsTx);
84-
const recoveredAddress = ethers.utils.recoverAddress(
85-
ethers.utils.arrayify(ethers.utils.keccak256(raw)),
86-
// @ts-ignore
87-
ethers.utils.joinSignature({ 'r': tx.r, 's': tx.s, 'v': tx.v })
88-
);
89-
const accountInfo = await this.mirrorNodeClient.getAccount(recoveredAddress, requestId);
85+
async nonce(tx: Transaction, accountInfoNonce: number, requestId?: string) {
86+
this.logger.trace(`${requestId} Nonce precheck for sendRawTransaction(tx.nonce=${tx.nonce}, accountInfoNonce=${accountInfoNonce})`);
9087

9188
// @ts-ignore
92-
if (accountInfo && accountInfo.ethereum_nonce > tx.nonce) {
89+
if (accountInfoNonce > tx.nonce) {
9390
throw predefined.NONCE_TOO_LOW;
9491
}
9592
}
@@ -140,7 +137,7 @@ export class Precheck {
140137
const accountResponse: any = await this.mirrorNodeClient.getAccount(tx.from!, requestId);
141138
if (accountResponse == null) {
142139
this.logger.trace(`${requestIdPrefix} Failed to retrieve account details from mirror node on balance precheck for sendRawTransaction(transaction=${JSON.stringify(tx)}, totalValue=${txTotalValue})`);
143-
throw predefined.RESOURCE_NOT_FOUND;
140+
throw predefined.RESOURCE_NOT_FOUND(`tx.from '${tx.from}'.`);
144141
}
145142

146143
try {

packages/relay/tests/lib/precheck.spec.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const registry = new Registry();
2626
import sinon from 'sinon';
2727
import pino from 'pino';
2828
import { Precheck } from "../../src/lib/precheck";
29-
import { expectedError, signTransaction } from "../helpers";
29+
import { expectedError, mockData, signTransaction } from "../helpers";
3030
import { MirrorNodeClient, SDKClient } from "../../src/lib/clients";
3131
import axios from "axios";
3232
import MockAdapter from "axios-mock-adapter";
@@ -345,4 +345,92 @@ describe('Precheck', async function() {
345345
expect(result).to.not.exist;
346346
});
347347
});
348+
349+
describe('nonce', async function() {
350+
const defaultNonce = 3;
351+
const defaultTx = {
352+
value: oneTinyBar,
353+
gasPrice: defaultGasPrice,
354+
chainId: defaultChainId,
355+
nonce: defaultNonce
356+
};
357+
358+
const mirrorAccount = {
359+
ethereum_nonce: defaultNonce
360+
};
361+
362+
it(`should fail for low nonce`, async function () {
363+
const tx = {
364+
...defaultTx,
365+
nonce: 1
366+
};
367+
const signed = await signTransaction(tx);
368+
const parsedTx = ethers.utils.parseTransaction(signed);
369+
370+
mock.onGet(`accounts/${parsedTx.from}`).reply(200, mirrorAccount);
371+
372+
373+
try {
374+
await precheck.nonce(parsedTx, mirrorAccount.ethereum_nonce);
375+
expectedError();
376+
} catch (e: any) {
377+
expect(e).to.eql(predefined.NONCE_TOO_LOW);
378+
}
379+
});
380+
381+
it(`should not fail for next nonce`, async function () {
382+
const tx = {
383+
...defaultTx,
384+
nonce: 4
385+
};
386+
const signed = await signTransaction(tx);
387+
const parsedTx = ethers.utils.parseTransaction(signed);
388+
389+
mock.onGet(`accounts/${parsedTx.from}`).reply(200, mirrorAccount);
390+
391+
await precheck.nonce(parsedTx, mirrorAccount.ethereum_nonce);
392+
});
393+
});
394+
395+
describe('account', async function() {
396+
const defaultNonce = 3;
397+
const defaultTx = {
398+
value: oneTinyBar,
399+
gasPrice: defaultGasPrice,
400+
chainId: defaultChainId,
401+
nonce: defaultNonce,
402+
from: mockData.accountEvmAddress
403+
};
404+
405+
const signed = await signTransaction(defaultTx);
406+
const parsedTx = ethers.utils.parseTransaction(signed);
407+
408+
const mirrorAccount = {
409+
evm_address: mockData.accountEvmAddress,
410+
ethereum_nonce: defaultNonce
411+
};
412+
413+
it(`should fail for missing account`, async function () {
414+
mock.onGet(`accounts/${mockData.accountEvmAddress}`).reply(404, mockData.notFound);
415+
416+
417+
try {
418+
await precheck.verifyAccount(parsedTx);
419+
expectedError();
420+
} catch (e: any) {
421+
expect(e).to.exist;
422+
expect(e.code).to.eq(-32001);
423+
expect(e.name).to.eq('Resource not found');
424+
expect(e.message).to.contain(mockData.accountEvmAddress);
425+
}
426+
});
427+
428+
it(`should not fail for matched account`, async function () {
429+
mock.onGet(`accounts/${mockData.accountEvmAddress}`).reply(200, mirrorAccount);
430+
const account = await precheck.verifyAccount(parsedTx);
431+
432+
433+
expect(account.ethereum_nonce).to.eq(defaultNonce);
434+
});
435+
});
348436
});

0 commit comments

Comments
 (0)