Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 84 additions & 6 deletions integration_tests/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Comment on lines +36 to +39
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_BLOCKHASH_AGE and SOL_RPC_SLOT_ROUNDING are duplicated test constants that must stay in sync with production code and the HTTP mock behavior (e.g., integration_tests/src/fixtures.rs hard-codes the / 20 * 20 rounding in get_block_request). To avoid drift, consider defining these values in one place (e.g., export a constant from the SOL-RPC fixture/helpers for the rounding, and/or expose MAX_BLOCKHASH_AGE from the minter monitor module for tests) and referencing that here instead of re-stating the numbers.

Suggested change
/// 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;
/// Mirrors the SOL RPC mock behavior in `integration_tests/src/fixtures.rs`, which rounds
/// the slot returned by `getSlot` down to the nearest multiple of 20 before issuing `getBlock`.
fn round_sol_rpc_slot(slot: Slot) -> Slot {
(slot / 20) * 20
}

Copilot uses AI. Check for mistakes.
/// Deposits funds into the minter via `process_deposit`, consolidates them,
/// and finalizes the consolidation so the minter's internal balance is credited.
///
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1065,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;
Expand Down Expand Up @@ -1096,16 +1099,91 @@ mod consolidation_tests {
setup.drop().await;
}

#[tokio::test]
async fn should_resubmit_expired_consolidation_transaction() {
let setup = SetupBuilder::new().with_proxy_canister().build().await;

Comment thread
lpahlavi marked this conversation as resolved.
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,
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.
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,
// 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
)));
Comment on lines +1146 to +1152
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test only asserts new_slot is past the expiry threshold, but it doesn't verify that the resubmission actually produced a replacement transaction for the original consolidation (e.g., that ResubmittedTransaction.old_signature matches the original SubmittedTransaction.signature, and that new_signature != old_signature). Capturing the original signature from the SubmittedTransaction event and asserting it is the one resubmitted (with a different new signature) would make this test reliably detect regressions where the blockhash/signature is not actually refreshed.

Copilot uses AI. Check for mistakes.
});

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)
.submit_transaction(
100_000_000,
INITIAL_SLOT,
"4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZAMdL4VZHirAn",
"5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW",
)
.build()
Comment thread
lpahlavi marked this conversation as resolved.
}

/// 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 {
Expand Down
Loading