Skip to content

Commit deacd76

Browse files
sameh-faroukclaude
andcommitted
test(bridge): add regression tests for expiry block reset (#1080)
Cover the scenario the existing suite missed: a first signature added when current_block - tx.block has again reached RetryInterval must reset tx.block so on_finalize (which runs after extrinsics in the same block) does not wipe the signature immediately. Both tests fail without the fix (tx.block is not reset) and pass with it: - burn_signature_after_expiry_resets_block_and_survives - refund_signature_after_expiry_resets_block_and_survives Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5fd89a9 commit deacd76

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

  • substrate-node/pallets/pallet-tft-bridge/src

substrate-node/pallets/pallet-tft-bridge/src/tests.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
499613
fn 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

Comments
 (0)