Skip to content

Commit a0de46e

Browse files
committed
feat: create acceptance tests nonce ordering (#4642)
Signed-off-by: Konstantina Blazhukova <[email protected]>
1 parent 5bcbd60 commit a0de46e

File tree

2 files changed

+221
-2
lines changed

2 files changed

+221
-2
lines changed

packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -919,4 +919,4 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
919919
});
920920
});
921921
});
922-
});
922+
});

packages/server/tests/acceptance/sendRawTransactionExtension.spec.ts

Lines changed: 220 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Constants from '@hashgraph/json-rpc-relay/dist/lib/constants';
99
// Errors and constants from local resources
1010
import { predefined } from '@hashgraph/json-rpc-relay/dist/lib/errors/JsonRpcError';
1111
import { RequestDetails } from '@hashgraph/json-rpc-relay/dist/lib/types';
12+
import { overrideEnvsInMochaDescribe, withOverriddenEnvsInMochaTest } from '@hashgraph/json-rpc-relay/tests/helpers';
1213
import { expect } from 'chai';
1314

1415
import MirrorClient from '../clients/mirrorClient';
@@ -32,6 +33,15 @@ describe('@sendRawTransactionExtension Acceptance Tests', function () {
3233
}: { servicesNode: ServicesClient; mirrorNode: MirrorClient; relay: RelayClient; initialBalance: string } = global;
3334

3435
const CHAIN_ID = ConfigService.get('CHAIN_ID');
36+
const ONE_TINYBAR = Utils.add0xPrefix(Utils.toHex(Constants.TINYBAR_TO_WEIBAR_COEF));
37+
const defaultLondonTransactionData = {
38+
value: ONE_TINYBAR,
39+
chainId: Number(CHAIN_ID),
40+
maxPriorityFeePerGas: Assertions.defaultGasPrice,
41+
maxFeePerGas: Assertions.defaultGasPrice,
42+
gasLimit: numberTo0x(3_000_000),
43+
type: 2,
44+
};
3545
const requestDetails = new RequestDetails({ requestId: 'sendRawTransactionPrecheck', ipAddress: '0.0.0.0' });
3646
const sendRawTransaction = relay.sendRawTransaction;
3747

@@ -41,7 +51,7 @@ describe('@sendRawTransactionExtension Acceptance Tests', function () {
4151

4252
this.beforeAll(async () => {
4353
const initialAccount: AliasAccount = global.accounts[0];
44-
const neededAccounts: number = 3;
54+
const neededAccounts: number = 5;
4555
accounts.push(
4656
...(await Utils.createMultipleAliasAccounts(mirrorNode, initialAccount, neededAccounts, initialBalance)),
4757
);
@@ -297,4 +307,213 @@ describe('@sendRawTransactionExtension Acceptance Tests', function () {
297307
expect(info.result).to.equal('INSUFFICIENT_TX_FEE');
298308
});
299309
});
310+
311+
describe('@nonce-ordering Lock Service Tests', function () {
312+
this.timeout(240 * 1000); // 240 seconds
313+
overrideEnvsInMochaDescribe({ ENABLE_NONCE_ORDERING: true, USE_ASYNC_TX_PROCESSING: true });
314+
const sendTransactionWithoutWaiting = (signer: any, nonce: number, numOfTxs: number, gasPrice: number) => {
315+
const txPromises = Array.from({ length: numOfTxs }, async (_, i) => {
316+
const tx = {
317+
...defaultLondonTransactionData,
318+
to: accounts[2].address,
319+
value: ONE_TINYBAR,
320+
nonce: nonce + i,
321+
maxPriorityFeePerGas: gasPrice,
322+
maxFeePerGas: gasPrice,
323+
};
324+
const signedTx = await signer.wallet.signTransaction(tx);
325+
return relay.sendRawTransaction(signedTx);
326+
});
327+
328+
return txPromises;
329+
};
330+
331+
it('should handle rapid burst of 10 transactions from same sender', async function () {
332+
const sender = accounts[1];
333+
const startNonce = await relay.getAccountNonce(sender.address);
334+
const gasPrice = await relay.gasPrice();
335+
336+
const txPromises = sendTransactionWithoutWaiting(sender, startNonce, 10, gasPrice);
337+
const txHashes = await Promise.all(txPromises);
338+
const receipts = await Promise.all(txHashes.map((txHash) => relay.pollForValidTransactionReceipt(txHash)));
339+
340+
receipts.forEach((receipt, i) => {
341+
expect(receipt.status).to.equal('0x1', `Transaction ${i} failed`);
342+
});
343+
344+
const finalNonce = await relay.getAccountNonce(sender.address);
345+
expect(finalNonce).to.equal(startNonce + 10);
346+
});
347+
348+
it('should process three transactions from different senders concurrently', async function () {
349+
const senders = [accounts[0], accounts[1], accounts[3]];
350+
const startNonces = await Promise.all(senders.map((sender) => relay.getAccountNonce(sender.address)));
351+
const gasPrice = await relay.gasPrice();
352+
353+
const startTime = Date.now();
354+
355+
// Send transactions from different senders simultaneously
356+
const txPromises = senders.flatMap((sender, i) =>
357+
sendTransactionWithoutWaiting(sender, startNonces[i], 1, gasPrice),
358+
);
359+
360+
const txHashes = await Promise.all(txPromises);
361+
const submitTime = Date.now() - startTime;
362+
363+
// All should succeed
364+
const receipts = await Promise.all(txHashes.map((hash) => relay.pollForValidTransactionReceipt(hash)));
365+
366+
receipts.forEach((receipt) => {
367+
expect(receipt.status).to.equal('0x1');
368+
});
369+
370+
// Verify nonces incremented for each sender independently
371+
const finalNonces = await Promise.all(senders.map((sender) => relay.getAccountNonce(sender.address)));
372+
373+
finalNonces.forEach((nonce, i) => {
374+
expect(nonce).to.equal(startNonces[i] + 1);
375+
});
376+
377+
// Submission should be fast (not blocking each other)
378+
// Even with network latency, parallel submission should be < 5 seconds
379+
expect(submitTime).to.be.lessThan(5000);
380+
});
381+
382+
it('should handle mixed load: 5 txs each from 3 different senders', async function () {
383+
const senders = [accounts[0], accounts[1], accounts[3]];
384+
const startNonces = await Promise.all(senders.map((sender) => relay.getAccountNonce(sender.address)));
385+
const gasPrice = await relay.gasPrice();
386+
387+
// Each sender sends 5 transactions
388+
const allTxPromises = senders.flatMap((sender, senderIdx) =>
389+
sendTransactionWithoutWaiting(sender, startNonces[senderIdx], 5, gasPrice),
390+
);
391+
392+
const txHashes = await Promise.all(allTxPromises);
393+
394+
const receipts = await Promise.all(txHashes.map((txHash) => relay.pollForValidTransactionReceipt(txHash)));
395+
receipts.forEach((receipt, i) => {
396+
expect(receipt.status).to.equal('0x1', `Transaction ${i} failed`);
397+
});
398+
399+
const finalNonces = await Promise.all(senders.map((sender) => relay.getAccountNonce(sender.address)));
400+
401+
finalNonces.forEach((nonce, i) => {
402+
expect(nonce).to.equal(startNonces[i] + 5);
403+
});
404+
});
405+
406+
it('should release lock after consensus submission in async mode', async function () {
407+
const sender = accounts[0];
408+
const startNonce = await relay.getAccountNonce(sender.address);
409+
const gasPrice = await relay.gasPrice();
410+
411+
// Send first transaction
412+
const tx1Hash = await (await sendTransactionWithoutWaiting(sender, startNonce, 1, gasPrice))[0];
413+
414+
// Immediately send second transaction (should queue behind first)
415+
const tx2Hash = await (await sendTransactionWithoutWaiting(sender, startNonce + 1, 1, gasPrice))[0];
416+
417+
// In async mode, both should return immediately with tx hashes
418+
expect(tx1Hash).to.exist;
419+
expect(tx2Hash).to.exist;
420+
421+
// Both should eventually succeed
422+
const receipt1 = await relay.pollForValidTransactionReceipt(tx1Hash);
423+
const receipt2 = await relay.pollForValidTransactionReceipt(tx2Hash);
424+
425+
expect(receipt1.status).to.equal('0x1');
426+
expect(receipt2.status).to.equal('0x1');
427+
428+
// Verify correct nonces
429+
const result1 = await mirrorNode.get(`/contracts/results/${tx1Hash}`);
430+
const result2 = await mirrorNode.get(`/contracts/results/${tx2Hash}`);
431+
432+
expect(result1.nonce).to.equal(startNonce);
433+
expect(result2.nonce).to.equal(startNonce + 1);
434+
});
435+
436+
withOverriddenEnvsInMochaTest({ USE_ASYNC_TX_PROCESSING: false }, () => {
437+
it('should release lock after full processing in sync mode', async function () {
438+
const sender = accounts[0];
439+
const startNonce = await relay.getAccountNonce(sender.address);
440+
const gasPrice = await relay.gasPrice();
441+
442+
// Submit both transactions concurrently (no await until Promise.all)
443+
const tx1Promise = sendTransactionWithoutWaiting(sender, startNonce, 1, gasPrice);
444+
const tx2Promise = sendTransactionWithoutWaiting(sender, startNonce + 1, 1, gasPrice);
445+
446+
// Wait for both to complete (lock service ensures they process sequentially internally)
447+
const [tx1Hashes, tx2Hashes] = await Promise.all([tx1Promise[0], tx2Promise[0]]);
448+
const tx1Hash = tx1Hashes;
449+
const tx2Hash = tx2Hashes;
450+
451+
// Both should succeed - no WRONG_NONCE errors
452+
expect(tx1Hash).to.exist;
453+
expect(tx2Hash).to.exist;
454+
455+
const receipts = await Promise.all([
456+
relay.pollForValidTransactionReceipt(tx1Hash),
457+
relay.pollForValidTransactionReceipt(tx2Hash),
458+
]);
459+
460+
expect(receipts[0].status).to.equal('0x1');
461+
expect(receipts[0].status).to.equal('0x1');
462+
});
463+
464+
it('should release lock and allow next transaction after gas price validation error', async function () {
465+
const sender = accounts[0];
466+
const startNonce = await relay.getAccountNonce(sender.address);
467+
const tooLowGasPrice = '0x0'; // Intentionally too low
468+
469+
// First tx with invalid gas price (will fail validation and not reach consensus)
470+
const invalidTx = {
471+
value: ONE_TINYBAR,
472+
chainId: Number(CHAIN_ID),
473+
maxPriorityFeePerGas: tooLowGasPrice,
474+
maxFeePerGas: tooLowGasPrice,
475+
gasLimit: numberTo0x(3_000_000),
476+
type: 2,
477+
to: accounts[2].address,
478+
nonce: startNonce,
479+
};
480+
const signedInvalidTx = await sender.wallet.signTransaction(invalidTx);
481+
482+
// Second tx with correct nonce (startNonce + 1), but will fail with WRONG_NONCE
483+
// because first tx never executed (account's actual nonce is still startNonce)
484+
const secondTx = {
485+
...defaultLondonTransactionData,
486+
to: accounts[2].address,
487+
value: ONE_TINYBAR,
488+
nonce: startNonce + 1, // This nonce is ahead of the account's actual nonce
489+
};
490+
const signedSecondTx = await sender.wallet.signTransaction(secondTx);
491+
492+
// Submit both transactions immediately to test lock release
493+
const invalidTxPromise = relay.call('eth_sendRawTransaction', [signedInvalidTx]).catch((error: any) => error);
494+
const secondTxPromise = relay.sendRawTransaction(signedSecondTx);
495+
496+
// Wait for both to complete
497+
const [invalidResult, txHash] = await Promise.all([invalidTxPromise, secondTxPromise]);
498+
// Verify first tx failed with validation error
499+
expect(invalidResult).to.be.instanceOf(Error);
500+
expect(invalidResult.message).to.include('gas price');
501+
502+
// Verify lock was released (second tx was allowed to proceed)
503+
expect(txHash).to.exist;
504+
505+
// Wait for second tx to be processed
506+
await new Promise((r) => setTimeout(r, 2100));
507+
508+
// Second tx should result in WRONG_NONCE (filtered out by mirror node)
509+
await expect(mirrorNode.get(`/contracts/results/${txHash}`)).to.eventually.be.rejected.and.satisfy(
510+
(error: any) => error.response.status === 404,
511+
);
512+
513+
// Verify account nonce hasn't changed (neither tx succeeded)
514+
const finalNonce = await relay.getAccountNonce(sender.address);
515+
expect(finalNonce).to.equal(startNonce);
516+
});
517+
});
518+
});
300519
});

0 commit comments

Comments
 (0)