-
Notifications
You must be signed in to change notification settings - Fork 405
test(cketh): build the live balance-scan harness on the shared fixtures #11124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 17 commits
5f853e6
5c6bda6
c2053c6
f683e8c
7e84561
04d586b
206bb5c
d14bc70
33d34d1
651d03a
ad8ce46
5d619c3
a114ffd
501c22f
d000a55
78aa809
986fd38
336bc52
7ebe033
75f3d8e
0a4ea72
8a44389
a87674b
96d3b0b
7a5682b
8aff162
3cb7de4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ use crate::flow::{ | |
| use crate::mock::JsonRpcMethod; | ||
| use assert_matches::assert_matches; | ||
| use candid::{Decode, Encode, Nat, Principal}; | ||
| use evm_rpc_types::{InstallArgs, OverrideProvider, RegexSubstitution}; | ||
| use ic_base_types::PrincipalId; | ||
| use ic_cketh_minter::endpoints::events::{Event, EventPayload, GetEventsResult}; | ||
| use ic_cketh_minter::endpoints::{ | ||
|
|
@@ -31,7 +32,7 @@ use pocket_ic::common::rest::{ | |
| CanisterHttpReject, CanisterHttpReply, CanisterHttpRequest, CanisterHttpResponse, IcpConfig, | ||
| IcpConfigFlag, MockCanisterHttpResponse, RawEffectivePrincipal, RawMessageId, | ||
| }; | ||
| use pocket_ic::{PocketIc, PocketIcBuilder, RejectResponse}; | ||
| use pocket_ic::{CanisterSettings, PocketIc, PocketIcBuilder, RejectResponse}; | ||
| use std::path::PathBuf; | ||
| use std::str::FromStr; | ||
| use std::sync::Arc; | ||
|
|
@@ -122,7 +123,7 @@ pub struct CkEthSetup { | |
|
|
||
| impl Default for CkEthSetup { | ||
| fn default() -> Self { | ||
| Self::new(Arc::new(new_pocket_ic())) | ||
| Self::builder().build() | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -144,43 +145,18 @@ impl PocketIcHttpQuery for &CkEthSetup { | |
|
|
||
| impl CkEthSetup { | ||
| pub fn new(env: Arc<PocketIc>) -> Self { | ||
| // Create minter canister first to match canister ID and Ethereum address hardcoded in tests. | ||
| let minter_id = env.create_canister(); | ||
| env.add_cycles(minter_id, u128::MAX); | ||
| let ledger_id = env.create_canister(); | ||
| env.add_cycles(ledger_id, u128::MAX); | ||
| let evm_rpc_id = env.create_canister(); | ||
| env.add_cycles(evm_rpc_id, u128::MAX); | ||
|
|
||
| env.install_canister( | ||
| ledger_id, | ||
| ledger_wasm(), | ||
| Encode!(&LedgerArgument::Init( | ||
| LedgerInitArgsBuilder::with_symbol_and_name("ckETH", "ckETH") | ||
| .with_minting_account(minter_id) | ||
| .with_transfer_fee(CKETH_TRANSFER_FEE) | ||
| .with_max_memo_length(80) | ||
| .with_decimals(18) | ||
| .with_feature_flags(ic_icrc1_ledger::FeatureFlags { | ||
| icrc2: true, | ||
| icrc152: false | ||
| }) | ||
| .build(), | ||
| )) | ||
| .unwrap(), | ||
| None, | ||
| ); | ||
| install_evm_rpc(&env, evm_rpc_id); | ||
| let minter_id = install_minter(&env, ledger_id, minter_id, evm_rpc_id); | ||
| Self::builder().with_env(env).build() | ||
| } | ||
|
|
||
| let caller = PrincipalId::new_user_test_id(DEFAULT_PRINCIPAL_ID); | ||
| Self { | ||
| env, | ||
| caller, | ||
| ledger_id, | ||
| minter_id, | ||
| evm_rpc_id, | ||
| support_subaccount: false, | ||
| /// A builder for [`CkEthSetup`], defaulting to today's mocked-fixture behaviour: a fresh | ||
| /// PocketIC instance (fiduciary subnet only), an anonymous controller, and canned JSON-RPC | ||
| /// responses. [`live_scan`] extends it for the live balance-scan harness via | ||
| /// [`CkEthSetupBuilder::with_live_mode`]. | ||
| fn builder<'a>() -> CkEthSetupBuilder<'a> { | ||
| CkEthSetupBuilder { | ||
| env: None, | ||
| backend: EthereumBackend::Mocked, | ||
| live: false, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -747,18 +723,117 @@ impl CkEthSetup { | |
| } | ||
| } | ||
|
|
||
| struct CkEthSetupBuilder<'a> { | ||
| env: Option<Arc<PocketIc>>, | ||
| backend: EthereumBackend<'a>, | ||
| live: bool, | ||
| } | ||
|
|
||
| impl<'a> CkEthSetupBuilder<'a> { | ||
| /// Reuses an existing PocketIC instance instead of building one via [`new_env`], so this | ||
| /// fixture can be composed with others that need the same instance (e.g. `CkErc20Setup`'s | ||
| /// orchestrator). Not combined with [`Self::with_live_mode`] today: live mode's instance needs | ||
| /// the added NNS subnet and to already be live before any canister exists. | ||
| fn with_env(mut self, env: Arc<PocketIc>) -> Self { | ||
|
gregorydemay marked this conversation as resolved.
Outdated
|
||
| self.env = Some(env); | ||
| self | ||
| } | ||
|
|
||
| fn with_ethereum_backend(mut self, backend: EthereumBackend<'a>) -> Self { | ||
| self.backend = backend; | ||
| self | ||
| } | ||
|
|
||
| /// Switches PocketIC to live mode: an NNS subnet in addition to the fiduciary one, going live | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧐 🔵 Nit — this doc enumerates what live mode switches ("an NNS subnet …, going live …, and a fixed non-anonymous controller"), reading as a complete list, but
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Fixed in 5d619c3 (same commit as the medium above, since both touch |
||
| /// before any canister exists (see [`new_env`]), a fixed non-anonymous controller (instead of | ||
| /// every other fixture's anonymous default) that owns every canister this fixture creates (see | ||
| /// [`live_controller`]), and leaving the ckETH ledger uninstalled — the balance scan never | ||
| /// calls it, and installing it would need the ledger canister Wasm declared as a Bazel data | ||
| /// dependency of the anvil-backed test target, which it is not. | ||
| fn with_live_mode(mut self) -> Self { | ||
| self.live = true; | ||
| self | ||
| } | ||
|
|
||
| fn build(self) -> CkEthSetup { | ||
| debug_assert!( | ||
| !(self.live && self.env.is_some()), | ||
| "with_env's instance would be used as-is, but self.live would still select the \ | ||
| controller and skip the ledger install: a live-flagged fixture on a non-live instance" | ||
| ); | ||
| let env_mode = if self.live { | ||
| EnvMode::Live | ||
| } else { | ||
| EnvMode::Mocked | ||
| }; | ||
| let env = self.env.unwrap_or_else(|| Arc::new(new_env(env_mode))); | ||
| let controller = self.live.then(live_controller); | ||
| let canisters = create_cketh_canisters(&env, controller); | ||
| if !self.live { | ||
| // Live mode leaves the ckETH ledger uninstalled; see with_live_mode's doc for why. | ||
| install_ledger(&env, &canisters); | ||
| } | ||
| install_evm_rpc(&env, &canisters, &self.backend); | ||
| install_minter(&env, &canisters, &self.backend); | ||
|
|
||
| CkEthSetup { | ||
| env, | ||
| caller: PrincipalId::new_user_test_id(DEFAULT_PRINCIPAL_ID), | ||
| ledger_id: canisters.ledger_id, | ||
| minter_id: canisters.minter_id, | ||
| evm_rpc_id: canisters.evm_rpc_id, | ||
| support_subaccount: false, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// A fixed non-anonymous principal used as the live harness's canisters' controller and as the | ||
| /// minter's stand-in ledger-suite-orchestrator id, so [`live_scan`] can both register supported | ||
| /// tokens itself (calling `add_ckerc20_token` as the orchestrator) and fetch the minter's canister | ||
| /// logs (controller-only by default) as that same principal. | ||
| fn live_controller() -> Principal { | ||
| Principal::from_slice(&[0x0a; 10]) | ||
| } | ||
|
|
||
| #[derive(Clone, Copy)] | ||
| enum EnvMode { | ||
| Mocked, | ||
| Live, | ||
| } | ||
|
|
||
| /// Builds the PocketIC instance for [`CkEthSetupBuilder::build`]: the fiduciary subnet every | ||
| /// ckETH fixture needs for the secp256k1 `key_1` used by the minter, plus — in [`EnvMode::Live`] — | ||
| /// an NNS subnet (required by [`PocketIc::make_live`]) and going live immediately, before any | ||
| /// canister of this fixture exists to schedule a timer whose outcall could stall waiting for an | ||
| /// answer. | ||
| fn new_env(mode: EnvMode) -> PocketIc { | ||
| let mut builder = pocket_ic_builder().with_icp_config(IcpConfig { | ||
| canister_execution_rate_limiting: Some(IcpConfigFlag::Disabled), | ||
| ..Default::default() | ||
| }); | ||
| if let EnvMode::Live = mode { | ||
| builder = builder.with_nns_subnet(); | ||
| } | ||
| let mut env = builder.build(); | ||
| if let EnvMode::Live = mode { | ||
| env.make_live(None); | ||
| } | ||
| env | ||
| } | ||
|
|
||
| pub fn format_ethereum_address_to_eip_55(address: &str) -> String { | ||
| Address::from_str(address).unwrap().to_string() | ||
| } | ||
|
|
||
| pub fn new_pocket_ic() -> PocketIc { | ||
| PocketIcBuilder::new() | ||
| .with_fiduciary_subnet() | ||
| .with_icp_config(IcpConfig { | ||
| canister_execution_rate_limiting: Some(IcpConfigFlag::Disabled), | ||
| ..Default::default() | ||
| }) | ||
| .build() | ||
| new_env(EnvMode::Mocked) | ||
| } | ||
|
|
||
| /// A [`PocketIcBuilder`] with the fiduciary subnet every ckETH fixture needs for the secp256k1 | ||
| /// `key_1` used by the minter; callers add anything further (e.g. an NNS subnet for | ||
| /// [`PocketIc::make_live`]) before `build()`. | ||
| fn pocket_ic_builder() -> PocketIcBuilder { | ||
|
gregorydemay marked this conversation as resolved.
Outdated
|
||
| PocketIcBuilder::new().with_fiduciary_subnet() | ||
| } | ||
|
|
||
| fn ledger_wasm() -> Vec<u8> { | ||
|
|
@@ -791,36 +866,150 @@ fn evm_rpc_wasm() -> Vec<u8> { | |
| ) | ||
| } | ||
|
|
||
| fn install_minter( | ||
| env: &PocketIc, | ||
| ledger_id: Principal, | ||
| /// The minter, its ckETH ledger and the EVM RPC canister it calls out to, plus the sender to | ||
| /// install/upgrade them as (`None`: every other fixture's anonymous default; `Some`: the live | ||
| /// harness' [`live_controller`]). Built by [`create_cketh_canisters`] and installed by | ||
| /// [`install_ledger`]/[`install_minter`]/[`install_evm_rpc`], used by | ||
| /// [`CkEthSetupBuilder::build`] for both modes. | ||
| struct CkEthCanisters { | ||
| minter_id: Principal, | ||
| ledger_id: Principal, | ||
| evm_rpc_id: Principal, | ||
| ) -> Principal { | ||
| controller: Option<Principal>, | ||
| } | ||
|
|
||
| fn create_canister(env: &PocketIc, controller: Option<Principal>) -> Principal { | ||
| match controller { | ||
| None => env.create_canister(), | ||
| Some(controller) => env.create_canister_with_settings( | ||
| Some(controller), | ||
| Some(CanisterSettings { | ||
| controllers: Some(vec![controller]), | ||
| ..Default::default() | ||
| }), | ||
| ), | ||
| } | ||
| } | ||
|
|
||
| /// Cycles every canister this fixture creates is funded with. `u128::MAX` — the natural "as much as | ||
| /// possible" choice — reproducibly crashes the live harness' PocketIC replica with a | ||
| /// cycle-accounting assertion failure (`Invalid cycle change`) on the minter's first HTTPS outcall: | ||
| /// a canister already at the saturating `Cycles` balance ceiling cannot observe any further | ||
| /// addition. This amount leaves headroom below that ceiling instead. | ||
| const CANISTER_CYCLES: u128 = u64::MAX as u128; | ||
|
|
||
| fn create_cketh_canisters(env: &PocketIc, controller: Option<Principal>) -> CkEthCanisters { | ||
| // Create minter canister first to match canister ID and Ethereum address hardcoded in tests. | ||
| let minter_id = create_canister(env, controller); | ||
| env.add_cycles(minter_id, CANISTER_CYCLES); | ||
| let ledger_id = create_canister(env, controller); | ||
| env.add_cycles(ledger_id, CANISTER_CYCLES); | ||
| let evm_rpc_id = create_canister(env, controller); | ||
| env.add_cycles(evm_rpc_id, CANISTER_CYCLES); | ||
| CkEthCanisters { | ||
| minter_id, | ||
| ledger_id, | ||
| evm_rpc_id, | ||
| controller, | ||
| } | ||
| } | ||
|
|
||
| fn install_ledger(env: &PocketIc, canisters: &CkEthCanisters) { | ||
| env.install_canister( | ||
| canisters.ledger_id, | ||
| ledger_wasm(), | ||
| Encode!(&LedgerArgument::Init( | ||
| LedgerInitArgsBuilder::with_symbol_and_name("ckETH", "ckETH") | ||
| .with_minting_account(canisters.minter_id) | ||
| .with_transfer_fee(CKETH_TRANSFER_FEE) | ||
| .with_max_memo_length(80) | ||
| .with_decimals(18) | ||
| .with_feature_flags(ic_icrc1_ledger::FeatureFlags { | ||
| icrc2: true, | ||
| icrc152: false | ||
| }) | ||
| .build(), | ||
| )) | ||
| .unwrap(), | ||
| canisters.controller, | ||
| ); | ||
| } | ||
|
|
||
| /// The Ethereum chain under test: which JSON-RPC endpoint the EVM RPC canister's outcalls reach, | ||
| /// and the corresponding minter init/upgrade assumptions about that chain's state (the block | ||
| /// height to track, and where its log-scraping cursor starts). | ||
| enum EthereumBackend<'a> { | ||
| /// Canned JSON-RPC mocks pinned to a historical mainnet snapshot. | ||
| Mocked, | ||
| /// A live anvil node reached over HTTP at `url`: a fresh chain with no finalized blocks yet. | ||
| Anvil(&'a str), | ||
| } | ||
|
|
||
| impl EthereumBackend<'_> { | ||
| fn install_args(&self) -> InstallArgs { | ||
| InstallArgs { | ||
| override_provider: match self { | ||
| EthereumBackend::Mocked => None, | ||
| EthereumBackend::Anvil(url) => Some(OverrideProvider { | ||
| override_url: Some(RegexSubstitution { | ||
| pattern: ".*".into(), | ||
| replacement: url.to_string(), | ||
| }), | ||
| }), | ||
| }, | ||
| ..Default::default() | ||
| } | ||
| } | ||
|
|
||
| fn ethereum_block_height(&self) -> CandidBlockTag { | ||
| match self { | ||
| // The mocked responses replay a historical mainnet snapshot, long since finalized. | ||
| EthereumBackend::Mocked => CandidBlockTag::Finalized, | ||
| // A fresh anvil chain has no finalized blocks, so track its "latest" head instead. | ||
| EthereumBackend::Anvil(_) => CandidBlockTag::Latest, | ||
| } | ||
| } | ||
|
|
||
| fn last_scraped_block_number(&self) -> Nat { | ||
| match self { | ||
| // The block the mocked JSON-RPC responses are canned to scrape logs from onward. | ||
| EthereumBackend::Mocked => LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into(), | ||
| EthereumBackend::Anvil(_) => 0_u8.into(), | ||
| } | ||
| } | ||
|
gregorydemay marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /// PocketIC's fiduciary subnet holds the secp256k1 test key under this name, the key the minter | ||
| /// derives deposit addresses from. | ||
| const ECDSA_KEY_NAME: &str = "key_1"; | ||
|
|
||
| fn install_minter(env: &PocketIc, canisters: &CkEthCanisters, backend: &EthereumBackend) { | ||
| let args = MinterInitArgs { | ||
| ecdsa_key_name: "key_1".parse().unwrap(), | ||
| ecdsa_key_name: ECDSA_KEY_NAME.to_string(), | ||
| ethereum_network: EthereumNetwork::Mainnet, | ||
| ledger_id, | ||
| ledger_id: canisters.ledger_id, | ||
| next_transaction_nonce: 0_u8.into(), | ||
| ethereum_block_height: CandidBlockTag::Finalized, | ||
| ethereum_block_height: backend.ethereum_block_height(), | ||
| ethereum_contract_address: Some(ETH_HELPER_CONTRACT_ADDRESS.to_string()), | ||
| minimum_withdrawal_amount: CKETH_MINIMUM_WITHDRAWAL_AMOUNT.into(), | ||
| last_scraped_block_number: LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into(), | ||
| evm_rpc_id: Some(evm_rpc_id), | ||
| last_scraped_block_number: backend.last_scraped_block_number(), | ||
| evm_rpc_id: Some(canisters.evm_rpc_id), | ||
| }; | ||
| let minter_arg = MinterArg::InitArg(args); | ||
| env.install_canister( | ||
| minter_id, | ||
| canisters.minter_id, | ||
| minter_wasm(), | ||
| Encode!(&minter_arg).unwrap(), | ||
| None, | ||
| Encode!(&MinterArg::InitArg(args)).unwrap(), | ||
| canisters.controller, | ||
| ); | ||
| minter_id | ||
| } | ||
|
|
||
| fn install_evm_rpc(env: &PocketIc, evm_rpc_id: Principal) { | ||
| let args = evm_rpc_types::InstallArgs::default(); | ||
| env.install_canister(evm_rpc_id, evm_rpc_wasm(), Encode!(&args).unwrap(), None); | ||
| fn install_evm_rpc(env: &PocketIc, canisters: &CkEthCanisters, backend: &EthereumBackend) { | ||
| env.install_canister( | ||
| canisters.evm_rpc_id, | ||
| evm_rpc_wasm(), | ||
| Encode!(&backend.install_args()).unwrap(), | ||
| canisters.controller, | ||
| ); | ||
| } | ||
|
|
||
| fn fail_as_timed_out(env: &PocketIc, request: &CanisterHttpRequest) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.