@@ -496,6 +496,120 @@ fn burn_fails_if_less_than_withdraw_fee_amount() {
496496 } ) ;
497497}
498498
499+ #[ test]
500+ // Regression test for #1080: a signature added >= RetryInterval blocks after the
501+ // last block-reset must NOT be immediately wiped by on_finalize in the same block.
502+ // Before the fix, add_stellar_sig_* never reset tx.block, so on_finalize (which runs
503+ // after extrinsics in the same block) saw current_block - tx.block >= RetryInterval
504+ // and cleared the just-added signature.
505+ fn burn_signature_after_expiry_resets_block_and_survives ( ) {
506+ new_test_ext ( ) . execute_with ( || {
507+ prepare_validators ( ) ;
508+ run_to_block ( 1 ) ;
509+
510+ // Create a burn at block 1 (tx.block = 1, no signatures yet).
511+ assert_ok ! ( TFTBridgeModule :: swap_to_stellar(
512+ RuntimeOrigin :: signed( alice( ) ) ,
513+ b"GBIYYEQO73AYJEADTHMTF5M42WICTHU55IIT2CPEZBBLLDSJ322OGW7Z" . to_vec( ) ,
514+ 750000000
515+ ) ) ;
516+
517+ // Advance to exactly RetryInterval (20) blocks after creation WITHOUT triggering
518+ // an auto-expiry: at block 20 the gap is 19 (< 20), so on_finalize does not fire.
519+ // Now at block 21, tx.block is still 1 and the gap is exactly 20.
520+ run_to_block ( 21 ) ;
521+ let burn_tx = TFTBridgeModule :: burn_transactions ( 1 ) . unwrap ( ) ;
522+ assert_eq ! ( burn_tx. block, 1 , "precondition: block not yet reset" ) ;
523+ assert_eq ! ( burn_tx. signatures. len( ) , 0 , "precondition: no signatures" ) ;
524+
525+ // First signature arrives when current_block - tx.block == RetryInterval.
526+ assert_ok ! ( TFTBridgeModule :: propose_burn_transaction_or_add_sig(
527+ RuntimeOrigin :: signed( alice( ) ) ,
528+ 1 ,
529+ b"GBIYYEQO73AYJEADTHMTF5M42WICTHU55IIT2CPEZBBLLDSJ322OGW7Z" . to_vec( ) ,
530+ 250000000 ,
531+ b"alice_sig" . to_vec( ) ,
532+ b"alice_stellar_pubkey" . to_vec( ) ,
533+ 1
534+ ) ) ;
535+
536+ // The fix resets tx.block to the current block on the first signature.
537+ let burn_tx = TFTBridgeModule :: burn_transactions ( 1 ) . unwrap ( ) ;
538+ assert_eq ! ( burn_tx. block, 21 , "tx.block must be reset to the signing block" ) ;
539+ assert_eq ! ( burn_tx. signatures. len( ) , 1 ) ;
540+
541+ // Run on_finalize for block 21 (advancing to 22). Without the reset this would
542+ // see 21 - 1 == 20 >= RetryInterval and clear the signature in the same block.
543+ run_to_block ( 22 ) ;
544+ let burn_tx = TFTBridgeModule :: burn_transactions ( 1 ) . unwrap ( ) ;
545+ assert_eq ! (
546+ burn_tx. signatures. len( ) ,
547+ 1 ,
548+ "signature must survive on_finalize, not be immediately re-expired"
549+ ) ;
550+ } ) ;
551+ }
552+
553+ #[ test]
554+ // Regression test for #1080 (refund path): mirrors the burn case for
555+ // add_stellar_sig_refund_transaction. After an expiry clears the signatures, the
556+ // first re-signature must reset tx.block so on_finalize does not wipe it the same block.
557+ fn refund_signature_after_expiry_resets_block_and_survives ( ) {
558+ new_test_ext ( ) . execute_with ( || {
559+ prepare_validators ( ) ;
560+ run_to_block ( 1 ) ;
561+
562+ let tx_hash = b"some_refund_tx_hash" . to_vec ( ) ;
563+ let target = b"GBIYYEQO73AYJEADTHMTF5M42WICTHU55IIT2CPEZBBLLDSJ322OGW7Z" . to_vec ( ) ;
564+
565+ // Create a refund with a first signature at block 1 (tx.block = 1).
566+ assert_ok ! ( TFTBridgeModule :: create_refund_transaction_or_add_sig(
567+ RuntimeOrigin :: signed( alice( ) ) ,
568+ tx_hash. clone( ) ,
569+ target. clone( ) ,
570+ 250000000 ,
571+ b"alice_sig" . to_vec( ) ,
572+ b"alice_stellar_pubkey" . to_vec( ) ,
573+ 1
574+ ) ) ;
575+
576+ // Let it expire: on_finalize at block 21 (21 - 1 == 20) clears signatures and
577+ // sets tx.block = 21.
578+ run_to_block ( 22 ) ;
579+ let refund_tx = TFTBridgeModule :: refund_transactions ( & tx_hash) ;
580+ assert_eq ! ( refund_tx. block, 21 , "precondition: expiry reset block to 21" ) ;
581+ assert_eq ! ( refund_tx. signatures. len( ) , 0 , "precondition: signatures cleared" ) ;
582+
583+ // Advance to where current_block - tx.block == 20 again, without triggering
584+ // another auto-expiry (block 40 gap is 19). Now at block 41 the gap is exactly 20.
585+ run_to_block ( 41 ) ;
586+
587+ // First signature after expiry arrives at the expiry boundary.
588+ assert_ok ! ( TFTBridgeModule :: create_refund_transaction_or_add_sig(
589+ RuntimeOrigin :: signed( alice( ) ) ,
590+ tx_hash. clone( ) ,
591+ target. clone( ) ,
592+ 250000000 ,
593+ b"alice_sig" . to_vec( ) ,
594+ b"alice_stellar_pubkey" . to_vec( ) ,
595+ 1
596+ ) ) ;
597+
598+ let refund_tx = TFTBridgeModule :: refund_transactions ( & tx_hash) ;
599+ assert_eq ! ( refund_tx. block, 41 , "tx.block must be reset to the signing block" ) ;
600+ assert_eq ! ( refund_tx. signatures. len( ) , 1 ) ;
601+
602+ // on_finalize for block 41 must not immediately re-expire the signature.
603+ run_to_block ( 42 ) ;
604+ let refund_tx = TFTBridgeModule :: refund_transactions ( & tx_hash) ;
605+ assert_eq ! (
606+ refund_tx. signatures. len( ) ,
607+ 1 ,
608+ "signature must survive on_finalize, not be immediately re-expired"
609+ ) ;
610+ } ) ;
611+ }
612+
499613fn prepare_validators ( ) {
500614 TFTBridgeModule :: add_bridge_validator ( RawOrigin :: Root . into ( ) , alice ( ) ) . unwrap ( ) ;
501615 TFTBridgeModule :: add_bridge_validator ( RawOrigin :: Root . into ( ) , bob ( ) ) . unwrap ( ) ;
0 commit comments