Skip to content

Commit e0823e1

Browse files
gregorydemayclaude
andauthored
test(cketh): build the live balance-scan harness on the shared fixtures (#11124)
## Summary The live-anvil balance-scan harness had grown into a second test fixture: it created and funded canisters, sequenced their installs, and registered ckERC20 tokens on its own, in parallel with the fixtures every other cketh integration test already uses. This PR removes that duplication — the harness now builds on the shared fixtures and keeps only what is genuinely specific to it: the local Ethereum node it owns, and the balance-scan assertions. Collapsing the two fixtures meant removing the knobs the duplicate had accumulated: - **Whether PocketIC runs live is decided once, after construction, not by the chain under test.** Every fixture — mocked or live — builds on an ordinary, non-live PocketIC instance, where setup calls tick deterministically; only `LiveBalanceScanSetup::new_live` switches the instance to auto-progress, and only once its whole fixture — including the ledger-suite orchestrator and the tokens it registers — is already built. Building against a live (wall-clock-paced) instance from the start made every setup call race a round deadline it didn't control, which reproducibly failed under CPU contention. - **The harness owns the local Ethereum node** rather than borrowing a URL to it, so the type proves a node is really running behind that URL. - **Callers can no longer supply a pre-built environment.** The chain under test already determines the canisters' init args, and every caller was passing exactly what would have been built anyway — the parameter only offered a way to get it wrong. - **Canisters are created and installed in the same order for every chain under test**, as on master; only their init args differ. The harness' dedicated controller and its skipped ledger install turned out not to be load-bearing: the anonymous principal already controls everything PocketIC creates by default, and the skipped install was down to a Bazel data dependency that simply wasn't declared. - **Token registration goes through a real ledger-suite orchestrator** instead of the harness impersonating one with placeholder ledgers — which is what testing the deposit flow will need next. - **The set of tokens the balance scan credits with anvil bytecode is now exactly the set the orchestrator registered**, not a separately hardcoded list — `Holding` and the harness' token-facing methods take the orchestrator's own token type directly, so the two can no longer silently drift apart. Behaviour of the mocked fixtures is otherwise unchanged: same canister creation order, same derived minter address, and the same cold-start property the deposit-flow tests depend on. One thing did change for every fixture, mocked included: canisters are now funded with `u64::MAX` cycles rather than `u128::MAX` — the amount that reproducibly crashes the live harness' replica on its first HTTPS outcall — deliberately funding every fixture the same way rather than carrying two cycle amounts. No cketh test asserts on cycle balances, so this is not expected to be observable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent d6163ed commit e0823e1

6 files changed

Lines changed: 270 additions & 293 deletions

File tree

rs/ethereum/cketh/minter/BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ rust_test(
301301
"tests/deposit_from_cex_demo/MockUSDT.sol",
302302
# End-to-end balance scan on a live PocketIC + local anvil node.
303303
":cketh_minter_debug.wasm.gz",
304+
"//rs/ethereum/ledger-suite-orchestrator:ledger_suite_orchestrator_canister.wasm.gz",
305+
"//rs/ledger_suite/icrc1/archive:archive_canister_u256.wasm.gz",
306+
"//rs/ledger_suite/icrc1/index-ng:index_ng_canister_u256.wasm.gz",
307+
"//rs/ledger_suite/icrc1/ledger:ledger_canister_u256.wasm.gz",
304308
"//rs/pocket_ic_server:pocket-ic-server",
305309
"@evm_rpc.wasm.gz//file",
306310
],
@@ -311,6 +315,10 @@ rust_test(
311315
"CARGO_MANIFEST_DIR": "rs/ethereum/cketh/minter",
312316
"CKETH_MINTER_WASM_PATH": "$(rootpath :cketh_minter_debug.wasm.gz)",
313317
"EVM_RPC_CANISTER_WASM_PATH": "$(rootpath @evm_rpc.wasm.gz//file)",
318+
"INDEX_CANISTER_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/index-ng:index_ng_canister_u256.wasm.gz)",
319+
"LEDGER_ARCHIVE_NODE_CANISTER_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/archive:archive_canister_u256.wasm.gz)",
320+
"LEDGER_CANISTER_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/ledger:ledger_canister_u256.wasm.gz)",
321+
"LEDGER_SUITE_ORCHESTRATOR_WASM_PATH": "$(rootpath //rs/ethereum/ledger-suite-orchestrator:ledger_suite_orchestrator_canister.wasm.gz)",
314322
"POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)",
315323
},
316324
deps = [

rs/ethereum/cketh/minter/tests/ckerc20.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,7 @@ mod deposit_erc20 {
150150
use ic_cketh_minter::endpoints::{DepositErc20Error, DepositStatus};
151151
use ic_cketh_minter::state::automatic_deposits::DEPOSIT_ADDRESS_SCAN_WINDOW;
152152
use ic_cketh_test_utils::ckerc20::CkErc20Setup;
153-
use ic_cketh_test_utils::{
154-
DEFAULT_USER_SUBACCOUNT, format_ethereum_address_to_eip_55, new_pocket_ic,
155-
};
156-
use std::sync::Arc;
153+
use ic_cketh_test_utils::{DEFAULT_USER_SUBACCOUNT, format_ethereum_address_to_eip_55};
157154

158155
/// Number of `AutomaticDepositReceived` events currently in the minter's audit log.
159156
fn count_automatic_deposits_received(ckerc20: &CkErc20Setup) -> usize {
@@ -172,7 +169,7 @@ mod deposit_erc20 {
172169

173170
#[test]
174171
fn should_trap_when_ckerc20_feature_not_active() {
175-
let ckerc20 = CkErc20Setup::new_without_ckerc20_active(Arc::new(new_pocket_ic()));
172+
let ckerc20 = CkErc20Setup::new_without_ckerc20_active();
176173
let caller = ckerc20.caller();
177174
ckerc20
178175
.call_minter_deposit_erc20(
@@ -577,21 +574,20 @@ mod withdraw_erc20 {
577574
CKETH_TRANSFER_FEE, DEFAULT_BLOCK_HASH, DEFAULT_BLOCK_NUMBER,
578575
DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION, DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION_FEE,
579576
DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION_HASH, DEFAULT_PRINCIPAL_ID, EXPECTED_BALANCE,
580-
JsonRpcProvider, new_pocket_ic,
577+
JsonRpcProvider,
581578
};
582579
use ic_ledger_suite_orchestrator_test_utils::CKERC20_TRANSFER_FEE;
583580
use icrc_ledger_types::icrc3::transactions::Burn;
584581
use num_bigint::BigUint;
585582
use num_traits::ToPrimitive;
586583
use serde_bytes::ByteBuf;
587584
use std::convert::identity;
588-
use std::sync::Arc;
589585

590586
const NOT_SUPPORTED_CKERC20_LEDGER_ID: Principal = Principal::management_canister();
591587

592588
#[test]
593589
fn should_trap_when_ckerc20_feature_not_active() {
594-
CkErc20Setup::new_without_ckerc20_active(Arc::new(new_pocket_ic()))
590+
CkErc20Setup::new_without_ckerc20_active()
595591
.call_minter_withdraw_erc20(
596592
Principal::anonymous(),
597593
0_u8,

rs/ethereum/cketh/minter/tests/deposit_from_cex.rs

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use ic_cketh_minter::deposit_address::DepositAddress;
2929
use ic_cketh_minter::endpoints::DepositStatus;
3030
use ic_cketh_minter::numeric::Erc20Value;
3131
use ic_cketh_test_utils::anvil::{Anvil, DEV_ACCOUNT, address_from_hex, deploy_mock_erc20};
32-
use ic_cketh_test_utils::live_scan::{CkErc20LiveScanSetup, Holding, SupportedToken};
32+
use ic_cketh_test_utils::live_scan::{Holding, LiveBalanceScanSetup};
3333
use ic_ethereum_types::Address;
3434
use std::time::Duration;
3535

@@ -197,26 +197,18 @@ fn should_flag_only_deposits_at_or_above_the_per_token_minimum() {
197197
const USDC_ABOVE_MINIMUM: u128 = 15_000_000;
198198
const USDT_BELOW_MINIMUM: u128 = 1_000_000;
199199

200-
let setup = CkErc20LiveScanSetup::new_live();
200+
let setup = LiveBalanceScanSetup::new_live();
201+
// `supported_erc20_tokens()` registers ckUSDC then ckUSDT, in that order.
202+
let [usdc, usdt] = setup.supported_erc20_tokens() else {
203+
panic!("expected exactly 2 supported tokens")
204+
};
201205
let deposits = [
202-
(
203-
setup.depositor(1),
204-
SupportedToken::CkUsdt,
205-
USDT_ABOVE_MINIMUM,
206-
),
207-
(
208-
setup.depositor(2),
209-
SupportedToken::CkUsdc,
210-
USDC_ABOVE_MINIMUM,
211-
),
212-
(
213-
setup.depositor(3),
214-
SupportedToken::CkUsdt,
215-
USDT_BELOW_MINIMUM,
216-
),
206+
(setup.depositor(1), usdt, USDT_ABOVE_MINIMUM),
207+
(setup.depositor(2), usdc, USDC_ABOVE_MINIMUM),
208+
(setup.depositor(3), usdt, USDT_BELOW_MINIMUM),
217209
];
218210

219-
let holdings: Vec<Holding> = deposits
211+
let holdings: Vec<Holding<'_>> = deposits
220212
.iter()
221213
.map(|&(depositor, token, amount)| Holding {
222214
deposit: setup.register_deposit_address(depositor, DEPOSIT_SUBACCOUNT, token),
@@ -228,21 +220,21 @@ fn should_flag_only_deposits_at_or_above_the_per_token_minimum() {
228220

229221
let deadline = Duration::from_secs(180);
230222
assert_matches!(
231-
setup.await_scan(setup.depositor(1), DEPOSIT_SUBACCOUNT, SupportedToken::CkUsdt, deadline).status,
223+
setup.await_scan(setup.depositor(1), DEPOSIT_SUBACCOUNT, usdt, deadline).status,
232224
DepositStatus::AwaitingSweep(detected)
233-
if detected.erc20_contract_address == SupportedToken::CkUsdt.contract().to_string()
225+
if detected.erc20_contract_address == usdt.contract.address
234226
&& detected.scanned_balance == USDT_ABOVE_MINIMUM
235227
&& detected.detected_at_block > 0_u8
236228
);
237229
assert_matches!(
238-
setup.await_scan(setup.depositor(2), DEPOSIT_SUBACCOUNT, SupportedToken::CkUsdc, deadline).status,
230+
setup.await_scan(setup.depositor(2), DEPOSIT_SUBACCOUNT, usdc, deadline).status,
239231
DepositStatus::AwaitingSweep(detected)
240-
if detected.erc20_contract_address == SupportedToken::CkUsdc.contract().to_string()
232+
if detected.erc20_contract_address == usdc.contract.address
241233
&& detected.scanned_balance == USDC_ABOVE_MINIMUM
242234
&& detected.detected_at_block > 0_u8
243235
);
244236
assert_matches!(
245-
setup.await_scan(setup.depositor(3), DEPOSIT_SUBACCOUNT, SupportedToken::CkUsdt, deadline).status,
237+
setup.await_scan(setup.depositor(3), DEPOSIT_SUBACCOUNT, usdt, deadline).status,
246238
DepositStatus::Scanning { scan_count, last_scanned_block, .. }
247239
if scan_count >= 1 && last_scanned_block.is_some()
248240
);

rs/ethereum/cketh/test_utils/src/ckerc20.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::{
1313
DEPOSIT_WITH_SUBACCOUNT_HELPER_CONTRACT_ADDRESS, ERC20_HELPER_CONTRACT_ADDRESS,
1414
ETH_HELPER_CONTRACT_ADDRESS, LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL, LedgerBalance, MAX_TICKS,
1515
RECEIVED_ERC20_EVENT_TOPIC, RECEIVED_ETH_OR_ERC20_WITH_SUBACCOUNT_EVENT_TOPIC, assert_reply,
16-
format_ethereum_address_to_eip_55, new_pocket_ic,
16+
format_ethereum_address_to_eip_55,
1717
};
1818
use assert_matches::assert_matches;
1919
use candid::{Decode, Encode, Nat, Principal};
@@ -62,7 +62,7 @@ pub struct CkErc20Setup {
6262

6363
impl Default for CkErc20Setup {
6464
fn default() -> Self {
65-
Self::new(Arc::new(new_pocket_ic()))
65+
Self::new()
6666
}
6767
}
6868

@@ -73,8 +73,14 @@ impl AsRef<CkEthSetup> for CkErc20Setup {
7373
}
7474

7575
impl CkErc20Setup {
76-
pub fn new(env: Arc<PocketIc>) -> Self {
77-
let mut ckerc20 = Self::new_without_ckerc20_active(env);
76+
pub fn new() -> Self {
77+
Self::with_cketh(CkEthSetup::default())
78+
}
79+
80+
/// Activates the ckERC20 feature on an existing ckETH fixture: the balance-scan harness in
81+
/// [`crate::live_scan`] supplies one backed by a live anvil node rather than the mocked default.
82+
pub(crate) fn with_cketh(cketh: CkEthSetup) -> Self {
83+
let mut ckerc20 = Self::without_ckerc20_active(cketh);
7884
ckerc20.cketh = ckerc20
7985
.cketh
8086
.upgrade_minter_to_add_orchestrator_id(
@@ -84,8 +90,14 @@ impl CkErc20Setup {
8490
ckerc20
8591
}
8692

87-
pub fn new_without_ckerc20_active(env: Arc<PocketIc>) -> Self {
88-
let cketh = CkEthSetup::new(env.clone());
93+
pub fn new_without_ckerc20_active() -> Self {
94+
Self::without_ckerc20_active(CkEthSetup::default())
95+
}
96+
97+
/// The ckETH fixture builds the PocketIC instance; the orchestrator is then created on that
98+
/// same instance so both share one replica.
99+
fn without_ckerc20_active(cketh: CkEthSetup) -> Self {
100+
let env = cketh.env.clone();
89101
let orchestrator = LedgerSuiteOrchestrator::new(
90102
env.clone(),
91103
LedgerSuiteOrchestratorInitArg {

0 commit comments

Comments
 (0)