@@ -371,7 +371,9 @@ mod tests {
371371 use katana_primitives:: chain:: ChainId ;
372372 use katana_primitives:: contract:: { ContractAddress , Nonce } ;
373373 use katana_primitives:: env:: BlockEnv ;
374- use katana_primitives:: transaction:: { ExecutableTx , ExecutableTxWithHash , InvokeTx , InvokeTxV1 } ;
374+ use katana_primitives:: transaction:: {
375+ ExecutableTx , ExecutableTxWithHash , InvokeTx , InvokeTxV1 ,
376+ } ;
375377 use katana_primitives:: Felt ;
376378 use katana_provider:: api:: block:: BlockWriter ;
377379 use katana_provider:: api:: state:: { StateFactoryProvider , StateProvider } ;
@@ -389,9 +391,7 @@ mod tests {
389391 status : FinalityStatus :: AcceptedOnL2 ,
390392 block : Block :: default ( ) . seal_with_hash ( Felt :: ZERO ) ,
391393 } ;
392- provider_mut
393- . insert_block_with_states_and_receipts ( block, states, vec ! [ ] , vec ! [ ] )
394- . unwrap ( ) ;
394+ provider_mut. insert_block_with_states_and_receipts ( block, states, vec ! [ ] , vec ! [ ] ) . unwrap ( ) ;
395395 provider_mut. commit ( ) . unwrap ( ) ;
396396 provider_factory. provider ( ) . latest ( ) . unwrap ( )
397397 }
@@ -442,8 +442,7 @@ mod tests {
442442 let sender = * chain_spec. genesis ( ) . accounts ( ) . next ( ) . unwrap ( ) . 0 ;
443443
444444 let state = create_test_state ( & chain_spec) ;
445- let execution_flags =
446- ExecutionFlags :: new ( ) . with_account_validation ( false ) . with_fee ( false ) ;
445+ let execution_flags = ExecutionFlags :: new ( ) . with_account_validation ( false ) . with_fee ( false ) ;
447446 let block_env = BlockEnv :: default ( ) ;
448447 let permit = Arc :: new ( Mutex :: new ( ( ) ) ) ;
449448
@@ -487,8 +486,155 @@ mod tests {
487486
488487 assert ! (
489488 matches!( result, Ok ( ValidationOutcome :: Dependent { .. } ) ) ,
490- "After update(), tx with nonce 3 should be Dependent (state nonce is 0), \
491- but stale pool_nonces caused it to be accepted as Valid. Got: {result:?}"
489+ "After update(), tx with nonce 3 should be Dependent (state nonce is 0), but stale \
490+ pool_nonces caused it to be accepted as Valid. Got: {result:?}"
492491 ) ;
493492 }
493+
494+ /// End-to-end test reproducing the production error through the full
495+ /// pool → executor pipeline, mirroring the actual node setup.
496+ ///
497+ /// When pool_nonces drifts ahead of the actual state nonce (the bug this
498+ /// fix addresses), a transaction whose nonce matches the drifted value
499+ /// passes the pool validator's checks and enters the pool. The block
500+ /// producer then picks it up from the pool and feeds it to the executor.
501+ /// The executor uses strict nonce checking and rejects it with:
502+ ///
503+ /// "Invalid transaction nonce of contract at address 0x...
504+ /// Account nonce: 0x0; got: 0x3."
505+ ///
506+ /// This is the same error observed in production:
507+ /// "Invalid transaction nonce of contract at address 0x4250...bccf.
508+ /// Account nonce: 0x27ed; got: 0x2bbf."
509+ ///
510+ /// The test exercises the full flow:
511+ /// 1. Tx submitted to pool via `add_transaction` (goes through TxValidator)
512+ /// 2. Tx picked up from pool via `pending_transactions` (like block producer)
513+ /// 3. Tx executed by blockifier executor
514+ /// 4. Executor rejects with InvalidNonce
515+ #[ tokio:: test]
516+ async fn pool_to_executor_nonce_drift_produces_invalid_nonce_error ( ) {
517+ use futures:: StreamExt ;
518+ use katana_executor:: blockifier:: BlockifierFactory ;
519+ use katana_executor:: { ExecutionResult , ExecutorFactory } ;
520+ use katana_pool_api:: TransactionPool ;
521+ use katana_primitives:: env:: VersionedConstantsOverrides ;
522+
523+ use crate :: ordering:: FiFo ;
524+ use crate :: pool:: Pool ;
525+
526+ let _ = ClassCacheBuilder :: new ( ) . build_global ( ) ;
527+
528+ let chain_spec = Arc :: new ( ChainSpec :: dev ( ) ) ;
529+ let chain_id = chain_spec. id ( ) ;
530+ let sender = * chain_spec. genesis ( ) . accounts ( ) . next ( ) . unwrap ( ) . 0 ;
531+
532+ // -- Set up pool with TxValidator (same as real node) --
533+ let state = create_test_state ( & chain_spec) ;
534+ let execution_flags = ExecutionFlags :: new ( ) . with_account_validation ( false ) . with_fee ( false ) ;
535+ let block_env = BlockEnv :: default ( ) ;
536+ let permit = Arc :: new ( Mutex :: new ( ( ) ) ) ;
537+
538+ let validator = TxValidator :: new (
539+ state,
540+ execution_flags. clone ( ) ,
541+ None ,
542+ block_env. clone ( ) ,
543+ permit,
544+ chain_spec. clone ( ) ,
545+ ) ;
546+
547+ let pool = Pool :: new ( validator. clone ( ) , FiFo :: new ( ) ) ;
548+
549+ // -- Set up executor factory (same config as block producer) --
550+ let executor_factory = BlockifierFactory :: new (
551+ Some ( VersionedConstantsOverrides {
552+ validate_max_n_steps : Some ( u32:: MAX ) ,
553+ invoke_tx_max_n_steps : Some ( u32:: MAX ) ,
554+ max_recursion_depth : Some ( usize:: MAX ) ,
555+ } ) ,
556+ // Executor uses default flags: strict nonce check enabled
557+ execution_flags,
558+ katana_executor:: BlockLimits :: default ( ) ,
559+ katana_executor:: blockifier:: cache:: ClassCache :: global ( ) ,
560+ chain_spec. clone ( ) ,
561+ ) ;
562+
563+ // -- Simulate pool_nonces drift (pre-fix bug) --
564+ //
565+ // In production, pool_nonces drifts because update() didn't clear it.
566+ // After many blocks, pool_nonces[sender] could be far ahead of state.
567+ // We simulate this by directly setting pool_nonces to a drifted value.
568+ let drifted_nonce = Felt :: THREE ;
569+ {
570+ let mut inner = validator. inner . lock ( ) ;
571+ inner. pool_nonces . insert ( sender, drifted_nonce) ;
572+ }
573+
574+ // -- Step 1: Submit tx to pool via add_transaction --
575+ //
576+ // The validator sees pool_nonces[sender] = 3, tx_nonce = 3.
577+ // Since tx_nonce == current_nonce, it falls through to blockifier.
578+ // Blockifier (non-strict in validator): state_nonce(0) <= tx_nonce(3) → passes.
579+ // Tx enters the pool as Valid.
580+ let tx = create_invoke_tx ( sender, chain_id, drifted_nonce) ;
581+ let tx_hash = tx. hash ;
582+ pool. add_transaction ( tx) . await . expect ( "tx should pass validation and enter pool" ) ;
583+
584+ assert ! ( pool. contains( tx_hash) , "tx should be in the pool" ) ;
585+
586+ // -- Step 2: Pull tx from pool (like block producer does) --
587+ let mut pending = pool. pending_transactions ( ) ;
588+ let pending_tx = pending. next ( ) . await . expect ( "should have a pending tx" ) ;
589+ let picked_tx = pending_tx. tx . as_ref ( ) . clone ( ) ;
590+ assert_eq ! ( picked_tx. hash, tx_hash) ;
591+
592+ // -- Step 3: Execute with executor (like block producer does) --
593+ //
594+ // Executor is created with fresh state (nonce = 0) and strict nonce check.
595+ // This is what happens in the real node: the executor's state reflects
596+ // committed blocks, not the pool's speculative nonce tracking.
597+ let executor_state = create_test_state ( & chain_spec) ;
598+ let mut executor = executor_factory. executor ( executor_state, block_env) ;
599+
600+ let ( executed, _) = executor. execute_transactions ( vec ! [ picked_tx] ) . unwrap ( ) ;
601+ assert_eq ! ( executed, 1 , "executor should process the tx (even if it fails)" ) ;
602+
603+ // -- Step 4: Verify the exact production error --
604+ let ( _, result) = & executor. transactions ( ) [ 0 ] ;
605+ match result {
606+ ExecutionResult :: Failed { error } => {
607+ let error_msg = error. to_string ( ) ;
608+ // This is the exact error format from production logs:
609+ // "Invalid transaction nonce of contract at address {addr}.
610+ // Account nonce: {current_nonce:#x}; got: {tx_nonce:#x}."
611+ assert ! (
612+ error_msg. contains( "Invalid transaction nonce" ) ,
613+ "Expected InvalidNonce error, got: {error_msg}"
614+ ) ;
615+ assert ! (
616+ error_msg. contains( & format!( "{sender}" ) ) ,
617+ "Error should reference the sender address, got: {error_msg}"
618+ ) ;
619+ assert ! (
620+ error_msg. contains( "Account nonce: 0x0" ) ,
621+ "Error should show state nonce 0x0, got: {error_msg}"
622+ ) ;
623+ assert ! (
624+ error_msg. contains( "got: 0x3" ) ,
625+ "Error should show tx nonce 0x3, got: {error_msg}"
626+ ) ;
627+ }
628+ ExecutionResult :: Success { .. } => {
629+ panic ! (
630+ "Executor should reject tx with nonce 3 when state nonce is 0, but it \
631+ succeeded"
632+ ) ;
633+ }
634+ }
635+
636+ // -- Cleanup: remove from pool (like block producer does post-execution) --
637+ pool. remove_transactions ( & [ tx_hash] ) ;
638+ assert ! ( !pool. contains( tx_hash) ) ;
639+ }
494640}
0 commit comments