From 04e0051c8308605accdb2d2e1fc1e27d2ff94ede Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Tue, 1 Sep 2026 20:59:54 +0000 Subject: [PATCH] fix: deflake //rs/ethereum/cketh/minter:deposit_from_cex The minter's balance scan reads every balance at its cached latest_block_height, refreshed only by a separate 30-second timer - so a scan can run after the harness funded the deposit addresses on anvil and still eth_call at a pre-funding block, recording scan_count = 1 without having seen the funds. The live harness' await_scan accepted any Scanning status with scan_count >= 1 as terminal and returned that transient verdict, so the test's AwaitingSweep assertion flaked. An address now counts as scanned only once its status is AwaitingSweep (terminal by construction) or Scanning with a last_scanned_block at or past the chain head as of await_scan entry - the funding is mined by then, so such a scan cannot have missed the balance. SCAN_TICKS grows from 4 to 8 (a budget, not a cost): the pair's block-based backoff gap can demand up to 300 block-seconds before it is due for the scan the new predicate waits for. Co-Authored-By: Claude Fable 5 --- rs/ethereum/cketh/test_utils/src/live.rs | 43 ++++++++++++++++++------ 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/rs/ethereum/cketh/test_utils/src/live.rs b/rs/ethereum/cketh/test_utils/src/live.rs index 9274e75a2fa6..646b4063c118 100644 --- a/rs/ethereum/cketh/test_utils/src/live.rs +++ b/rs/ethereum/cketh/test_utils/src/live.rs @@ -123,8 +123,10 @@ const AWAIT_DEADLINE: Duration = Duration::from_secs(60); const SCAN_TICK: Duration = Duration::from_secs(BALANCE_SCAN_INTERVAL.as_secs() + 5); /// A budget, not a cost: one tick refreshes the latest block height the scan needs, the next scans; -/// the spares cover a tick lost to an outcall the jump timed out. -const SCAN_TICKS: u32 = 4; +/// the spares cover a tick lost to an outcall the jump timed out, and the blocks the pair's +/// block-based backoff gap demands before it is due again — up to 300 block-seconds (25 blocks) +/// once an address has been scanned a few times, against the ~10 blocks each tick's settle mines. +const SCAN_TICKS: u32 = 8; /// A budget, not a cost: the sweep path crosses the enqueue, send, and finalization timers, and /// driving stops the moment the expected transactions are on chain. @@ -334,28 +336,47 @@ impl LiveSetup { } } - /// Waits until the minter's periodic balance scan has scanned `caller`'s deposit address — - /// observed through `deposit_erc20`'s own status — and returns that response. An address counts - /// as scanned once its status is `Scanning` with `scan_count >= 1` (a below-minimum address, - /// advanced in place) or `AwaitingSweep` (a funded address, detected and queued). Either proves - /// the `eth_call` against anvil succeeded and decoded, since a failing batch never advances or - /// queues an address. Buys balance-scan ticks rather than waiting the interval out, and panics - /// if no scan completes within [`SCAN_TICKS`] of them. + /// Waits until the minter's periodic balance scan has scanned `caller`'s deposit address *at a + /// block where its funding is visible* — observed through `deposit_erc20`'s own status — and + /// returns that response. An address counts as scanned once its status is `AwaitingSweep` (a + /// funded address, detected and queued — terminal by construction) or `Scanning` with + /// `scan_count >= 1` and a `last_scanned_block` at or past the chain head as of entry (a + /// below-minimum address, advanced in place). Either proves the `eth_call` against anvil + /// succeeded, decoded, and read the funded balance. Buys balance-scan ticks rather than waiting + /// the interval out, and panics if no such scan completes within [`SCAN_TICKS`] of them. + /// + /// The pinned-block requirement is what makes the wait sound: the scan reads every balance at + /// the minter's *cached* latest block height, refreshed on its own thirty-second timer — so a + /// scan can run after [`Self::credit_deposits`] funded the addresses and still read them at a + /// pre-funding block, bumping `scan_count` without having seen the funds. Accepting any + /// `scan_count >= 1` returned that transient verdict and made the caller's `AwaitingSweep` + /// assertion flaky; a scan pinned at or past the entry head (the funding is mined before a + /// test awaits its scan) cannot have missed the balance. pub fn await_scan( &self, caller: Principal, subaccount: [u8; 32], token: &Erc20Token, ) -> DepositErc20Response { + let funded_by = Nat::from(self.anvil.block_number()); let mut scanned = None; self.drive_until_with( SCAN_TICK, SCAN_TICKS, - |_| "the deposit address was not scanned".to_string(), + |_| format!("the deposit address was not scanned at or past block {funded_by}"), |setup| { let progress = setup.deposit_erc20(caller, subaccount, token); let is_scanned = match &progress.status { - DepositStatus::Scanning { scan_count, .. } => *scan_count >= 1, + DepositStatus::Scanning { + scan_count, + last_scanned_block, + .. + } => { + *scan_count >= 1 + && last_scanned_block + .as_ref() + .is_some_and(|block| *block >= funded_by) + } DepositStatus::AwaitingSweep(_) => true, }; if is_scanned {