@@ -9,6 +9,7 @@ import Constants from '@hashgraph/json-rpc-relay/dist/lib/constants';
99// Errors and constants from local resources
1010import { predefined } from '@hashgraph/json-rpc-relay/dist/lib/errors/JsonRpcError' ;
1111import { RequestDetails } from '@hashgraph/json-rpc-relay/dist/lib/types' ;
12+ import { overrideEnvsInMochaDescribe , withOverriddenEnvsInMochaTest } from '@hashgraph/json-rpc-relay/tests/helpers' ;
1213import { expect } from 'chai' ;
1314
1415import 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