Skip to content
9 changes: 9 additions & 0 deletions integration_tests/src/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ impl MockBuilder {
self.get_transaction(get_deposit_transaction_response())
}

/// Mocks for `getSlot` → `getBlock`.
pub fn get_slot_and_block(self, slot: u64, blockhash: &str) -> Self {
self.expect(get_slot_request(), get_slot_response(slot))
.expect(get_block_request(slot), get_block_response(blockhash))
}

/// Mocks for `getSlot` → `getBlock` → `sendTransaction`.
pub fn submit_transaction(self, slot: u64, blockhash: &str, tx_signature: &str) -> Self {
self.expect(get_slot_request(), get_slot_response(slot))
Expand Down Expand Up @@ -279,6 +285,9 @@ fn get_slot_response(slot: u64) -> JsonRpcResponse {
}

fn get_block_request(slot: u64) -> JsonRpcRequestMatcher {
// The SOL RPC canister rounds the slot down to the nearest multiple of 20
// before making getBlock requests, so we match that behavior here.
let slot = slot / 20 * 20;
JsonRpcRequestMatcher::with_method("getBlock").with_params(json!([
slot,
{
Expand Down
67 changes: 44 additions & 23 deletions integration_tests/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ use sol_rpc_types::{CommitmentLevel, ConsensusStrategy, GetTransactionEncoding,
use std::time::Duration;
use tokio::join;

const WITHDRAWAL_PROCESSING_DELAY: Duration = Duration::from_mins(1);
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);

/// Deposits funds into the minter via `update_balance`, consolidates them,
/// and finalizes the consolidation so the minter's internal balance is credited.
///
Expand All @@ -39,7 +44,7 @@ async fn deposit_and_consolidate_funds(setup: &Setup) {
assert_matches!(result, Ok(DepositStatus::Minted { .. }));

// Consolidate
setup.advance_time(Duration::from_mins(10)).await;
setup.advance_time(DEPOSIT_CONSOLIDATION_DELAY).await;
setup
.execute_http_mocks(
MockBuilder::with_start_id(4)
Expand All @@ -53,7 +58,7 @@ async fn deposit_and_consolidate_funds(setup: &Setup) {
.await;

// Finalize
setup.advance_time(Duration::from_secs(60)).await;
setup.advance_time(FINALIZE_TRANSACTIONS_DELAY).await;
setup
.execute_http_mocks(
MockBuilder::with_start_id(16)
Expand Down Expand Up @@ -255,8 +260,10 @@ mod withdrawal_tests {

use super::*;

const WITHDRAWAL_PROCESSING_DELAY: Duration = Duration::from_mins(1);
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() {
Expand Down Expand Up @@ -663,15 +670,22 @@ mod withdrawal_tests {
other => panic!("Expected TxSent, got: {other:?}"),
};

// Advance time to trigger resubmission. The mocked slot exceeds
// INITIAL_SLOT + MAX_PROCESSING_AGE, so the original transaction
// is now considered expired.
const MONITOR_DELAY: Duration = Duration::from_secs(60);
setup.advance_time(MONITOR_DELAY).await;
// Advance time to trigger finalize_transactions, which fetches the current slot,
// checks statuses (not found), and marks the expired transaction for resubmission.
// The SOL RPC canister rounds the slot down to SOL_RPC_SLOT_ROUNDING before returning
// it, so we add SOL_RPC_SLOT_ROUNDING + 1 to ensure the rounded slot is strictly
// greater than INITIAL_SLOT + MAX_BLOCKHASH_AGE (the expiry threshold).
let resubmission_slot = INITIAL_SLOT + MAX_BLOCKHASH_AGE + SOL_RPC_SLOT_ROUNDING + 1;
setup.advance_time(FINALIZE_TRANSACTIONS_DELAY).await;
setup
.execute_http_mocks(resubmit_withdrawal_http_mocks(
INITIAL_SLOT + MAX_BLOCKHASH_AGE + 50,
))
.execute_http_mocks(mark_expired_withdrawal_http_mocks(resubmission_slot))
.await;

// Advance time to trigger resubmit_transactions. finalize_transactions also
// fires but has no pending transactions, so it makes no HTTP outcalls.
setup.advance_time(RESUBMIT_TRANSACTIONS_DELAY).await;
setup
.execute_http_mocks(resubmit_withdrawal_http_mocks(resubmission_slot))
.await;

// Withdrawal status should now have a different signature
Expand All @@ -687,11 +701,11 @@ mod withdrawal_tests {
other => panic!("Expected TxSent after resubmission, got: {other:?}"),
};

// Advance time to trigger finalization. The monitor checks signature statuses
// and this time the transaction is reported as finalized.
setup.advance_time(MONITOR_DELAY).await;
// Advance time to trigger finalize_transactions again. This time the
// transaction is reported as finalized.
setup.advance_time(FINALIZE_TRANSACTIONS_DELAY).await;
setup
.execute_http_mocks(finalize_withdrawal_http_mocks())
.execute_http_mocks(finalize_withdrawal_http_mocks(resubmission_slot))
.await;

// Withdrawal status should now be TxFinalized with Success
Expand Down Expand Up @@ -721,21 +735,30 @@ mod withdrawal_tests {
.build()
}

/// HTTP mocks for resubmitting an expired withdrawal transaction.
fn resubmit_withdrawal_http_mocks(current_slot: u64) -> MockHttpOutcalls {
/// HTTP mocks for finalize_transactions detecting an expired transaction:
/// fetches slot, checks status (not found), marks for resubmission.
fn mark_expired_withdrawal_http_mocks(current_slot: u64) -> MockHttpOutcalls {
MockBuilder::with_start_id(40)
.resubmit_transaction(
.get_current_slot(current_slot, "9ZNTfG4NyQgxy2SWjSiQoUyBPEvXT2xo7fKc5hPYYJ7b")
.check_signature_statuses_not_found(1)
.build()
}

/// HTTP mocks for resubmit_transactions sending the replacement transaction.
fn resubmit_withdrawal_http_mocks(current_slot: u64) -> MockHttpOutcalls {
MockBuilder::with_start_id(52)
.submit_transaction(
current_slot,
"9ZNTfG4NyQgxy2SWjSiQoUyBPEvXT2xo7fKc5hPYYJ7b",
"5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW",
)
.build()
}

/// HTTP mocks for finalizing a withdrawal transaction.
fn finalize_withdrawal_http_mocks() -> MockHttpOutcalls {
/// HTTP mocks for finalize_transactions confirming the resubmitted transaction.
fn finalize_withdrawal_http_mocks(current_slot: u64) -> MockHttpOutcalls {
MockBuilder::with_start_id(64)
.get_current_slot(350_000_200, "9ZNTfG4NyQgxy2SWjSiQoUyBPEvXT2xo7fKc5hPYYJ7b")
.get_current_slot(current_slot, "9ZNTfG4NyQgxy2SWjSiQoUyBPEvXT2xo7fKc5hPYYJ7b")
.check_signature_statuses_finalized(1)
.build()
}
Expand Down Expand Up @@ -1040,8 +1063,6 @@ mod anonymous_caller_tests {
mod consolidation_tests {
use super::*;

const DEPOSIT_CONSOLIDATION_DELAY: Duration = Duration::from_mins(10);

#[tokio::test]
async fn should_consolidate_deposits_after_timer() {
let setup = SetupBuilder::new().with_proxy_canister().build().await;
Expand Down
4 changes: 4 additions & 0 deletions minter/src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/// Maximum number of concurrent calls to the SOL RPC canister.
pub const MAX_CONCURRENT_RPC_CALLS: usize = 10;

/// Maximum number of rounds per timer invocation.
/// Each round issues up to [`MAX_CONCURRENT_RPC_CALLS`] parallel RPC calls.
pub const MAX_TIMER_ROUNDS: usize = 5;

/// Matches the ICP HTTPS outcall response limit for variable-length RPC calls
/// such as `getTransaction` and `getSignatureStatuses`:
/// https://docs.internetcomputer.org/references/ic-interface-spec#ic-http_request
Expand Down
16 changes: 12 additions & 4 deletions minter/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use candid::Principal;
use canlog::{Log, Sort};
use cksol_minter::consolidate::{DEPOSIT_CONSOLIDATION_DELAY, consolidate_deposits};
use cksol_minter::monitor::{MONITOR_SUBMITTED_TRANSACTIONS_DELAY, monitor_submitted_transactions};
use cksol_minter::withdraw::{WITHDRAWAL_PROCESSING_DELAY, process_pending_withdrawals};
use cksol_minter::{
address::lazy_get_schnorr_master_key, runtime::IcCanisterRuntime, state::read_state,
};
use cksol_minter::{
consolidate::{DEPOSIT_CONSOLIDATION_DELAY, consolidate_deposits},
monitor::{
FINALIZE_TRANSACTIONS_DELAY, RESUBMIT_TRANSACTIONS_DELAY, finalize_transactions,
resubmit_transactions,
},
};
use cksol_types::{
Address, DepositStatus, GetDepositAddressArgs, MinterInfo, UpdateBalanceArgs,
UpdateBalanceError, WithdrawalArgs, WithdrawalError, WithdrawalOk, WithdrawalStatus,
Expand Down Expand Up @@ -335,8 +340,11 @@ fn setup_timers() {
ic_cdk_timers::set_timer_interval(WITHDRAWAL_PROCESSING_DELAY, async || {
process_pending_withdrawals(&IcCanisterRuntime::new()).await;
});
ic_cdk_timers::set_timer_interval(MONITOR_SUBMITTED_TRANSACTIONS_DELAY, async || {
monitor_submitted_transactions(IcCanisterRuntime::new()).await;
ic_cdk_timers::set_timer_interval(FINALIZE_TRANSACTIONS_DELAY, async || {
finalize_transactions(IcCanisterRuntime::new()).await;
});
ic_cdk_timers::set_timer_interval(RESUBMIT_TRANSACTIONS_DELAY, async || {
resubmit_transactions(IcCanisterRuntime::new()).await;
});
}

Expand Down
Loading
Loading