diff --git a/rs/dogecoin/ckdoge/minter/tests/tests.rs b/rs/dogecoin/ckdoge/minter/tests/tests.rs index 7fae5bb01b59..9560ce702c44 100644 --- a/rs/dogecoin/ckdoge/minter/tests/tests.rs +++ b/rs/dogecoin/ckdoge/minter/tests/tests.rs @@ -383,6 +383,7 @@ mod withdrawal { setup .withdrawal_flow() + .minter_await_fee_refresh() .ledger_approve_minter(account, RETRIEVE_DOGE_MIN_AMOUNT) .minter_retrieve_doge_with_approval( RETRIEVE_DOGE_MIN_AMOUNT, @@ -415,6 +416,7 @@ mod withdrawal { let withdrawal_flow = setup .withdrawal_flow() + .minter_await_fee_refresh() .ledger_approve_minter(account, RETRIEVE_DOGE_MIN_AMOUNT); setup.ledger().stop(); @@ -454,6 +456,7 @@ mod withdrawal { setup .withdrawal_flow() + .minter_await_fee_refresh() .ledger_approve_minter(account, RETRIEVE_DOGE_MIN_AMOUNT) .minter_retrieve_doge_with_approval( RETRIEVE_DOGE_MIN_AMOUNT, @@ -542,6 +545,7 @@ mod withdrawal { setup .withdrawal_flow() + .minter_await_fee_refresh() .ledger_approve_minter(account, withdrawal_amount) .minter_retrieve_doge_with_approval( withdrawal_amount, @@ -584,6 +588,7 @@ mod withdrawal { setup .withdrawal_flow() + .minter_await_fee_refresh() .ledger_approve_minter(account, withdrawal_amount) .minter_retrieve_doge_with_approval( withdrawal_amount, @@ -634,6 +639,8 @@ fn should_estimate_withdrawal_fee() { .minter_update_balance() .expect_mint(); + minter.await_fee_refresh(); + assert_eq!( estimate_withdrawal_fee_and_check(&minter, DOGE, 2), Err(EstimateWithdrawalFeeError::AmountTooLow { @@ -697,6 +704,7 @@ mod post_upgrade { setup .withdrawal_flow() + .minter_await_fee_refresh() .ledger_approve_minter(account, RETRIEVE_DOGE_MIN_AMOUNT) .minter_retrieve_doge_with_approval( RETRIEVE_DOGE_MIN_AMOUNT, diff --git a/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs b/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs index 45be16e56c69..320e6efdc6f9 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs @@ -28,7 +28,7 @@ use std::time::Duration; /// Entry point in the withdrawal flow /// -/// Step 1: approve the minter to burn user's funds +/// Step 1: await a refresh of the minter's median fee percentiles pub struct WithdrawalFlowStart { setup: S, } @@ -38,6 +38,21 @@ impl WithdrawalFlowStart { Self { setup } } + pub fn minter_await_fee_refresh(self) -> WithdrawalFlowApproval + where + S: AsRef, + { + self.setup.as_ref().minter().await_fee_refresh(); + WithdrawalFlowApproval { setup: self.setup } + } +} + +/// Step 2: approve the minter to burn user's funds +pub struct WithdrawalFlowApproval { + setup: S, +} + +impl WithdrawalFlowApproval { pub fn ledger_approve_minter(self, account: A, amount: u64) -> RetrieveDogeFlow where A: Into, diff --git a/rs/dogecoin/ckdoge/test_utils/src/lib.rs b/rs/dogecoin/ckdoge/test_utils/src/lib.rs index b6dba5818a64..f2086450d375 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/lib.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/lib.rs @@ -48,6 +48,31 @@ pub const LEDGER_TRANSFER_FEE: u64 = DOGE / 100; const MAX_TIME_IN_QUEUE: Duration = Duration::from_secs(10); pub const MIN_CONFIRMATIONS: u32 = 60; pub const BLOCK_TIME: Duration = Duration::from_secs(60); +/// Must be at least the minter's `refresh_fee_percentiles_frequency` (currently `SIX_MINUTES`, +/// i.e. 360s, defined in `rs/dogecoin/ckdoge/minter/src/lib.rs`). There is no compile-time link +/// between the two values: if the minter ever lowers that frequency below this interval, the fee +/// refresh in `MinterCanister::await_fee_refresh` may not fire and the withdrawal-fee flake +/// returns. Keep this value in sync with, and no smaller than, the minter's refresh frequency. +pub const FEE_PERCENTILES_REFRESH_INTERVAL: Duration = Duration::from_secs(360); + +/// Drains the minter's startup timer tasks. +/// +/// Installing or upgrading the minter calls `setup_tasks()`, which schedules both `ProcessLogic` +/// and `RefreshFeePercentiles` at T0. If left unfired, `RefreshFeePercentiles` would fire during +/// the first `DogecoinSyncGuard` tick in a subsequent `mine_blocks` call, sending a fee-percentile +/// XNet call to the dogecoin canister while it is syncing new blocks. This puts the canister in a +/// "not fully synced" state that prevents `DogecoinSyncGuard` from observing the target block +/// height, causing a timeout. +/// +/// Tick 1 fires `ProcessLogic`; tick 2 fires `RefreshFeePercentiles` and starts the XNet call; the +/// remaining ticks let the round-trip complete. Afterwards the timer is at T0+360s, far beyond any +/// `DogecoinSyncGuard` window. +pub(crate) fn drain_startup_tasks(env: &PocketIc) { + const STARTUP_DRAIN_TICKS: usize = 20; + for _ in 0..STARTUP_DRAIN_TICKS { + env.tick(); + } +} pub struct Setup { pub env: Arc, @@ -186,6 +211,8 @@ impl Setup { ); } + drain_startup_tasks(&env); + Self { env, doge_network, diff --git a/rs/dogecoin/ckdoge/test_utils/src/minter.rs b/rs/dogecoin/ckdoge/test_utils/src/minter.rs index 7be9b6fc7153..b01934df32ab 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/minter.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/minter.rs @@ -1,5 +1,7 @@ use crate::events::MinterEventAssert; -use crate::{MAX_TIME_IN_QUEUE, NNS_ROOT_PRINCIPAL}; +use crate::{ + FEE_PERCENTILES_REFRESH_INTERVAL, MAX_TIME_IN_QUEUE, NNS_ROOT_PRINCIPAL, drain_startup_tasks, +}; use candid::{Decode, Encode, Principal}; use canlog::LogEntry; use ic_ckdoge_minter::{ @@ -163,6 +165,31 @@ impl MinterCanister { Decode!(&call_result, Result).unwrap() } + pub fn await_fee_refresh(&self) { + let refreshes_before = self.count_fee_percentile_refreshes(); + self.env + .advance_time(FEE_PERCENTILES_REFRESH_INTERVAL + Duration::from_secs(1)); + let max_ticks = 100; + for _ in 0..max_ticks { + self.env.tick(); + if self.count_fee_percentile_refreshes() > refreshes_before { + return; + } + } + dbg!(self.get_logs()); + panic!( + "BUG: did not observe a successful fee-percentile refresh within {max_ticks} ticks \ + (the RefreshFeePercentiles task may have run but failed to compute a median fee)" + ); + } + + fn count_fee_percentile_refreshes(&self) -> usize { + self.get_logs() + .iter() + .filter(|entry| entry.message.contains("update median fee per vbyte")) + .count() + } + pub fn retrieve_doge_status(&self, ledger_burn_index: u64) -> RetrieveDogeStatus { let call_result = self .env @@ -351,8 +378,7 @@ impl MinterCanister { Some(NNS_ROOT_PRINCIPAL), ) .expect("BUG: failed to upgrade minter"); - // run immediate tasks after upgrade, like refreshing fee percentiles. - self.env.tick(); + drain_startup_tasks(&self.env); } }