From 14599f671d96a542e5885ecad39134be8fd9aa54 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 17 Apr 2026 15:44:41 +0200 Subject: [PATCH 1/2] test: add integration test for consolidation transaction resubmission Adds `should_resubmit_expired_consolidation_transaction` which verifies that the monitor resubmits a consolidation transaction whose blockhash has expired (slot > original_slot + MAX_BLOCKHASH_AGE). Also moves MAX_BLOCKHASH_AGE and SOL_RPC_SLOT_ROUNDING to file scope so they can be shared between the withdrawal and consolidation test modules. Co-Authored-By: Claude Sonnet 4.6 --- integration_tests/tests/tests.rs | 85 ++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 94f48b8c..5821fdf7 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -31,6 +31,12 @@ const FINALIZE_TRANSACTIONS_DELAY: Duration = Duration::from_mins(2); const RESUBMIT_TRANSACTIONS_DELAY: Duration = Duration::from_mins(3); const DEPOSIT_CONSOLIDATION_DELAY: Duration = Duration::from_mins(10); +/// Must match `MAX_BLOCKHASH_AGE` in the minter's monitor module. +const MAX_BLOCKHASH_AGE: Slot = 150; +/// The SOL RPC canister rounds the slot returned by getSlot down to the nearest multiple +/// of this value before querying getBlock and returning the slot to callers. +const SOL_RPC_SLOT_ROUNDING: u64 = 20; + /// Deposits funds into the minter via `process_deposit`, consolidates them, /// and finalizes the consolidation so the minter's internal balance is credited. /// @@ -262,11 +268,6 @@ mod withdrawal_tests { use super::*; - const MAX_BLOCKHASH_AGE: Slot = 150; - /// The SOL RPC canister rounds the slot returned by getSlot down to the nearest multiple - /// of this value before querying getBlock and returning the slot to callers. - const SOL_RPC_SLOT_ROUNDING: u64 = 20; - #[tokio::test] async fn should_validate_solana_address() { let setup = SetupBuilder::new().build().await; @@ -1096,6 +1097,61 @@ mod consolidation_tests { setup.drop().await; } + #[tokio::test] + async fn should_resubmit_expired_consolidation_transaction() { + let setup = SetupBuilder::new().with_proxy_canister().build().await; + + let result = setup + .minter() + .with_http_mocks(MockBuilder::new().get_deposit_transaction().build()) + .process_deposit(default_process_deposit_args()) + .await; + let mint_block_index = + assert_matches!(result, Ok(DepositStatus::Minted { block_index, .. }) => block_index); + + // Advance time past the consolidation delay to trigger the timer + setup.advance_time(DEPOSIT_CONSOLIDATION_DELAY).await; + setup + .execute_http_mocks(http_mocks_for_deposit_consolidation()) + .await; + + setup.minter().assert_that_events().await.satisfy(|events| { + check!(events.iter().any(|e| matches!( + &e.payload, + EventType::SubmittedTransaction { + purpose: TransactionPurpose::ConsolidateDeposits { mint_indices }, + .. + } if mint_indices == &[mint_block_index] + ))); + }); + + // Advance time to trigger finalize_transactions. The mocked slot exceeds + // INITIAL_SLOT + MAX_BLOCKHASH_AGE + SOL_RPC_SLOT_ROUNDING, so the + // consolidation transaction is considered expired. + const INITIAL_SLOT: Slot = 100_000_000; + let resubmission_slot = INITIAL_SLOT + MAX_BLOCKHASH_AGE + SOL_RPC_SLOT_ROUNDING + 1; + setup.advance_time(FINALIZE_TRANSACTIONS_DELAY).await; + setup + .execute_http_mocks(mark_expired_consolidation_http_mocks(resubmission_slot)) + .await; + + // Advance time to trigger resubmit_transactions + setup.advance_time(RESUBMIT_TRANSACTIONS_DELAY).await; + setup + .execute_http_mocks(resubmit_consolidation_http_mocks(resubmission_slot)) + .await; + + setup.minter().assert_that_events().await.satisfy(|events| { + check!( + events + .iter() + .any(|e| matches!(&e.payload, EventType::ResubmittedTransaction { .. })) + ); + }); + + setup.drop().await; + } + // Returns the required HTTP outcall mocks for executing the deposit consolidation task fn http_mocks_for_deposit_consolidation() -> MockHttpOutcalls { MockBuilder::with_start_id(4) @@ -1106,6 +1162,25 @@ mod consolidation_tests { ) .build() } + + /// HTTP mocks for finalize_transactions detecting an expired consolidation transaction. + fn mark_expired_consolidation_http_mocks(current_slot: Slot) -> MockHttpOutcalls { + MockBuilder::with_start_id(16) + .get_current_slot(current_slot, "9ZNTfG4NyQgxy2SWjSiQoUyBPEvXT2xo7fKc5hPYYJ7b") + .check_signature_statuses_not_found(1) + .build() + } + + /// HTTP mocks for resubmit_transactions sending the replacement consolidation transaction. + fn resubmit_consolidation_http_mocks(current_slot: Slot) -> MockHttpOutcalls { + MockBuilder::with_start_id(28) + .submit_transaction( + current_slot, + "9ZNTfG4NyQgxy2SWjSiQoUyBPEvXT2xo7fKc5hPYYJ7b", + "2gQDVht4vqs8FeKnbGCtXjCXjbTwRnKJNzuYfDFXhkWBn5MhZKXaKMDJSzaq4G7FnNmah7SWj4TX2mB3bo7NQGnm", + ) + .build() + } } mod metrics_tests { From ac25895c92377305132780464da65f13b370a9d3 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 17 Apr 2026 16:24:17 +0200 Subject: [PATCH 2/2] fixup! test: add integration test for consolidation transaction resubmission Address Copilot review comments: - Fix e.payload -> e in assert_that_events (events are EventType directly) - Move INITIAL_SLOT to module scope and use it in http_mocks_for_deposit_consolidation - Strengthen ResubmittedTransaction assertion with new_slot >= expiry threshold Co-Authored-By: Claude Sonnet 4.6 --- integration_tests/tests/tests.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/integration_tests/tests/tests.rs b/integration_tests/tests/tests.rs index 5821fdf7..2705ecab 100644 --- a/integration_tests/tests/tests.rs +++ b/integration_tests/tests/tests.rs @@ -1066,6 +1066,8 @@ mod anonymous_caller_tests { mod consolidation_tests { use super::*; + const INITIAL_SLOT: Slot = 100_000_000; + #[tokio::test] async fn should_consolidate_deposits_after_timer() { let setup = SetupBuilder::new().with_proxy_canister().build().await; @@ -1117,7 +1119,7 @@ mod consolidation_tests { setup.minter().assert_that_events().await.satisfy(|events| { check!(events.iter().any(|e| matches!( - &e.payload, + e, EventType::SubmittedTransaction { purpose: TransactionPurpose::ConsolidateDeposits { mint_indices }, .. @@ -1128,7 +1130,6 @@ mod consolidation_tests { // Advance time to trigger finalize_transactions. The mocked slot exceeds // INITIAL_SLOT + MAX_BLOCKHASH_AGE + SOL_RPC_SLOT_ROUNDING, so the // consolidation transaction is considered expired. - const INITIAL_SLOT: Slot = 100_000_000; let resubmission_slot = INITIAL_SLOT + MAX_BLOCKHASH_AGE + SOL_RPC_SLOT_ROUNDING + 1; setup.advance_time(FINALIZE_TRANSACTIONS_DELAY).await; setup @@ -1142,11 +1143,13 @@ mod consolidation_tests { .await; setup.minter().assert_that_events().await.satisfy(|events| { - check!( - events - .iter() - .any(|e| matches!(&e.payload, EventType::ResubmittedTransaction { .. })) - ); + check!(events.iter().any(|e| matches!( + e, + // new_slot must be past the original blockhash expiry threshold, + // confirming the resubmitted transaction uses a fresh blockhash. + EventType::ResubmittedTransaction { new_slot, .. } + if *new_slot >= INITIAL_SLOT + MAX_BLOCKHASH_AGE + ))); }); setup.drop().await; @@ -1156,7 +1159,7 @@ mod consolidation_tests { fn http_mocks_for_deposit_consolidation() -> MockHttpOutcalls { MockBuilder::with_start_id(4) .submit_transaction( - 100_000_000, + INITIAL_SLOT, "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZAMdL4VZHirAn", "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW", )