From 598f329bd83b9b8e40ce3adaa55403751bd5d384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Demay?= Date: Fri, 29 May 2026 11:24:52 +0000 Subject: [PATCH 1/7] fix: deflake //rs/dogecoin/ckdoge/minter:integration_tests The ckDOGE withdrawal flow asserts that the fee returned by the `estimate_withdrawal_fee` query equals the fee used in the transaction that the minter actually submits. The query reflects the last refreshed median fee percentiles, which the minter only updates once its periodic refresh task runs after the dogecoin canister is synced. The estimate was therefore captured while the median was still the initialization default, racing the refresh and producing a fee mismatch only on the first withdrawal of a setup. Introduce a mandatory `minter_await_fee_refresh` step at the start of the withdrawal flow that drives the refresh and waits until the fee estimate stabilizes before the withdrawal request is submitted. The type-state makes the step impossible to skip for any caller reaching `expect_withdrawal_request_accepted`. Co-Authored-By: Claude Opus 4.8 (1M context) --- rs/dogecoin/ckdoge/minter/tests/tests.rs | 6 ++++ .../ckdoge/test_utils/src/flow/withdrawal.rs | 35 ++++++++++++++++++- rs/dogecoin/ckdoge/test_utils/src/lib.rs | 2 ++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/rs/dogecoin/ckdoge/minter/tests/tests.rs b/rs/dogecoin/ckdoge/minter/tests/tests.rs index 7fae5bb01b59..ff2b57ff2f3a 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(RETRIEVE_DOGE_MIN_AMOUNT) .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(RETRIEVE_DOGE_MIN_AMOUNT) .ledger_approve_minter(account, RETRIEVE_DOGE_MIN_AMOUNT); setup.ledger().stop(); @@ -454,6 +456,7 @@ mod withdrawal { setup .withdrawal_flow() + .minter_await_fee_refresh(RETRIEVE_DOGE_MIN_AMOUNT) .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(withdrawal_amount) .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(withdrawal_amount) .ledger_approve_minter(account, withdrawal_amount) .minter_retrieve_doge_with_approval( withdrawal_amount, @@ -697,6 +702,7 @@ mod post_upgrade { setup .withdrawal_flow() + .minter_await_fee_refresh(RETRIEVE_DOGE_MIN_AMOUNT) .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..c01f1c066682 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs @@ -1,4 +1,5 @@ use crate::BLOCK_TIME; +use crate::FEE_PERCENTILES_REFRESH_INTERVAL; use crate::MAX_TIME_IN_QUEUE; use crate::MIN_CONFIRMATIONS; use crate::{Setup, into_outpoint, parse_dogecoin_address}; @@ -28,7 +29,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 +39,38 @@ impl WithdrawalFlowStart { Self { setup } } + pub fn minter_await_fee_refresh(self, withdrawal_amount: u64) -> WithdrawalFlowApproval + where + S: AsRef, + { + let minter = self.setup.as_ref().minter(); + let env = self.setup.as_ref().env.clone(); + + let max_refreshes = 10; + let mut previous = minter.estimate_withdrawal_fee(withdrawal_amount); + for _ in 0..max_refreshes { + env.advance_time(FEE_PERCENTILES_REFRESH_INTERVAL + Duration::from_secs(1)); + for _ in 0..10 { + env.tick(); + } + let current = minter.estimate_withdrawal_fee(withdrawal_amount); + if current == previous { + return WithdrawalFlowApproval { setup: self.setup }; + } + previous = current; + } + panic!( + "BUG: fee estimate did not stabilize after {max_refreshes} refreshes; last estimate: {previous:?}" + ); + } +} + +/// 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..762d58a4f683 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/lib.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/lib.rs @@ -48,6 +48,8 @@ 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`. +pub const FEE_PERCENTILES_REFRESH_INTERVAL: Duration = Duration::from_secs(360); pub struct Setup { pub env: Arc, From 0a4fba02f89025f535276a84cba99032fd4d4f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Demay?= Date: Fri, 29 May 2026 11:49:54 +0000 Subject: [PATCH 2/7] Simplify minter_await_fee_refresh: advance once, tick until fee changes Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ckdoge/test_utils/src/flow/withdrawal.rs | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs b/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs index c01f1c066682..e8b3f6d22c9a 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs @@ -46,22 +46,15 @@ impl WithdrawalFlowStart { let minter = self.setup.as_ref().minter(); let env = self.setup.as_ref().env.clone(); - let max_refreshes = 10; - let mut previous = minter.estimate_withdrawal_fee(withdrawal_amount); - for _ in 0..max_refreshes { - env.advance_time(FEE_PERCENTILES_REFRESH_INTERVAL + Duration::from_secs(1)); - for _ in 0..10 { - env.tick(); + let previous = minter.estimate_withdrawal_fee(withdrawal_amount); + env.advance_time(FEE_PERCENTILES_REFRESH_INTERVAL + Duration::from_secs(1)); + for _ in 0..10 { + env.tick(); + if minter.estimate_withdrawal_fee(withdrawal_amount) != previous { + break; } - let current = minter.estimate_withdrawal_fee(withdrawal_amount); - if current == previous { - return WithdrawalFlowApproval { setup: self.setup }; - } - previous = current; } - panic!( - "BUG: fee estimate did not stabilize after {max_refreshes} refreshes; last estimate: {previous:?}" - ); + WithdrawalFlowApproval { setup: self.setup } } } From b90d4cf025c8ca9a37ec447cde765669b6c81d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Demay?= Date: Fri, 29 May 2026 12:28:49 +0000 Subject: [PATCH 3/7] Extract await_fee_refresh and use it in should_estimate_withdrawal_fee The standalone fee-estimation test does not go through the withdrawal flow and was hitting the same stale median fee percentiles. Move the refresh helper onto MinterCanister so both the withdrawal flow and the estimation test can warm up the fee state. Co-Authored-By: Claude Opus 4.8 (1M context) --- rs/dogecoin/ckdoge/minter/tests/tests.rs | 2 ++ .../ckdoge/test_utils/src/flow/withdrawal.rs | 16 ++++------------ rs/dogecoin/ckdoge/test_utils/src/minter.rs | 14 +++++++++++++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/rs/dogecoin/ckdoge/minter/tests/tests.rs b/rs/dogecoin/ckdoge/minter/tests/tests.rs index ff2b57ff2f3a..a401eb3ff33a 100644 --- a/rs/dogecoin/ckdoge/minter/tests/tests.rs +++ b/rs/dogecoin/ckdoge/minter/tests/tests.rs @@ -639,6 +639,8 @@ fn should_estimate_withdrawal_fee() { .minter_update_balance() .expect_mint(); + minter.await_fee_refresh(RETRIEVE_DOGE_MIN_AMOUNT); + assert_eq!( estimate_withdrawal_fee_and_check(&minter, DOGE, 2), Err(EstimateWithdrawalFeeError::AmountTooLow { diff --git a/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs b/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs index e8b3f6d22c9a..de6d98a84db4 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs @@ -1,5 +1,4 @@ use crate::BLOCK_TIME; -use crate::FEE_PERCENTILES_REFRESH_INTERVAL; use crate::MAX_TIME_IN_QUEUE; use crate::MIN_CONFIRMATIONS; use crate::{Setup, into_outpoint, parse_dogecoin_address}; @@ -43,17 +42,10 @@ impl WithdrawalFlowStart { where S: AsRef, { - let minter = self.setup.as_ref().minter(); - let env = self.setup.as_ref().env.clone(); - - let previous = minter.estimate_withdrawal_fee(withdrawal_amount); - env.advance_time(FEE_PERCENTILES_REFRESH_INTERVAL + Duration::from_secs(1)); - for _ in 0..10 { - env.tick(); - if minter.estimate_withdrawal_fee(withdrawal_amount) != previous { - break; - } - } + self.setup + .as_ref() + .minter() + .await_fee_refresh(withdrawal_amount); WithdrawalFlowApproval { setup: self.setup } } } diff --git a/rs/dogecoin/ckdoge/test_utils/src/minter.rs b/rs/dogecoin/ckdoge/test_utils/src/minter.rs index 7be9b6fc7153..212069eba1dc 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/minter.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/minter.rs @@ -1,5 +1,5 @@ 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}; use candid::{Decode, Encode, Principal}; use canlog::LogEntry; use ic_ckdoge_minter::{ @@ -163,6 +163,18 @@ impl MinterCanister { Decode!(&call_result, Result).unwrap() } + pub fn await_fee_refresh(&self, withdrawal_amount: u64) { + let previous = self.estimate_withdrawal_fee(withdrawal_amount); + self.env + .advance_time(FEE_PERCENTILES_REFRESH_INTERVAL + Duration::from_secs(1)); + for _ in 0..10 { + self.env.tick(); + if self.estimate_withdrawal_fee(withdrawal_amount) != previous { + break; + } + } + } + pub fn retrieve_doge_status(&self, ledger_burn_index: u64) -> RetrieveDogeStatus { let call_result = self .env From f0000cd89cf7f0c121b3a7e0b41b33bd66e82620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Demay?= Date: Fri, 29 May 2026 13:50:12 +0000 Subject: [PATCH 4/7] Wait for fee-percentile refresh evidence instead of fee value change Addresses review feedback: comparing fee estimates cannot distinguish an already-fresh estimate from one whose refresh has not yet succeeded, so a stale estimate could slip through. Wait until the minter logs a successful `estimate_fee_per_vbyte` refresh, and fail loudly if none occurs. Co-Authored-By: Claude Opus 4.8 (1M context) --- rs/dogecoin/ckdoge/minter/tests/tests.rs | 14 ++++++------- .../ckdoge/test_utils/src/flow/withdrawal.rs | 7 ++----- rs/dogecoin/ckdoge/test_utils/src/minter.rs | 20 ++++++++++++++----- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/rs/dogecoin/ckdoge/minter/tests/tests.rs b/rs/dogecoin/ckdoge/minter/tests/tests.rs index a401eb3ff33a..9560ce702c44 100644 --- a/rs/dogecoin/ckdoge/minter/tests/tests.rs +++ b/rs/dogecoin/ckdoge/minter/tests/tests.rs @@ -383,7 +383,7 @@ mod withdrawal { setup .withdrawal_flow() - .minter_await_fee_refresh(RETRIEVE_DOGE_MIN_AMOUNT) + .minter_await_fee_refresh() .ledger_approve_minter(account, RETRIEVE_DOGE_MIN_AMOUNT) .minter_retrieve_doge_with_approval( RETRIEVE_DOGE_MIN_AMOUNT, @@ -416,7 +416,7 @@ mod withdrawal { let withdrawal_flow = setup .withdrawal_flow() - .minter_await_fee_refresh(RETRIEVE_DOGE_MIN_AMOUNT) + .minter_await_fee_refresh() .ledger_approve_minter(account, RETRIEVE_DOGE_MIN_AMOUNT); setup.ledger().stop(); @@ -456,7 +456,7 @@ mod withdrawal { setup .withdrawal_flow() - .minter_await_fee_refresh(RETRIEVE_DOGE_MIN_AMOUNT) + .minter_await_fee_refresh() .ledger_approve_minter(account, RETRIEVE_DOGE_MIN_AMOUNT) .minter_retrieve_doge_with_approval( RETRIEVE_DOGE_MIN_AMOUNT, @@ -545,7 +545,7 @@ mod withdrawal { setup .withdrawal_flow() - .minter_await_fee_refresh(withdrawal_amount) + .minter_await_fee_refresh() .ledger_approve_minter(account, withdrawal_amount) .minter_retrieve_doge_with_approval( withdrawal_amount, @@ -588,7 +588,7 @@ mod withdrawal { setup .withdrawal_flow() - .minter_await_fee_refresh(withdrawal_amount) + .minter_await_fee_refresh() .ledger_approve_minter(account, withdrawal_amount) .minter_retrieve_doge_with_approval( withdrawal_amount, @@ -639,7 +639,7 @@ fn should_estimate_withdrawal_fee() { .minter_update_balance() .expect_mint(); - minter.await_fee_refresh(RETRIEVE_DOGE_MIN_AMOUNT); + minter.await_fee_refresh(); assert_eq!( estimate_withdrawal_fee_and_check(&minter, DOGE, 2), @@ -704,7 +704,7 @@ mod post_upgrade { setup .withdrawal_flow() - .minter_await_fee_refresh(RETRIEVE_DOGE_MIN_AMOUNT) + .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 de6d98a84db4..320e6efdc6f9 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/flow/withdrawal.rs @@ -38,14 +38,11 @@ impl WithdrawalFlowStart { Self { setup } } - pub fn minter_await_fee_refresh(self, withdrawal_amount: u64) -> WithdrawalFlowApproval + pub fn minter_await_fee_refresh(self) -> WithdrawalFlowApproval where S: AsRef, { - self.setup - .as_ref() - .minter() - .await_fee_refresh(withdrawal_amount); + self.setup.as_ref().minter().await_fee_refresh(); WithdrawalFlowApproval { setup: self.setup } } } diff --git a/rs/dogecoin/ckdoge/test_utils/src/minter.rs b/rs/dogecoin/ckdoge/test_utils/src/minter.rs index 212069eba1dc..4c18c3382f4f 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/minter.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/minter.rs @@ -163,16 +163,26 @@ impl MinterCanister { Decode!(&call_result, Result).unwrap() } - pub fn await_fee_refresh(&self, withdrawal_amount: u64) { - let previous = self.estimate_withdrawal_fee(withdrawal_amount); + 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)); - for _ in 0..10 { + let max_ticks = 10; + for _ in 0..max_ticks { self.env.tick(); - if self.estimate_withdrawal_fee(withdrawal_amount) != previous { - break; + if self.count_fee_percentile_refreshes() > refreshes_before { + return; } } + dbg!(self.get_logs()); + panic!("BUG: minter did not refresh fee percentiles within {max_ticks} ticks"); + } + + 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 { From f5734f304114c043efac500a85183aaa7bece649 Mon Sep 17 00:00:00 2001 From: Gregory Demay Date: Wed, 10 Jun 2026 08:17:46 +0000 Subject: [PATCH 5/7] fix: increase await_fee_refresh max_ticks to handle post-upgrade XNet latency After a second upgrade, both ProcessLogic and RefreshFeePercentiles are rescheduled at the current time T0. The upgrade's single tick fires ProcessLogic (lower enum order). When await_fee_refresh then runs, tick 1 fires RefreshFeePercentiles (starts XNet call) and tick 2 fires ProcessLogic again (T0+5s < T0+361s), consuming an extra tick. The remaining 8 ticks were insufficient for the full XNet round trip through the management canister to the bitcoin adapter and back. Increasing to 100 gives ample headroom while still returning early once the refresh completes. Co-Authored-By: Claude Sonnet 4.6 --- rs/dogecoin/ckdoge/test_utils/src/minter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/dogecoin/ckdoge/test_utils/src/minter.rs b/rs/dogecoin/ckdoge/test_utils/src/minter.rs index 4c18c3382f4f..b924fc1fac7a 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/minter.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/minter.rs @@ -167,7 +167,7 @@ impl MinterCanister { let refreshes_before = self.count_fee_percentile_refreshes(); self.env .advance_time(FEE_PERCENTILES_REFRESH_INTERVAL + Duration::from_secs(1)); - let max_ticks = 10; + let max_ticks = 100; for _ in 0..max_ticks { self.env.tick(); if self.count_fee_percentile_refreshes() > refreshes_before { From 01aa61d856d0d315276bd52ab33cb7cc41ba4a3a Mon Sep 17 00:00:00 2001 From: Gregory Demay Date: Wed, 10 Jun 2026 09:15:31 +0000 Subject: [PATCH 6/7] fix: complete fee refresh in Setup::new and upgrade() before returning After minter install (in setup_tasks()) and after each upgrade, both ProcessLogic and RefreshFeePercentiles are scheduled at T0. Previously only one tick was called, firing only ProcessLogic (lower enum order), leaving RefreshFeePercentiles at T0. It would then fire during the first DogecoinSyncGuard tick in a subsequent mine_blocks call, sending a fee- percentile XNet call to the dogecoin canister while it was busy syncing new blocks. This put the canister in a persistent "not fully synced" state that prevented DogecoinSyncGuard from observing the target block height, causing it to time out after 1000 ticks. Add 20 ticks in Setup::new (after install) and in upgrade() so that: - Tick 1 fires ProcessLogic synchronously - Tick 2 fires RefreshFeePercentiles and starts the XNet call - Remaining ticks let the XNet round-trip complete After this, the RefreshFeePercentiles timer is at T0+360s, well outside any DogecoinSyncGuard window, so mine_blocks never sees the concurrent fee-percentile call. Co-Authored-By: Claude Sonnet 4.6 --- rs/dogecoin/ckdoge/test_utils/src/lib.rs | 12 ++++++++++++ rs/dogecoin/ckdoge/test_utils/src/minter.rs | 12 ++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/rs/dogecoin/ckdoge/test_utils/src/lib.rs b/rs/dogecoin/ckdoge/test_utils/src/lib.rs index 762d58a4f683..8c411eeea4c3 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/lib.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/lib.rs @@ -188,6 +188,18 @@ impl Setup { ); } + // After minter install, setup_tasks() schedules ProcessLogic and RefreshFeePercentiles at + // T0. If left unfired, RefreshFeePercentiles would fire during the first DogecoinSyncGuard + // tick in mine_blocks, 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 XNet round-trip complete. After this the timer is at T0+360s, + // far beyond any DogecoinSyncGuard window. + for _ in 0..20 { + env.tick(); + } + 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 b924fc1fac7a..7d8b2160a431 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/minter.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/minter.rs @@ -373,8 +373,16 @@ impl MinterCanister { Some(NNS_ROOT_PRINCIPAL), ) .expect("BUG: failed to upgrade minter"); - // run immediate tasks after upgrade, like refreshing fee percentiles. - self.env.tick(); + // post_upgrade calls setup_tasks() which resets both ProcessLogic and RefreshFeePercentiles + // to T0. Tick 1 fires ProcessLogic; tick 2 fires RefreshFeePercentiles and starts its XNet + // call to the dogecoin canister; the remaining ticks let the round-trip complete so the + // timer is pushed to T0+360s before control returns. Without this, RefreshFeePercentiles + // would fire during the first DogecoinSyncGuard tick in a subsequent mine_blocks call, + // sending the fee-percentile request while the dogecoin canister is syncing blocks, which + // puts it in a "not fully synced" state that causes DogecoinSyncGuard to time out. + for _ in 0..20 { + self.env.tick(); + } } } From 59fc26b60a319dde3ea806119cc2813b47fa356b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Demay?= Date: Thu, 11 Jun 2026 15:48:05 +0000 Subject: [PATCH 7/7] chore: address review comments on ckdoge deflake test utils Extract a shared drain_startup_tasks helper used by Setup::new and upgrade(), strengthen the FEE_PERCENTILES_REFRESH_INTERVAL coupling note, and clarify the await_fee_refresh panic message. Co-Authored-By: Claude Opus 4.8 (1M context) --- rs/dogecoin/ckdoge/test_utils/src/lib.rs | 37 ++++++++++++++------- rs/dogecoin/ckdoge/test_utils/src/minter.rs | 20 +++++------ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/rs/dogecoin/ckdoge/test_utils/src/lib.rs b/rs/dogecoin/ckdoge/test_utils/src/lib.rs index 8c411eeea4c3..f2086450d375 100644 --- a/rs/dogecoin/ckdoge/test_utils/src/lib.rs +++ b/rs/dogecoin/ckdoge/test_utils/src/lib.rs @@ -48,9 +48,32 @@ 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`. +/// 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, doge_network: Network, @@ -188,17 +211,7 @@ impl Setup { ); } - // After minter install, setup_tasks() schedules ProcessLogic and RefreshFeePercentiles at - // T0. If left unfired, RefreshFeePercentiles would fire during the first DogecoinSyncGuard - // tick in mine_blocks, 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 XNet round-trip complete. After this the timer is at T0+360s, - // far beyond any DogecoinSyncGuard window. - for _ in 0..20 { - env.tick(); - } + drain_startup_tasks(&env); Self { env, diff --git a/rs/dogecoin/ckdoge/test_utils/src/minter.rs b/rs/dogecoin/ckdoge/test_utils/src/minter.rs index 7d8b2160a431..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::{FEE_PERCENTILES_REFRESH_INTERVAL, 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::{ @@ -175,7 +177,10 @@ impl MinterCanister { } } dbg!(self.get_logs()); - panic!("BUG: minter did not refresh fee percentiles within {max_ticks} ticks"); + 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 { @@ -373,16 +378,7 @@ impl MinterCanister { Some(NNS_ROOT_PRINCIPAL), ) .expect("BUG: failed to upgrade minter"); - // post_upgrade calls setup_tasks() which resets both ProcessLogic and RefreshFeePercentiles - // to T0. Tick 1 fires ProcessLogic; tick 2 fires RefreshFeePercentiles and starts its XNet - // call to the dogecoin canister; the remaining ticks let the round-trip complete so the - // timer is pushed to T0+360s before control returns. Without this, RefreshFeePercentiles - // would fire during the first DogecoinSyncGuard tick in a subsequent mine_blocks call, - // sending the fee-percentile request while the dogecoin canister is syncing blocks, which - // puts it in a "not fully synced" state that causes DogecoinSyncGuard to time out. - for _ in 0..20 { - self.env.tick(); - } + drain_startup_tasks(&self.env); } }