diff --git a/Cargo.lock b/Cargo.lock index 3839770c87a8..80acd1b623ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7447,12 +7447,12 @@ dependencies = [ "ic-ethereum-types", "ic-http-types", "ic-ledger-suite-orchestrator-test-utils", + "ic-management-canister-types 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "ic-management-canister-types-private", "ic-metrics-encoder", "ic-secp256k1", "ic-sha3 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "ic-stable-structures 0.6.9", - "ic-state-machine-tests", "ic-utils-ensure", "icrc-cbor", "icrc-ledger-client-cdk", @@ -7462,6 +7462,7 @@ dependencies = [ "num-bigint 0.4.6", "num-traits", "phantom_newtype", + "pocket-ic", "proptest", "rand 0.8.6", "reqwest", @@ -7490,22 +7491,18 @@ dependencies = [ "evm_rpc_types", "hex", "ic-base-types", - "ic-cdk 0.19.0", "ic-cketh-minter", - "ic-error-types 0.2.0", "ic-ethereum-types", "ic-http-types", "ic-icrc1-ledger", "ic-ledger-suite-orchestrator", "ic-ledger-suite-orchestrator-test-utils", - "ic-management-canister-types-private", + "ic-management-canister-types 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "ic-metrics-assert", - "ic-state-machine-tests", "ic-test-utilities-load-wasm", - "ic-types", - "ic-types-cycles", "icrc-ledger-types", "num-traits", + "pocket-ic", "serde", "serde_bytes", "serde_json", diff --git a/rs/ethereum/cketh/minter/BUILD.bazel b/rs/ethereum/cketh/minter/BUILD.bazel index fce8dcd23dc2..87fd39be12c6 100644 --- a/rs/ethereum/cketh/minter/BUILD.bazel +++ b/rs/ethereum/cketh/minter/BUILD.bazel @@ -208,6 +208,7 @@ rust_ic_test_suite( ), data = [ ":cketh_minter_debug.wasm.gz", + "//:pocket-ic-server", "//rs/ethereum/ledger-suite-orchestrator:ledger_suite_orchestrator_canister.wasm.gz", "//rs/ledger_suite/icrc1/archive:archive_canister_u256.wasm.gz", "//rs/ledger_suite/icrc1/index-ng:index_ng_canister_u256.wasm.gz", @@ -222,6 +223,7 @@ rust_ic_test_suite( "INDEX_CANISTER_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/index-ng:index_ng_canister_u256.wasm.gz)", "LEDGER_ARCHIVE_NODE_CANISTER_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/archive:archive_canister_u256.wasm.gz)", "EVM_RPC_CANISTER_WASM_PATH": "$(rootpath @evm_rpc.wasm.gz//file)", + "POCKET_IC_BIN": "$(rootpath //:pocket-ic-server)", }, proc_macro_deps = [], deps = [ @@ -229,15 +231,15 @@ rust_ic_test_suite( ":minter", "//packages/ic-ethereum-types", "//packages/icrc-ledger-types:icrc_ledger_types_storable", + "//packages/pocket-ic", "//rs/ethereum/cketh/test_utils", "//rs/ethereum/ledger-suite-orchestrator/test_utils", - "//rs/state_machine_tests", "//rs/types/base_types", - "//rs/types/management_canister_types", "@crate_index//:assert_matches", "@crate_index//:candid", "@crate_index//:ethers-core", "@crate_index//:hex", + "@crate_index//:ic-management-canister-types", "@crate_index//:minicbor", "@crate_index//:num-bigint", "@crate_index//:num-traits", diff --git a/rs/ethereum/cketh/minter/Cargo.toml b/rs/ethereum/cketh/minter/Cargo.toml index 919517949c81..c5272f0b816e 100644 --- a/rs/ethereum/cketh/minter/Cargo.toml +++ b/rs/ethereum/cketh/minter/Cargo.toml @@ -66,8 +66,9 @@ ic-cketh-test-utils = { path = "../test_utils" } ic-config = { path = "../../../config" } ic-crypto-test-utils-reproducible-rng = { path = "../../../crypto/test_utils/reproducible_rng" } ic-ledger-suite-orchestrator-test-utils = { path = "../../ledger-suite-orchestrator/test_utils" } -ic-state-machine-tests = { path = "../../../state_machine_tests" } +ic-management-canister-types = { workspace = true } maplit = { workspace = true } +pocket-ic = { path = "../../../../packages/pocket-ic" } proptest = { workspace = true } rand = { workspace = true } reqwest = { workspace = true } diff --git a/rs/ethereum/cketh/minter/tests/ckerc20.rs b/rs/ethereum/cketh/minter/tests/ckerc20.rs index 331d343a9db1..4dcd65d64e6e 100644 --- a/rs/ethereum/cketh/minter/tests/ckerc20.rs +++ b/rs/ethereum/cketh/minter/tests/ckerc20.rs @@ -1,6 +1,5 @@ use assert_matches::assert_matches; use candid::{Nat, Principal}; -use ic_base_types::CanisterId; use ic_cketh_minter::blocklist::SAMPLE_BLOCKED_ADDRESS; use ic_cketh_minter::endpoints::CandidBlockTag::Finalized; use ic_cketh_minter::endpoints::events::{EventPayload, EventSource}; @@ -31,10 +30,9 @@ use ic_cketh_test_utils::{ LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL, MINTER_ADDRESS, format_ethereum_address_to_eip_55, }; use ic_ethereum_types::Address; -use ic_ledger_suite_orchestrator_test_utils::flow::call_ledger_icrc1_total_supply; +use ic_ledger_suite_orchestrator_test_utils::pocket_ic::flow::call_ledger_icrc1_total_supply; use ic_ledger_suite_orchestrator_test_utils::{supported_erc20_tokens, usdc}; -use ic_management_canister_types_private::CanisterStatusType; -use ic_state_machine_tests::{ErrorCode, WasmResult}; +use ic_management_canister_types::CanisterStatusType; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc1::transfer::Memo; use icrc_ledger_types::icrc3::transactions::Mint; @@ -45,13 +43,13 @@ use std::time::Duration; fn should_refuse_to_add_ckerc20_token_from_unauthorized_principal() { let cketh = CkEthSetup::default(); let result = cketh.add_ckerc20_token(Principal::anonymous(), &ckusdc()); - assert_matches!(result, Err(e) if e.code() == ErrorCode::CanisterCalledTrap && e.description().contains("ERROR: ERC-20")); + assert_matches!(result, Err(e) if e.error_code == pocket_ic::ErrorCode::CanisterCalledTrap && e.reject_message.contains("ERROR: ERC-20")); let orchestrator_id: Principal = "nbsys-saaaa-aaaar-qaaga-cai".parse().unwrap(); let result = cketh .upgrade_minter_to_add_orchestrator_id(orchestrator_id) .add_ckerc20_token(Principal::anonymous(), &ckusdc()); - assert_matches!(result, Err(e) if e.code() == ErrorCode::CanisterCalledTrap && e.description().contains("ERROR: only the orchestrator")); + assert_matches!(result, Err(e) if e.error_code == pocket_ic::ErrorCode::CanisterCalledTrap && e.reject_message.contains("ERROR: only the orchestrator")); fn ckusdc() -> AddCkErc20Token { AddCkErc20Token { @@ -97,9 +95,7 @@ fn should_retry_to_add_usdc_when_minter_stopped() { let mut ckerc20 = CkErc20Setup::default(); let usdc = usdc(); - let stop_msg_id = ckerc20 - .env - .stop_canister_non_blocking(ckerc20.cketh.minter_id); + let stop_msg_id = ckerc20.cketh.submit_stop_minter(); assert_eq!(ckerc20.cketh.minter_status(), CanisterStatusType::Stopping); ckerc20.orchestrator = ckerc20 @@ -114,16 +110,19 @@ fn should_retry_to_add_usdc_when_minter_stopped() { .ledger .unwrap(); + // `stop_ongoing_https_outcalls` above guarantees no outcall is left pending (it panics + // otherwise), so pocket-ic's own built-in `await_call` can be used as-is here: unlike + // elsewhere in this suite, there is nothing left for a test-injected mock response to answer. ckerc20.cketh.stop_ongoing_https_outcalls(); - let stop_res = ckerc20.env.await_ingress(stop_msg_id, 100); - assert_matches!(stop_res, Ok(WasmResult::Reply(_))); + let stop_res = ckerc20.env.await_call(stop_msg_id); + assert_matches!(stop_res, Ok(_)); assert_eq!(ckerc20.cketh.minter_status(), CanisterStatusType::Stopped); - ckerc20.env.advance_time(RETRY_FREQUENCY); + ckerc20.advance_time(RETRY_FREQUENCY); ckerc20.env.tick(); ckerc20.cketh.start_minter(); assert_eq!(ckerc20.cketh.minter_status(), CanisterStatusType::Running); - ckerc20.env.advance_time(RETRY_FREQUENCY); + ckerc20.advance_time(RETRY_FREQUENCY); ckerc20.env.tick(); ckerc20 @@ -151,12 +150,12 @@ mod deposit_erc20 { use ic_cketh_minter::state::automatic_deposits::DEPOSIT_ADDRESS_SCAN_WINDOW; use ic_cketh_test_utils::DEFAULT_USER_SUBACCOUNT; use ic_cketh_test_utils::ckerc20::CkErc20Setup; - use ic_ledger_suite_orchestrator_test_utils::new_state_machine; + use ic_cketh_test_utils::new_pocket_ic; use std::sync::Arc; #[test] fn should_trap_when_ckerc20_feature_not_active() { - let ckerc20 = CkErc20Setup::new_without_ckerc20_active(Arc::new(new_state_machine())); + let ckerc20 = CkErc20Setup::new_without_ckerc20_active(Arc::new(new_pocket_ic())); let caller = ckerc20.caller(); ckerc20 .call_minter_deposit_erc20(caller, None) @@ -183,7 +182,7 @@ mod deposit_erc20 { let time_after = ckerc20.env.get_time().as_nanos_since_unix_epoch(); assert_eq!( - response.address, "0x9cEc8260d73Be0C2f2cC217808bf21008Bf22E4C", + response.address, "0xaEE5aaa7fBfA9802e128D93e12eC4387824E4e4E", "BUG: key derivation should be stable" ); assert!( @@ -390,9 +389,9 @@ mod withdraw_erc20 { CKETH_TRANSFER_FEE, DEFAULT_BLOCK_HASH, DEFAULT_BLOCK_NUMBER, DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION, DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION_FEE, DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION_HASH, DEFAULT_PRINCIPAL_ID, EXPECTED_BALANCE, - JsonRpcProvider, + JsonRpcProvider, new_pocket_ic, }; - use ic_ledger_suite_orchestrator_test_utils::{CKERC20_TRANSFER_FEE, new_state_machine}; + use ic_ledger_suite_orchestrator_test_utils::CKERC20_TRANSFER_FEE; use icrc_ledger_types::icrc3::transactions::Burn; use num_bigint::BigUint; use num_traits::ToPrimitive; @@ -404,7 +403,7 @@ mod withdraw_erc20 { #[test] fn should_trap_when_ckerc20_feature_not_active() { - CkErc20Setup::new_without_ckerc20_active(Arc::new(new_state_machine())) + CkErc20Setup::new_without_ckerc20_active(Arc::new(new_pocket_ic())) .call_minter_withdraw_erc20( Principal::anonymous(), 0_u8, @@ -644,7 +643,7 @@ mod withdraw_erc20 { subaccount: None, }, spender: Some(Account { - owner: minter.into(), + owner: minter, subaccount: None, }), memo: Some(Memo::from(BurnMemo::Erc20GasFee { @@ -673,7 +672,7 @@ mod withdraw_erc20 { }, ]); - ckerc20.env.advance_time(PROCESS_REIMBURSEMENT); + ckerc20.advance_time(PROCESS_REIMBURSEMENT); ckerc20.env.tick(); ckerc20.env.tick(); let balance_after_reimbursement = ckerc20.cketh.balance_of(caller); @@ -772,7 +771,7 @@ mod withdraw_erc20 { .expect_refresh_gas_fee_estimate(identity) .expect_error(insufficient_allowance_error.clone()); - ckerc20.env.advance_time(Duration::from_secs(59)); + ckerc20.advance_time(Duration::from_secs(59)); let ckerc20 = ckerc20 .call_minter_withdraw_erc20( @@ -784,7 +783,7 @@ mod withdraw_erc20 { .expect_no_refresh_gas_fee_estimate() .expect_error(insufficient_allowance_error.clone()); - ckerc20.env.advance_time(Duration::from_millis(1_001)); + ckerc20.advance_time(Duration::from_millis(1_001)); ckerc20 .call_minter_withdraw_erc20( @@ -930,7 +929,7 @@ mod withdraw_erc20 { amount: ckerc20_tx_fee.into(), from: cketh_account, spender: Some(Account { - owner: minter.into(), + owner: minter, subaccount: None, }), memo: Some(Memo::from(BurnMemo::Erc20GasFee { @@ -951,7 +950,7 @@ mod withdraw_erc20 { amount: ckerc20_withdrawal_amount.into(), from: ckerc20_account, spender: Some(Account { - owner: minter.into(), + owner: minter, subaccount: None, }), memo: Some(Memo::from(BurnMemo::Erc20Convert { @@ -1038,7 +1037,7 @@ mod withdraw_erc20 { }, ]); - ckerc20.env.advance_time(PROCESS_REIMBURSEMENT); + ckerc20.advance_time(PROCESS_REIMBURSEMENT); let cketh_balance_after_reimbursement = ckerc20.wait_for_updated_ledger_balance( ckerc20.cketh_ledger_id(), cketh_account, @@ -1313,7 +1312,7 @@ mod withdraw_erc20 { let ckerc20_tx_fee = DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION_FEE; let (first_tx, first_tx_sig) = default_erc20_signed_eip_1559_transaction(); let first_tx_hash = hash_transaction(first_tx.clone(), first_tx_sig); - let resubmitted_sent_tx = "0x02f8b0018084625900808507af2c9f6282fde894a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4880b844a9059cbb000000000000000000000000221e931fbfcb9bd54ddd26ce6f5e29e98add01c000000000000000000000000000000000000000000000000000000000001e8480c001a03acbc792d2f821acaab8da81517f1905e30cd3acd2f85d7995c68c0ad1fd8817a0793a076f2163658c833ccddd37ee0a762a18adb423f689db5ffcf528ae667bf0"; + let resubmitted_sent_tx = "0x02f8b0018084625900808507af2c9f6282fde894a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4880b844a9059cbb000000000000000000000000221e931fbfcb9bd54ddd26ce6f5e29e98add01c000000000000000000000000000000000000000000000000000000000001e8480c001a06e09ef9986f589954218ef3f4d8959beb931d28e837f7b26845612875bf2b6c0a02595a316273c3ec265988faf4a698f73d86fd8fb7050b1569e453cab743415a2"; let (resubmitted_tx, resubmitted_tx_sig) = decode_transaction(resubmitted_sent_tx); let resubmitted_tx_hash = hash_transaction(resubmitted_tx.clone(), resubmitted_tx_sig); assert_eq!( @@ -1526,7 +1525,7 @@ mod withdraw_erc20 { assert_eq!(price, second_price); // test an error case - let not_ledger_id = ckerc20.cketh.minter_id.into(); + let not_ledger_id = ckerc20.cketh.minter_id; ckerc20 .cketh .eip_1559_transaction_price_expecting_err(not_ledger_id); @@ -1689,12 +1688,8 @@ fn should_deposit_cketh_and_ckerc20_when_ledger_temporary_offline() { let ckusdc = ckerc20.find_ckerc20_token("ckUSDC"); let caller = ckerc20.caller(); - let stop_res = ckerc20.env.stop_canister(ckerc20.cketh.ledger_id); - assert_matches!( - stop_res, - Ok(WasmResult::Reply(_)), - "Failed to stop ckETH ledger" - ); + let stop_res = ckerc20.env.stop_canister(ckerc20.cketh.ledger_id, None); + assert_matches!(stop_res, Ok(()), "Failed to stop ckETH ledger"); ckerc20.stop_ckerc20_ledger(ckusdc.ledger_canister_id); let deposit_flow = ckerc20.deposit_cketh_and_ckerc20( @@ -1738,15 +1733,11 @@ fn should_deposit_cketh_and_ckerc20_when_ledger_temporary_offline() { ) }); - let start_res = ckerc20.env.start_canister(ckerc20.cketh.ledger_id); - assert_matches!( - start_res, - Ok(WasmResult::Reply(_)), - "Failed to start ckETH ledger" - ); + let start_res = ckerc20.env.start_canister(ckerc20.cketh.ledger_id, None); + assert_matches!(start_res, Ok(()), "Failed to start ckETH ledger"); ckerc20.start_ckerc20_ledger(ckusdc.ledger_canister_id); - ckerc20.env.advance_time(MINT_RETRY_DELAY); + ckerc20.advance_time(MINT_RETRY_DELAY); ckerc20.env.tick(); let ckerc20 = ckerc20.check_events().assert_has_unique_events_in_order(&[ @@ -1773,10 +1764,7 @@ fn should_deposit_cketh_and_ckerc20_when_ledger_temporary_offline() { Nat::from(CKETH_MINIMUM_WITHDRAWAL_AMOUNT) ); assert_eq!( - call_ledger_icrc1_total_supply( - &ckerc20.env, - CanisterId::unchecked_from_principal(ckusdc.ledger_canister_id.into()), - ), + call_ledger_icrc1_total_supply(&ckerc20.env, ckusdc.ledger_canister_id), Nat::from(ONE_USDC) ); } @@ -1910,7 +1898,7 @@ fn should_retrieve_minter_info() { LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into() ), cketh_ledger_id: Some(ckerc20.cketh_ledger_id()), - evm_rpc_id: Some(ckerc20.cketh.evm_rpc_id.into()), + evm_rpc_id: Some(ckerc20.cketh.evm_rpc_id), } ); } @@ -1962,8 +1950,9 @@ fn should_scrape_from_last_scraped_after_upgrade() { // Set latest_finalized_block so that we scraped twice each time. let latest_finalized_block = LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + max_eth_logs_block_range * 2; - ckerc20.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + ckerc20.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) + .with_request_params(json!(["finalized", false])) .respond_for_all_with(block_response(latest_finalized_block)) .build() .expect_rpc_calls(&ckerc20); @@ -2039,8 +2028,9 @@ fn should_scrape_from_last_scraped_after_upgrade() { // Advance block height and scrape again let latest_finalized_block = u64::try_from(second_to_block.into_inner()).unwrap() + max_eth_logs_block_range; - ckerc20.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + ckerc20.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) + .with_request_params(json!(["finalized", false])) .respond_for_all_with(block_response(latest_finalized_block)) .build() .expect_rpc_calls(&ckerc20); @@ -2081,7 +2071,7 @@ fn should_not_scrape_when_no_erc20_token() { // Set latest_finalized_block so that we scrapped twice each time. let latest_finalized_block = LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + max_eth_logs_block_range; - ckerc20.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + ckerc20.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) .respond_for_all_with(block_response(latest_finalized_block)) .build() diff --git a/rs/ethereum/cketh/minter/tests/cketh.rs b/rs/ethereum/cketh/minter/tests/cketh.rs index 94e618b52b6e..ced08dd28930 100644 --- a/rs/ethereum/cketh/minter/tests/cketh.rs +++ b/rs/ethereum/cketh/minter/tests/cketh.rs @@ -34,13 +34,12 @@ use ic_cketh_test_utils::{ LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL, MINTER_ADDRESS, }; use ic_ethereum_types::Address; -use ic_management_canister_types_private::CanisterStatusType; +use ic_management_canister_types::CanisterStatusType; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc1::transfer::Memo; use icrc_ledger_types::icrc3::transactions::{Burn, Mint}; use num_traits::cast::ToPrimitive; use serde_json::json; -use std::collections::BTreeMap; use std::str::FromStr; use std::time::Duration; @@ -80,7 +79,7 @@ fn should_deposit_and_withdraw() { subaccount: params.recipient_subaccount, }, }; - let minter: Principal = cketh.minter_id.into(); + let minter: Principal = cketh.minter_id; let withdrawal_amount = Nat::from(withdrawal_amount); let cketh = cketh @@ -182,7 +181,7 @@ fn should_retrieve_cache_transaction_price() { let destination = DEFAULT_WITHDRAWAL_DESTINATION_ADDRESS.to_string(); let result = cketh.eip_1559_transaction_price(None); - assert_matches!(result, Err(e) if e.code() == ic_state_machine_tests::ErrorCode::CanisterCalledTrap); + assert_matches!(result, Err(e) if e.error_code == pocket_ic::ErrorCode::CanisterCalledTrap); let cketh = cketh .deposit(DepositParams::default()) @@ -213,7 +212,7 @@ fn should_retrieve_cache_transaction_price() { assert_eq!(price, second_price); let price_using_ledger_id = - cketh.eip_1559_transaction_price_expecting_ok(Some(cketh.ledger_id.into())); + cketh.eip_1559_transaction_price_expecting_ok(Some(cketh.ledger_id)); assert_eq!(price, price_using_ledger_id); } @@ -437,7 +436,7 @@ fn should_not_send_eth_transaction_when_fee_history_inconsistent() { #[test] fn should_reimburse() { let cketh = CkEthSetup::default(); - let minter: Principal = cketh.minter_id.into(); + let minter: Principal = cketh.minter_id; let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); let destination = "0x221E931fbFcb9bd54DdD26cE6f5e29E98AdD01C0".to_string(); @@ -467,7 +466,7 @@ fn should_reimburse() { assert_eq!(balance_before_withdrawal, withdrawal_amount); // advance time so that time does not grow implicitly when executing a round - cketh.env.advance_time(Duration::from_secs(1)); + cketh.advance_time(Duration::from_secs(1)); let time_at_withdrawal = cketh.env.get_time().as_nanos_since_unix_epoch(); let cketh = cketh @@ -503,7 +502,7 @@ fn should_reimburse() { assert_eq!(cketh.balance_of(caller), Nat::from(0_u8)); - cketh.env.advance_time(PROCESS_REIMBURSEMENT); + cketh.advance_time(PROCESS_REIMBURSEMENT); cketh.env.tick(); let cost_of_failed_transaction = withdrawal_amount @@ -628,7 +627,7 @@ fn should_resubmit_new_transaction_when_price_increased() { let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); let (expected_tx, expected_sig) = default_signed_eip_1559_transaction(); let first_tx_hash = hash_transaction(expected_tx.clone(), expected_sig); - let resubmitted_sent_tx = "0x02f873018084625900808507b81d70e382520894221e931fbfcb9bd54ddd26ce6f5e29e98add01c0880160cc412e75c2de80c001a03d58ee49c9dce3b3c646eeb18317b46cc852a5384be9026cb0aa3d59f9b16292a007276dfb5e003bd7f527675e15c8512f1324e6434c62e7ffa4c68971d726fa0b"; + let resubmitted_sent_tx = "0x02f873018084625900808507b81d70e382520894221e931fbfcb9bd54ddd26ce6f5e29e98add01c0880160cc412e75c2de80c001a0a50a97743db2c45bfafaa668abb848ea6a15818d7c704b06c8957827fa68c6e8a04175e5fa7fbda1bb84e254f31acafd0fdf95f094127b0520b4e447e80d1afe37"; let (resubmitted_tx, resubmitted_tx_sig) = decode_transaction(resubmitted_sent_tx); let resubmitted_tx_hash = hash_transaction(resubmitted_tx.clone(), resubmitted_tx_sig); assert_eq!( @@ -703,7 +702,7 @@ fn should_not_overlap_when_scrapping_logs() { let cketh = CkEthSetup::default(); let max_eth_logs_block_range = cketh.max_logs_block_range(); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) .respond_for_all_with(block_response(DEFAULT_BLOCK_NUMBER)) .build() @@ -754,8 +753,11 @@ fn should_retry_from_same_block_when_scrapping_fails() { let max_eth_logs_block_range = cketh.max_logs_block_range(); let prev_events_len = cketh.get_all_events().len(); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + // Constrained so the block height refresh timer's query for the latest block + // cannot consume this stub and leave the finalized one unanswered. MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) + .with_request_params(json!(["finalized", false])) .respond_for_all_with(block_response(DEFAULT_BLOCK_NUMBER)) .build() .expect_rpc_calls(&cketh); @@ -783,8 +785,9 @@ fn should_retry_from_same_block_when_scrapping_fails() { block_number: LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into(), }]); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) + .with_request_params(json!(["finalized", false])) .respond_for_all_with(block_response(DEFAULT_BLOCK_NUMBER)) .build() .expect_rpc_calls(&cketh); @@ -810,7 +813,7 @@ fn should_retry_from_same_block_when_scrapping_fails() { fn should_scrap_one_block_when_at_boundary_with_last_finalized_block() { let cketh = CkEthSetup::default(); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) .respond_for_all_with(block_response(LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + 1)) .build() @@ -848,7 +851,7 @@ fn should_be_able_to_stop_canister_during_scraping() { let cketh = CkEthSetup::default(); let max_eth_logs_block_range = cketh.as_ref().max_logs_block_range(); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); mock_eth_get_block_by_number(&cketh, MAX_BLOCK); // Starts scraping to create open call contexts. @@ -871,7 +874,7 @@ fn should_be_able_to_stop_canister_during_scraping() { cketh.env.tick(); cketh.env.tick(); assert_eq!( - cketh.env.canister_http_request_contexts().len(), + cketh.env.get_canister_http().len(), 4, "Expected HTTPS outcalls since scraping is still in progress: {}", debug_http_outcalls(&cketh.env) @@ -904,15 +907,15 @@ fn should_be_able_to_stop_canister_during_scraping() { cketh.tick_until_minter_canister_status(CanisterStatusType::Stopped); assert_eq!( - cketh.env.canister_http_request_contexts(), - BTreeMap::default(), + cketh.env.get_canister_http(), + Vec::new(), "Unexpected pending HTTPS outcalls" ); // Restarting the canister should resume scraping from where we stopped cketh.start_minter(); cketh.tick_until_minter_canister_status(CanisterStatusType::Running); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); mock_eth_get_block_by_number(&cketh, MAX_BLOCK); from_block = to_block.checked_increment().unwrap(); @@ -936,8 +939,11 @@ fn should_panic_when_last_finalized_block_in_the_past() { let cketh = CkEthSetup::default(); let prev_events_len = cketh.get_all_events().len(); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + // Constrained so the block height refresh timer's query for the latest block + // cannot consume this stub and leave the finalized one unanswered. MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) + .with_request_params(json!(["finalized", false])) .respond_for_all_with(block_response(LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL - 1)) .build() .expect_rpc_calls(&cketh); @@ -951,8 +957,9 @@ fn should_panic_when_last_finalized_block_in_the_past() { }]); let last_finalized_block = LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + 10; - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) + .with_request_params(json!(["finalized", false])) .respond_for_all_with(block_response(last_finalized_block)) .build() .expect_rpc_calls(&cketh); @@ -1047,7 +1054,7 @@ fn should_half_range_of_scrapped_logs_when_response_over_two_mega_bytes() { .checked_add(BlockNumber::from(max_eth_logs_block_range / 2)) .unwrap(); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) .respond_for_all_with(block_response(DEFAULT_BLOCK_NUMBER)) .build() @@ -1096,7 +1103,7 @@ fn should_skip_single_block_containing_too_many_events() { let large_amount_of_logs = multi_logs_for_single_transaction(deposit.clone(), 3_500); assert!(serde_json::to_vec(&large_amount_of_logs).unwrap().len() > 2_000_000); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) .respond_for_all_with(block_response(LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + 3)) .build() @@ -1174,6 +1181,16 @@ fn should_skip_single_block_containing_too_many_events() { ]); } +#[test] +fn should_derive_minter_address() { + let cketh = CkEthSetup::default(); + + assert_eq!( + Address::from_str(&cketh.minter_address()).unwrap(), + Address::from_str(MINTER_ADDRESS).unwrap() + ); +} + #[allow(deprecated)] #[test] fn should_retrieve_minter_info() { @@ -1187,7 +1204,9 @@ fn should_retrieve_minter_info() { assert_eq!( info_at_start, MinterInfo { - minter_address: Some(format_ethereum_address_to_eip_55(MINTER_ADDRESS)), + // The minter derives its address from a timer scheduled at install, so a + // fixture that has executed no round has not derived it yet. + minter_address: None, smart_contract_address: Some(format_ethereum_address_to_eip_55( ETH_HELPER_CONTRACT_ADDRESS )), @@ -1208,8 +1227,8 @@ fn should_retrieve_minter_info() { last_deposit_with_subaccount_scraped_block_number: Some( LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into() ), - cketh_ledger_id: Some(cketh.ledger_id.into()), - evm_rpc_id: Some(cketh.evm_rpc_id.into()), + cketh_ledger_id: Some(cketh.ledger_id), + evm_rpc_id: Some(cketh.evm_rpc_id), } ); @@ -1220,6 +1239,7 @@ fn should_retrieve_minter_info() { assert_eq!( info_after_deposit, MinterInfo { + minter_address: Some(format_ethereum_address_to_eip_55(MINTER_ADDRESS)), last_observed_block_number: Some(Nat::from(new_eth_scraped_block_number)), eth_balance: Some(Nat::from(EXPECTED_BALANCE)), last_eth_scraped_block_number: Some(new_eth_scraped_block_number.into()), @@ -1310,7 +1330,7 @@ mod cketh_evm_rpc { fn should_retrieve_block_number() { let cketh = CkEthSetup::default(); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) .respond_for_all_with(block_response(LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + 3)) .build() @@ -1326,11 +1346,11 @@ mod cketh_evm_rpc { cketh.stop_minter(); cketh .env - .stop_canister(cketh.evm_rpc_id) + .stop_canister(cketh.evm_rpc_id, None) .expect("Failed to stop EVM RPC canister"); cketh.start_minter(); - cketh.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + cketh.advance_time(SCRAPING_ETH_LOGS_INTERVAL); for _ in 0..10 { cketh.env.tick(); diff --git a/rs/ethereum/cketh/test_utils/BUILD.bazel b/rs/ethereum/cketh/test_utils/BUILD.bazel index aed0c3de0a6d..3a2897973b5b 100644 --- a/rs/ethereum/cketh/test_utils/BUILD.bazel +++ b/rs/ethereum/cketh/test_utils/BUILD.bazel @@ -16,27 +16,23 @@ rust_library( version = "0.1.0", deps = [ # Keep sorted. - "//packages/ic-error-types", "//packages/ic-ethereum-types", "//packages/ic-http-types", - "//packages/ic-metrics-assert", + "//packages/ic-metrics-assert:ic-metrics-assert_pocket_ic", "//packages/icrc-ledger-types:icrc_ledger_types_storable", + "//packages/pocket-ic", "//rs/ethereum/cketh/minter", "//rs/ethereum/ledger-suite-orchestrator:ledger_suite_orchestrator", "//rs/ethereum/ledger-suite-orchestrator/test_utils", "//rs/ledger_suite/icrc1/ledger", - "//rs/state_machine_tests", "//rs/test_utilities/load_wasm", "//rs/types/base_types", - "//rs/types/cycles", - "//rs/types/management_canister_types", - "//rs/types/types", "@crate_index//:assert_matches", "@crate_index//:candid", "@crate_index//:ethers-core", "@crate_index//:evm_rpc_types", "@crate_index//:hex", - "@crate_index//:ic-cdk", + "@crate_index//:ic-management-canister-types", "@crate_index//:num-traits", "@crate_index//:serde", "@crate_index//:serde_bytes", diff --git a/rs/ethereum/cketh/test_utils/Cargo.toml b/rs/ethereum/cketh/test_utils/Cargo.toml index 377647797377..198197db88e2 100644 --- a/rs/ethereum/cketh/test_utils/Cargo.toml +++ b/rs/ethereum/cketh/test_utils/Cargo.toml @@ -13,22 +13,18 @@ ethers-core = { workspace = true } evm_rpc_types = { workspace = true } hex = { workspace = true } ic-base-types = { path = "../../../types/base_types" } -ic-cdk = { workspace = true } ic-cketh-minter = { path = "../minter" } -ic-error-types = { path = "../../../../packages/ic-error-types" } ic-ethereum-types = { path = "../../../../packages/ic-ethereum-types" } ic-http-types = { path = "../../../../packages/ic-http-types" } ic-icrc1-ledger = { path = "../../../ledger_suite/icrc1/ledger" } ic-ledger-suite-orchestrator = { path = "../../ledger-suite-orchestrator" } ic-ledger-suite-orchestrator-test-utils = { path = "../../ledger-suite-orchestrator/test_utils" } -ic-management-canister-types-private = { path = "../../../types/management_canister_types" } -ic-metrics-assert = { path = "../../../../packages/ic-metrics-assert" } -ic-state-machine-tests = { path = "../../../state_machine_tests" } +ic-management-canister-types = { workspace = true } +ic-metrics-assert = { path = "../../../../packages/ic-metrics-assert", features = ["pocket_ic"] } ic-test-utilities-load-wasm = { path = "../../../test_utilities/load_wasm" } -ic-types-cycles = { path = "../../../types/cycles" } -ic-types = { path = "../../../types/types" } icrc-ledger-types = { path = "../../../../packages/icrc-ledger-types" } num-traits = { workspace = true } +pocket-ic = { path = "../../../../packages/pocket-ic" } serde = { workspace = true } serde_bytes = { workspace = true } serde_json = { workspace = true } diff --git a/rs/ethereum/cketh/test_utils/src/ckerc20.rs b/rs/ethereum/cketh/test_utils/src/ckerc20.rs index 111fc47a758b..db83145ecebf 100644 --- a/rs/ethereum/cketh/test_utils/src/ckerc20.rs +++ b/rs/ethereum/cketh/test_utils/src/ckerc20.rs @@ -11,14 +11,14 @@ use crate::{ CkEthSetup, DEFAULT_DEPOSIT_FROM_ADDRESS, DEFAULT_ERC20_DEPOSIT_LOG_INDEX, DEFAULT_ERC20_DEPOSIT_TRANSACTION_HASH, DEFAULT_PRINCIPAL_ID, DEPOSIT_WITH_SUBACCOUNT_HELPER_CONTRACT_ADDRESS, ERC20_HELPER_CONTRACT_ADDRESS, - ETH_HELPER_CONTRACT_ADDRESS, LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL, LedgerBalance, MAX_TICKS, + ETH_HELPER_CONTRACT_ADDRESS, LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL, LedgerBalance, RECEIVED_ERC20_EVENT_TOPIC, RECEIVED_ETH_OR_ERC20_WITH_SUBACCOUNT_EVENT_TOPIC, assert_reply, - format_ethereum_address_to_eip_55, new_state_machine, + format_ethereum_address_to_eip_55, new_pocket_ic, }; use assert_matches::assert_matches; use candid::{Decode, Encode, Nat, Principal}; use evm_rpc_types::Hex32; -use ic_base_types::{CanisterId, PrincipalId}; +use ic_base_types::PrincipalId; use ic_cketh_minter::SCRAPING_ETH_LOGS_INTERVAL; use ic_cketh_minter::endpoints::ckerc20::{ RetrieveErc20Request, WithdrawErc20Arg, WithdrawErc20Error, @@ -31,11 +31,12 @@ use ic_cketh_minter::numeric::{BlockNumber, Erc20Value}; use ic_ethereum_types::Address; pub use ic_ledger_suite_orchestrator::candid::AddErc20Arg as Erc20Token; use ic_ledger_suite_orchestrator::candid::InitArg as LedgerSuiteOrchestratorInitArg; -use ic_ledger_suite_orchestrator_test_utils::{LedgerSuiteOrchestrator, supported_erc20_tokens}; -use ic_state_machine_tests::{ErrorCode, StateMachine, WasmResult}; -use ic_types::messages::MessageId; +use ic_ledger_suite_orchestrator_test_utils::pocket_ic::LedgerSuiteOrchestrator; +use ic_ledger_suite_orchestrator_test_utils::supported_erc20_tokens; use icrc_ledger_types::icrc1::account::Account; use num_traits::ToPrimitive; +use pocket_ic::common::rest::RawMessageId; +use pocket_ic::{ErrorCode, PocketIc}; use serde_json::json; use std::collections::{BTreeMap, BTreeSet}; use std::convert::identity; @@ -51,7 +52,7 @@ pub const ONE_USDC: u64 = 1_000_000; //6 decimals pub const TWO_USDC: u64 = 2_000_000; //6 decimals pub struct CkErc20Setup { - pub env: Arc, + pub env: Arc, pub cketh: CkEthSetup, pub orchestrator: LedgerSuiteOrchestrator, pub supported_erc20_tokens: Vec, @@ -59,7 +60,7 @@ pub struct CkErc20Setup { impl Default for CkErc20Setup { fn default() -> Self { - Self::new(Arc::new(new_state_machine())) + Self::new(Arc::new(new_pocket_ic())) } } @@ -70,28 +71,24 @@ impl AsRef for CkErc20Setup { } impl CkErc20Setup { - pub fn new(env: Arc) -> Self { + pub fn new(env: Arc) -> Self { let mut ckerc20 = Self::new_without_ckerc20_active(env); ckerc20.cketh = ckerc20 .cketh .upgrade_minter_to_add_orchestrator_id( - ckerc20 - .orchestrator - .ledger_suite_orchestrator_id - .get_ref() - .0, + ckerc20.orchestrator.ledger_suite_orchestrator_id, ) .upgrade_minter_to_add_erc20_helper_contract(ERC20_HELPER_CONTRACT_ADDRESS.to_string()); ckerc20 } - pub fn new_without_ckerc20_active(env: Arc) -> Self { + pub fn new_without_ckerc20_active(env: Arc) -> Self { let cketh = CkEthSetup::new(env.clone()); let orchestrator = LedgerSuiteOrchestrator::new( env.clone(), LedgerSuiteOrchestratorInitArg { more_controller_ids: vec![], - minter_id: Some(cketh.minter_id.get_ref().0), + minter_id: Some(cketh.minter_id), cycles_management: None, }, ) @@ -222,34 +219,25 @@ impl CkErc20Setup { } pub fn stop_ckerc20_ledger(&self, ledger_id: Principal) { - let stop_res = self.env.stop_canister_as( - self.orchestrator.ledger_suite_orchestrator_id.get(), - CanisterId::unchecked_from_principal(ledger_id.into()), - ); - assert_matches!( - stop_res, - Ok(WasmResult::Reply(_)), - "Failed to stop ckERC20 ledger" - ); + self.env + .stop_canister( + ledger_id, + Some(self.orchestrator.ledger_suite_orchestrator_id), + ) + .expect("Failed to stop ckERC20 ledger"); } pub fn start_ckerc20_ledger(&self, ledger_id: Principal) { - let start_res = self.env.start_canister_as( - self.orchestrator.ledger_suite_orchestrator_id.get(), - CanisterId::unchecked_from_principal(ledger_id.into()), - ); - assert_matches!( - start_res, - Ok(WasmResult::Reply(_)), - "Failed to start ckERC20 ledger" - ); + self.env + .start_canister( + ledger_id, + Some(self.orchestrator.ledger_suite_orchestrator_id), + ) + .expect("Failed to start ckERC20 ledger"); } pub fn balance_of_ledger(&self, ledger_id: Principal, account: impl Into) -> Nat { - self.cketh.balance_of_ledger( - CanisterId::unchecked_from_principal(ledger_id.into()), - account, - ) + self.cketh.balance_of_ledger(ledger_id, account) } pub fn call_cketh_ledger_approve_minter( @@ -274,12 +262,7 @@ impl CkErc20Setup { ) -> Self { self.cketh = self .cketh - .call_ledger_id_approve_minter( - CanisterId::unchecked_from_principal(ledger_id.into()), - from, - amount, - from_subaccount, - ) + .call_ledger_id_approve_minter(ledger_id, from, amount, from_subaccount) .expect_ok(1); self } @@ -304,11 +287,8 @@ impl CkErc20Setup { ledger_id: Principal, ledger_index: T, ) -> LedgerTransactionAssert { - let ledger_transaction = crate::flow::call_ledger_id_get_transaction( - &self.env, - CanisterId::unchecked_from_principal(ledger_id.into()), - ledger_index, - ); + let ledger_transaction = + crate::flow::call_ledger_id_get_transaction(&self.env, ledger_id, ledger_index); LedgerTransactionAssert { setup: self, ledger_transaction, @@ -337,12 +317,15 @@ impl CkErc20Setup { from: Principal, withdraw_erc20_arg: WithdrawErc20Arg, ) -> RefreshGasFeeEstimate { - let message_id = self.env.send_ingress( - PrincipalId::from(from), - self.cketh.minter_id, - "withdraw_erc20", - Encode!(&withdraw_erc20_arg).expect("failed to encode withdraw args"), - ); + let message_id = self + .env + .submit_call( + self.cketh.minter_id, + from, + "withdraw_erc20", + Encode!(&withdraw_erc20_arg).expect("failed to encode withdraw args"), + ) + .expect("failed to submit withdraw_erc20 call"); RefreshGasFeeEstimate { setup: self, message_id, @@ -365,24 +348,31 @@ impl CkErc20Setup { from: Principal, deposit_erc20_arg: DepositErc20Arg, ) -> DepositErc20Flow { - let message_id = self.env.send_ingress( - PrincipalId::from(from), - self.cketh.minter_id, - "deposit_erc20", - Encode!(&deposit_erc20_arg).expect("failed to encode deposit_erc20 args"), - ); + let message_id = self + .env + .submit_call( + self.cketh.minter_id, + from, + "deposit_erc20", + Encode!(&deposit_erc20_arg).expect("failed to encode deposit_erc20 args"), + ) + .expect("failed to submit deposit_erc20 call"); DepositErc20Flow { setup: self, message_id, } } + pub fn advance_time(&self, duration: Duration) { + self.cketh.advance_time(duration); + } + pub fn caller(&self) -> Principal { self.cketh.caller.into() } pub fn cketh_ledger_id(&self) -> Principal { - self.cketh.ledger_id.get_ref().0 + self.cketh.ledger_id } pub fn find_ckerc20_token(&self, token_symbol: &str) -> CkErc20Token { @@ -794,8 +784,9 @@ impl CkErc20DepositFlow { let max_eth_logs_block_range = self.as_ref().max_logs_block_range(); let latest_finalized_block = LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + 1 + max_eth_logs_block_range; - self.setup.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + self.setup.advance_time(SCRAPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) + .with_request_params(json!(["finalized", false])) .respond_for_all_with(block_response(latest_finalized_block)) .build() .expect_rpc_calls(self); @@ -906,7 +897,7 @@ impl CkErc20DepositFlow { pub struct RefreshGasFeeEstimate { pub setup: CkErc20Setup, - pub message_id: MessageId, + pub message_id: RawMessageId, } impl RefreshGasFeeEstimate { @@ -946,16 +937,13 @@ impl RefreshGasFeeEstimate { pub struct Erc20WithdrawalFlow { pub setup: CkErc20Setup, - pub message_id: MessageId, + pub message_id: RawMessageId, } impl Erc20WithdrawalFlow { pub fn expect_trap(self, error_substring: &str) -> CkErc20Setup { - let result = self - .setup - .env - .await_ingress(self.message_id.clone(), MAX_TICKS); - assert_matches!(result, Err(e) if e.code() == ErrorCode::CanisterCalledTrap && e.description().contains(error_substring)); + let result = self.setup.env.await_call(self.message_id.clone()); + assert_matches!(result, Err(e) if e.error_code == ErrorCode::CanisterCalledTrap && e.reject_message.contains(error_substring)); self.setup } @@ -982,27 +970,23 @@ impl Erc20WithdrawalFlow { #[allow(clippy::result_large_err)] fn minter_response(&self) -> Result { - Decode!(&assert_reply( - self.setup.env - .await_ingress(self.message_id.clone(), MAX_TICKS) - .expect("failed to resolve message with id: {message_id}"), - ), Result) + Decode!( + &assert_reply(self.setup.env.await_call(self.message_id.clone())), + Result + ) .unwrap() } } pub struct DepositErc20Flow { pub setup: CkErc20Setup, - pub message_id: MessageId, + pub message_id: RawMessageId, } impl DepositErc20Flow { pub fn expect_trap(self, error_substring: &str) -> CkErc20Setup { - let result = self - .setup - .env - .await_ingress(self.message_id.clone(), MAX_TICKS); - assert_matches!(result, Err(e) if e.code() == ErrorCode::CanisterCalledTrap && e.description().contains(error_substring)); + let result = self.setup.env.await_call(self.message_id.clone()); + assert_matches!(result, Err(e) if e.error_code == ErrorCode::CanisterCalledTrap && e.reject_message.contains(error_substring)); self.setup } @@ -1024,12 +1008,7 @@ impl DepositErc20Flow { fn minter_response(&self) -> Result { Decode!( - &assert_reply( - self.setup - .env - .await_ingress(self.message_id.clone(), MAX_TICKS) - .expect("failed to resolve message with id: {message_id}") - ), + &assert_reply(self.setup.env.await_call(self.message_id.clone())), Result ) .unwrap() diff --git a/rs/ethereum/cketh/test_utils/src/flow.rs b/rs/ethereum/cketh/test_utils/src/flow.rs index bc89f88270b3..916e51bb9a3a 100644 --- a/rs/ethereum/cketh/test_utils/src/flow.rs +++ b/rs/ethereum/cketh/test_utils/src/flow.rs @@ -9,13 +9,13 @@ use crate::{ DEFAULT_DEPOSIT_FROM_ADDRESS, DEFAULT_DEPOSIT_LOG_INDEX, DEFAULT_DEPOSIT_TRANSACTION_HASH, DEFAULT_DEPOSIT_TRANSACTION_INDEX, DEFAULT_PRINCIPAL_ID, DEFAULT_USER_SUBACCOUNT, EFFECTIVE_GAS_PRICE, EXPECTED_BALANCE, GAS_USED, JsonRpcProvider, - LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL, MAX_TICKS, MINTER_ADDRESS, RECEIVED_ETH_EVENT_TOPIC, + LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL, MINTER_ADDRESS, RECEIVED_ETH_EVENT_TOPIC, RECEIVED_ETH_OR_ERC20_WITH_SUBACCOUNT_EVENT_TOPIC, assert_reply, format_ethereum_address_to_eip_55, }; use candid::{Decode, Encode, Nat, Principal}; use ethers_core::utils::{hex, rlp}; -use ic_base_types::{CanisterId, PrincipalId}; +use ic_base_types::PrincipalId; use ic_cketh_minter::endpoints::ckerc20::RetrieveErc20Request; use ic_cketh_minter::endpoints::events::{Event, EventPayload, EventSource}; use ic_cketh_minter::endpoints::{ @@ -27,12 +27,12 @@ use ic_cketh_minter::{ SCRAPING_ETH_LOGS_INTERVAL, }; use ic_ethereum_types::Address; -use ic_state_machine_tests::StateMachine; -use ic_types::messages::MessageId; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc2::approve::ApproveError; use icrc_ledger_types::icrc3::transactions::{Burn, Mint, Transaction as LedgerTransaction}; use num_traits::ToPrimitive; +use pocket_ic::PocketIc; +use pocket_ic::common::rest::RawMessageId; use serde_json::json; use std::convert::identity; use std::str::FromStr; @@ -309,7 +309,7 @@ impl DepositFlow { fn updated_balance(&self, balance_before: &Nat) -> Nat { let mut current_balance = balance_before.clone(); for _ in 0..10 { - self.setup.env.advance_time(Duration::from_secs(1)); + self.setup.advance_time(Duration::from_secs(1)); self.setup.env.tick(); current_balance = self.setup.balance_of(self.params.recipient()); if ¤t_balance != balance_before { @@ -331,16 +331,19 @@ impl DepositFlow { let max_eth_logs_block_range = self.setup.max_logs_block_range(); let latest_finalized_block = LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + 1 + max_eth_logs_block_range; - self.setup.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + self.setup.advance_time(SCRAPING_ETH_LOGS_INTERVAL); let default_get_block_by_number = MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) + // Constrained so the block height refresh timer's query for the latest + // block cannot consume this stub. + .with_request_params(json!(["finalized", false])) .respond_for_all_with(block_response(latest_finalized_block)); (self.override_rpc_eth_get_block_by_number)(default_get_block_by_number) .build() .expect_rpc_calls(&self.setup); - self.setup.env.advance_time(SCRAPING_ETH_LOGS_INTERVAL); + self.setup.advance_time(SCRAPING_ETH_LOGS_INTERVAL); match &self.params { DepositParams::CkEth(_) => { @@ -398,8 +401,8 @@ impl LedgerTransactionAssert { } pub fn call_ledger_id_get_transaction>( - env: &StateMachine, - ledger_id: CanisterId, + env: &PocketIc, + ledger_id: Principal, ledger_index: T, ) -> LedgerTransaction { use icrc_ledger_types::icrc3::transactions::{GetTransactionsRequest, GetTransactionsResponse}; @@ -409,10 +412,12 @@ pub fn call_ledger_id_get_transaction>( length: 1_u8.into(), }; let mut response = Decode!( - &assert_reply( - env.query(ledger_id, "get_transactions", Encode!(&request).unwrap()) - .expect("failed to query get_transactions on the ledger") - ), + &assert_reply(env.query_call( + ledger_id, + Principal::anonymous(), + "get_transactions", + Encode!(&request).unwrap() + )), GetTransactionsResponse ) .unwrap(); @@ -452,7 +457,7 @@ impl ApprovalFlow { pub struct WithdrawalFlow { pub(crate) setup: CkEthSetup, - pub(crate) message_id: MessageId, + pub(crate) message_id: RawMessageId, } impl WithdrawalFlow { @@ -478,11 +483,10 @@ impl WithdrawalFlow { } fn minter_response(&self) -> Result { - Decode!(&assert_reply( - self.setup.env - .await_ingress(self.message_id.clone(), MAX_TICKS) - .expect("failed to resolve message with id: {message_id}"), - ), Result) + Decode!( + &assert_reply(self.setup.env.await_call(self.message_id.clone())), + Result + ) .unwrap() } } diff --git a/rs/ethereum/cketh/test_utils/src/lib.rs b/rs/ethereum/cketh/test_utils/src/lib.rs index 68016e42c5e2..a83bc4e804d7 100644 --- a/rs/ethereum/cketh/test_utils/src/lib.rs +++ b/rs/ethereum/cketh/test_utils/src/lib.rs @@ -5,7 +5,7 @@ use crate::flow::{ use crate::mock::JsonRpcMethod; use assert_matches::assert_matches; use candid::{Decode, Encode, Nat, Principal}; -use ic_base_types::{CanisterId, PrincipalId}; +use ic_base_types::PrincipalId; use ic_cketh_minter::endpoints::events::{Event, EventPayload, GetEventsResult}; use ic_cketh_minter::endpoints::{ AddCkErc20Token, DecodeLedgerMemoArgs, DecodeLedgerMemoResult, Eip1559TransactionPriceArg, @@ -21,17 +21,17 @@ use ic_cketh_minter::{ use ic_ethereum_types::Address; use ic_http_types::{HttpRequest, HttpResponse}; use ic_icrc1_ledger::{InitArgsBuilder as LedgerInitArgsBuilder, LedgerArgument}; -use ic_management_canister_types_private::{CanisterHttpResponsePayload, CanisterStatusType}; -use ic_metrics_assert::{CanisterHttpQuery, MetricsAssert}; -use ic_state_machine_tests::{ - PayloadBuilder, StateMachine, StateMachineBuilder, UserError, WasmResult, -}; +use ic_management_canister_types::{CanisterId, CanisterIdRecord, CanisterStatusType}; +use ic_metrics_assert::{MetricsAssert, PocketIcHttpQuery}; use ic_test_utilities_load_wasm::load_wasm; -use ic_types::ingress::{IngressState, IngressStatus}; -use ic_types_cycles::Cycles; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; use num_traits::cast::ToPrimitive; +use pocket_ic::common::rest::{ + CanisterHttpReject, CanisterHttpReply, CanisterHttpRequest, CanisterHttpResponse, IcpConfig, + IcpConfigFlag, MockCanisterHttpResponse, RawEffectivePrincipal, RawMessageId, +}; +use pocket_ic::{PocketIc, PocketIcBuilder, RejectResponse}; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; @@ -52,6 +52,13 @@ mod tests; pub const CKETH_TRANSFER_FEE: u64 = 2_000_000_000_000; pub const CKETH_MINIMUM_WITHDRAWAL_AMOUNT: u64 = 30_000_000_000_000_000; pub const MAX_TICKS: usize = 10; + +// `ic_error_types::RejectCode` value relevant to canister-http mocking. +pub(crate) const REJECT_CODE_SYS_FATAL: u64 = 1; +pub(crate) const REJECT_CODE_SYS_TRANSIENT: u64 = 2; +// ic_types::canister_http::CANISTER_HTTP_TIMEOUT_INTERVAL, past which PocketIC fails +// canister http requests still in flight. +const CANISTER_HTTP_TIMEOUT_INTERVAL: Duration = Duration::from_secs(60); pub const DEFAULT_PRINCIPAL_ID: u64 = 10352385; pub const DEFAULT_USER_SUBACCOUNT: [u8; 32] = [42; 32]; pub const DEFAULT_DEPOSIT_BLOCK_NUMBER: u64 = 0x9; @@ -76,18 +83,18 @@ pub const EFFECTIVE_GAS_PRICE: u64 = 4_277_923_390; pub const GAS_USED: u64 = 0x5208; pub const DEFAULT_WITHDRAWAL_TRANSACTION_HASH: &str = - "0x2cf1763e8ee3990103a31a5709b17b83f167738abb400844e67f608a98b0bdb5"; -pub const DEFAULT_WITHDRAWAL_TRANSACTION: &str = "0x02f87301808459682f008507af2c9f6282520894221e931fbfcb9bd54ddd26ce6f5e29e98add01c0880160cf1e9917a0e680c001a0b27af25a08e87836a778ac2858fdfcff1f6f3a0d43313782c81d05ca34b80271a078026b399a32d3d7abab625388a3c57f651c66a182eb7f8b1a58d9aef7547256"; + "0xa31221e733b030eb72eeb6973593a4d920c2c3391433429eed3c16b8f4f3ba7a"; +pub const DEFAULT_WITHDRAWAL_TRANSACTION: &str = "0x02f87301808459682f008507af2c9f6282520894221e931fbfcb9bd54ddd26ce6f5e29e98add01c0880160cf1e9917a0e680c001a0ad488ebb7c3cdf69ac424ab1e64e52c01fc4f93ac877946f707b0f29199f8a13a01996863aed39f3fca6350d907622c24864f9a04c21d16d4b27d37d8a3531653b"; -pub const DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION: &str = "0x02f8b001808459682f008507af2c9f6282fde894a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4880b844a9059cbb000000000000000000000000221e931fbfcb9bd54ddd26ce6f5e29e98add01c000000000000000000000000000000000000000000000000000000000001e8480c080a0bb694aec6175b489523a55d5fce39452368e97096d4afa2cdcc35cf2d805152fa00112b26a028af84dd397d23549844efdaf761d90cdcfdbe6c3608239648a85a3"; +pub const DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION: &str = "0x02f8b001808459682f008507af2c9f6282fde894a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4880b844a9059cbb000000000000000000000000221e931fbfcb9bd54ddd26ce6f5e29e98add01c000000000000000000000000000000000000000000000000000000000001e8480c080a0da4f476ede0aaf7da633371a938d5e2525a65a23699b55761779871a313f8cb3a045833d409eba50e3e9b145d04ea294ee791c14465503818f8b325a881938ddc1"; pub const DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION_HASH: &str = - "0x2c0c328876b8d60580e00d8e5a82599e22099e78d9d9c25cc5e6164bc8f4db62"; + "0x31d6e7ad4b7c167ca17777fd4aafd11a02c9fd8d3bc660f7ea4d7a2e2bf4a985"; pub const DEFAULT_CKERC20_WITHDRAWAL_TRANSACTION_FEE: u64 = 2_145_241_036_770_000_u64; pub const USDC_ERC20_CONTRACT_ADDRESS: &str = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; pub const USDC_ERC20_CONTRACT_ADDRESS_LOWERCASE: &str = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; -pub const MINTER_ADDRESS: &str = "0xfd644a761079369962386f8e4259217c2a10b8d0"; +pub const MINTER_ADDRESS: &str = "0x30a14171b7c4c93ff5213f82eeb74f7c7e3f1ebc"; pub const DEFAULT_WITHDRAWAL_DESTINATION_ADDRESS: &str = "0x221E931fbFcb9bd54DdD26cE6f5e29E98AdD01C0"; pub const ETH_HELPER_CONTRACT_ADDRESS: &str = "0x907b6efc1a398fd88a8161b3ca02eec8eaf72ca1"; @@ -103,17 +110,17 @@ const RECEIVED_ETH_OR_ERC20_WITH_SUBACCOUNT_EVENT_TOPIC: &str = pub const HEADER_SIZE_LIMIT: u64 = 2 * 1024; pub struct CkEthSetup { - pub env: Arc, + pub env: Arc, pub caller: PrincipalId, - pub ledger_id: CanisterId, - pub minter_id: CanisterId, - pub evm_rpc_id: CanisterId, + pub ledger_id: Principal, + pub minter_id: Principal, + pub evm_rpc_id: Principal, pub support_subaccount: bool, } impl Default for CkEthSetup { fn default() -> Self { - Self::new(Arc::new(new_state_machine())) + Self::new(Arc::new(new_pocket_ic())) } } @@ -123,28 +130,32 @@ impl AsRef for CkEthSetup { } } -impl CanisterHttpQuery for &CkEthSetup { - fn http_query(&self, request: Vec) -> Result, UserError> { - self.env - .query(self.minter_id, "http_request", request) - .map(assert_reply) +impl PocketIcHttpQuery for &CkEthSetup { + fn get_pocket_ic(&self) -> &PocketIc { + &self.env + } + + fn get_canister_id(&self) -> CanisterId { + self.minter_id } } impl CkEthSetup { - pub fn new(env: Arc) -> Self { + pub fn new(env: Arc) -> Self { // Create minter canister first to match canister ID and Ethereum address hardcoded in tests. - let minter_id = - env.create_canister_with_cycles(None, Cycles::new(100_000_000_000_000), None); - let ledger_id = env.create_canister(None); - let evm_rpc_id = env.create_canister(None); - - env.install_existing_canister( + 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.get().0) + .with_minting_account(minter_id) .with_transfer_fee(CKETH_TRANSFER_FEE) .with_max_memo_length(80) .with_decimals(18) @@ -155,27 +166,20 @@ impl CkEthSetup { .build(), )) .unwrap(), - ) - .unwrap(); + None, + ); install_evm_rpc(&env, evm_rpc_id); let minter_id = install_minter(&env, ledger_id, minter_id, evm_rpc_id); let caller = PrincipalId::new_user_test_id(DEFAULT_PRINCIPAL_ID); - let cketh = Self { + Self { env, caller, ledger_id, minter_id, evm_rpc_id, support_subaccount: false, - }; - - assert_eq!( - Address::from_str(MINTER_ADDRESS).unwrap(), - Address::from_str(&cketh.minter_address()).unwrap() - ); - - cketh + } } pub fn add_support_for_subaccount(self) -> Self { @@ -190,16 +194,12 @@ impl CkEthSetup { pub fn minter_address(&self) -> String { Decode!( - &assert_reply( - self.env - .execute_ingress_as( - self.caller, - self.minter_id, - "minter_address", - Encode!().unwrap(), - ) - .expect("failed to get eth address") - ), + &assert_reply(self.env.update_call( + self.minter_id, + self.caller.into(), + "minter_address", + Encode!().unwrap(), + )), String ) .unwrap() @@ -207,16 +207,12 @@ impl CkEthSetup { pub fn retrieve_eth_status(&self, block_index: &Nat) -> RetrieveEthStatus { Decode!( - &assert_reply( - self.env - .execute_ingress_as( - self.caller, - self.minter_id, - "retrieve_eth_status", - Encode!(&block_index.0.to_u64().unwrap()).unwrap(), - ) - .expect("failed to get eth address") - ), + &assert_reply(self.env.update_call( + self.minter_id, + self.caller.into(), + "retrieve_eth_status", + Encode!(&block_index.0.to_u64().unwrap()).unwrap(), + )), RetrieveEthStatus ) .unwrap() @@ -227,16 +223,12 @@ impl CkEthSetup { parameter: &WithdrawalSearchParameter, ) -> Vec { Decode!( - &assert_reply( - self.env - .query_as( - self.caller, - self.minter_id, - "withdrawal_status", - Encode!(parameter).unwrap(), - ) - .expect("failed to get eth address") - ), + &assert_reply(self.env.query_call( + self.minter_id, + self.caller.into(), + "withdrawal_status", + Encode!(parameter).unwrap(), + )), Vec ) .unwrap() @@ -247,17 +239,14 @@ impl CkEthSetup { self.balance_of_ledger(ledger_id, account) } - pub fn balance_of_ledger(&self, ledger_id: CanisterId, account: impl Into) -> Nat { + pub fn balance_of_ledger(&self, ledger_id: Principal, account: impl Into) -> Nat { Decode!( - &assert_reply( - self.env - .query( - ledger_id, - "icrc1_balance_of", - Encode!(&account.into()).unwrap() - ) - .expect("failed to query balance on the ledger") - ), + &assert_reply(self.env.query_call( + ledger_id, + Principal::anonymous(), + "icrc1_balance_of", + Encode!(&account.into()).unwrap() + )), Nat ) .unwrap() @@ -266,15 +255,19 @@ impl CkEthSetup { pub fn eip_1559_transaction_price( &self, ledger_id: Option, - ) -> Result { + ) -> Result, RejectResponse> { let arg = match ledger_id { None => Encode!().unwrap(), Some(ckerc20_ledger_id) => { Encode!(&Some(Eip1559TransactionPriceArg { ckerc20_ledger_id })).unwrap() } }; - self.env - .query(self.minter_id, "eip_1559_transaction_price", arg) + self.env.query_call( + self.minter_id, + Principal::anonymous(), + "eip_1559_transaction_price", + arg, + ) } pub fn eip_1559_transaction_price_expecting_ok( @@ -282,7 +275,7 @@ impl CkEthSetup { ledger_id: Option, ) -> Eip1559TransactionPrice { Decode!( - &assert_reply(self.eip_1559_transaction_price(ledger_id).unwrap()), + &assert_reply(self.eip_1559_transaction_price(ledger_id)), Eip1559TransactionPrice ) .unwrap() @@ -292,7 +285,7 @@ impl CkEthSetup { let error = self .eip_1559_transaction_price(Some(principal_id)) .expect_err("Expecting Err but got Ok"); - assert!(error.description().contains(&format!( + assert!(error.reject_message.contains(&format!( "ERROR: Unsupported ckERC20 token ledger {principal_id}" ))); } @@ -301,31 +294,28 @@ impl CkEthSetup { &self, from: Principal, erc20: &AddCkErc20Token, - ) -> Result { - self.env.execute_ingress_as( - PrincipalId::from(from), + ) -> Result, RejectResponse> { + self.env.update_call( self.minter_id, + from, "add_ckerc20_token", Encode!(erc20).unwrap(), ) } pub fn add_ckerc20_token_expecting_ok(self, from: Principal, erc20: &AddCkErc20Token) -> Self { - Decode!( - &assert_reply(self.add_ckerc20_token(from, erc20).unwrap()), - () - ) - .unwrap(); + Decode!(&assert_reply(self.add_ckerc20_token(from, erc20)), ()).unwrap(); self } pub fn get_minter_info(&self) -> MinterInfo { Decode!( - &assert_reply( - self.env - .query(self.minter_id, "get_minter_info", Encode!().unwrap()) - .unwrap() - ), + &assert_reply(self.env.query_call( + self.minter_id, + Principal::anonymous(), + "get_minter_info", + Encode!().unwrap() + )), MinterInfo ) .unwrap() @@ -343,19 +333,19 @@ impl CkEthSetup { pub fn call_ledger_id_approve_minter( self, - ledger_id: CanisterId, + ledger_id: Principal, from: Principal, amount: u64, from_subaccount: Option<[u8; 32]>, ) -> ApprovalFlow { - let approval_response = Decode!(&assert_reply(self.env.execute_ingress_as( - PrincipalId::from(from), + let approval_response = Decode!(&assert_reply(self.env.update_call( ledger_id, + from, "icrc2_approve", Encode!(&ApproveArgs { from_subaccount, spender: Account { - owner: self.minter_id.into(), + owner: self.minter_id, subaccount: None }, amount: Nat::from(amount), @@ -365,7 +355,7 @@ impl CkEthSetup { memo: None, created_at_time: None, }).unwrap() - ).expect("failed to execute token transfer")), + )), Result ) .unwrap(); @@ -385,7 +375,7 @@ impl CkEthSetup { pub fn call_ledger_id_get_transaction>( self, - ledger_id: CanisterId, + ledger_id: Principal, ledger_index: T, ) -> LedgerTransactionAssert { let ledger_transaction = @@ -408,12 +398,15 @@ impl CkEthSetup { recipient, from_subaccount: from.subaccount, }; - let message_id = self.env.send_ingress( - PrincipalId::from(from.owner), - self.minter_id, - "withdraw_eth", - Encode!(&arg).expect("failed to encode withdraw args"), - ); + let message_id = self + .env + .submit_call( + self.minter_id, + from.owner, + "withdraw_eth", + Encode!(&arg).expect("failed to encode withdraw args"), + ) + .expect("failed to submit withdraw_eth call"); WithdrawalFlow { setup: self, message_id, @@ -428,11 +421,12 @@ impl CkEthSetup { body: serde_bytes::ByteBuf::new(), }; let response = Decode!( - &assert_reply( - self.env - .query(self.minter_id, "http_request", Encode!(&request).unwrap(),) - .expect("failed to get minter info") - ), + &assert_reply(self.env.query_call( + self.minter_id, + Principal::anonymous(), + "http_request", + Encode!(&request).unwrap(), + )), HttpResponse ) .unwrap(); @@ -463,15 +457,12 @@ impl CkEthSetup { use ic_cketh_minter::endpoints::events::GetEventsArg; Decode!( - &assert_reply( - self.env - .execute_ingress( - self.minter_id, - "get_events", - Encode!(&GetEventsArg { start, length }).unwrap(), - ) - .expect("failed to get minter info") - ), + &assert_reply(self.env.update_call( + self.minter_id, + Principal::anonymous(), + "get_events", + Encode!(&GetEventsArg { start, length }).unwrap(), + )), GetEventsResult ) .unwrap() @@ -493,11 +484,12 @@ impl CkEthSetup { fn check_audit_log(&self) { Decode!( - &assert_reply( - self.env - .query(self.minter_id, "check_audit_log", Encode!().unwrap()) - .unwrap(), - ), + &assert_reply(self.env.query_call( + self.minter_id, + Principal::anonymous(), + "check_audit_log", + Encode!().unwrap() + )), () ) .unwrap() @@ -510,29 +502,39 @@ impl CkEthSetup { self.minter_id, minter_wasm(), Encode!(&MinterArg::UpgradeArg(upgrade_arg)).unwrap(), + None, ) .unwrap(); self.start_minter(); } + pub fn submit_stop_minter(&self) -> RawMessageId { + self.env + .submit_call_with_effective_principal( + Principal::management_canister(), + RawEffectivePrincipal::CanisterId(self.minter_id.as_slice().to_vec()), + Principal::anonymous(), + "stop_canister", + Encode!(&CanisterIdRecord { + canister_id: self.minter_id + }) + .unwrap(), + ) + .expect("failed to submit stop_canister call") + } + /// Try to stop the minter without first stopping the ongoing HTTPS outcalls. Assert that the - /// `IngressStatus` is `Processing`. + /// call is still processing (i.e. blocked on the open call contexts for those outcalls). pub fn try_stop_minter_without_stopping_ongoing_https_outcalls(&self) { - const MAX_TICKS: u64 = 10; - let stop_msg_id = self.env.stop_canister_non_blocking(self.minter_id); - let mut ingress_status = self.env.ingress_status(&stop_msg_id); - for _ in 0..MAX_TICKS { - if let IngressStatus::Known { state, .. } = &ingress_status - && state == &IngressState::Processing - { - return; - } - self.env.tick(); - ingress_status = self.env.ingress_status(&stop_msg_id); - } - panic!( - "expected minter ingress status to be `Processing`, ended up with {:?}", - ingress_status + let stop_msg_id = self.submit_stop_minter(); + // The stop request only takes effect in the next round, and the outcalls the + // minter's timers issue in that last running round become visible only after + // it, so a drain placed before this tick would run too early and leave them + // pending forever. + self.env.tick(); + assert!( + self.env.ingress_status(stop_msg_id).is_none(), + "expected the minter's stop_canister call to still be processing after one tick" ); } @@ -543,47 +545,63 @@ impl CkEthSetup { if status == expected_canister_status { break; } + self.env.tick(); status = self.minter_status(); } assert_eq!(status, expected_canister_status); } pub fn stop_minter(&self) { - let stop_msg_id = self.env.stop_canister_non_blocking(self.minter_id); + let stop_msg_id = self.submit_stop_minter(); + self.env.tick(); self.stop_ongoing_https_outcalls(); - let stop_res = self.env.await_ingress(stop_msg_id, 100); - assert_matches!(stop_res, Ok(WasmResult::Reply(_))); + let stop_res = self.env.await_call(stop_msg_id); + assert_matches!(stop_res, Ok(_)); } pub fn stop_ongoing_https_outcalls(&self) { - let server_error_response = CanisterHttpResponsePayload { - status: 500_u128, - headers: vec![], - body: vec![], - }; - let ongoing_https_outcalls: Vec<_> = self - .env - .canister_http_request_contexts() - .into_keys() - .collect(); - let mut payload = PayloadBuilder::new(); - for callback_id in ongoing_https_outcalls { - payload = payload.http_response(callback_id, &server_error_response); + for request in self.env.get_canister_http() { + reply_500(&self.env, &request); } - self.env.execute_payload(payload); + self.env.tick(); } pub fn start_minter(&self) { - let start_res = self.env.start_canister(self.minter_id); - assert_matches!(start_res, Ok(WasmResult::Reply(_))); + let start_res = self.env.start_canister(self.minter_id, None); + assert_matches!(start_res, Ok(())); + } + + /// Advancing past the canister http timeout fails every outcall in flight, but the + /// requests stay listed until a round processes the failures, and the cycle awaiting + /// them keeps holding its TimerGuard until it learns of them. A stub could then bind + /// to a request that is already dead, and the firing due at the new time would be + /// dropped as AlreadyProcessing. Do to those outcalls up front, and deterministically, + /// what the advance would have done to them anyway. + pub fn advance_time(&self, duration: Duration) { + if duration > CANISTER_HTTP_TIMEOUT_INTERVAL { + self.fail_pending_https_outcalls(); + } + self.env.advance_time(duration); + } + + fn fail_pending_https_outcalls(&self) { + let pending = self.env.get_canister_http(); + // Ticking with nothing in flight would let the timers of a fixture that has + // executed no round fire early, which the flows rely on doing themselves. + if pending.is_empty() { + return; + } + for request in &pending { + fail_as_timed_out(&self.env, request); + } + self.env.tick(); } pub fn minter_status(&self) -> CanisterStatusType { self.env - .canister_status(self.minter_id) - .unwrap() + .canister_status(self.minter_id, None) .unwrap() - .status() + .status } pub fn upgrade_minter_to_add_orchestrator_id(self, orchestrator_id: Principal) -> Self { @@ -629,11 +647,11 @@ impl CkEthSetup { for _ in 0..MAX_TICKS { if let Some(unexpected_request) = self .env - .canister_http_request_contexts() - .values() - .map(|context| { + .get_canister_http() + .into_iter() + .map(|request| { crate::mock::JsonRpcRequest::from_str( - std::str::from_utf8(&context.body.clone().unwrap()).unwrap(), + std::str::from_utf8(&request.body).unwrap(), ) .expect("BUG: invalid JSON RPC method") }) @@ -693,17 +711,16 @@ impl CkEthSetup { ) -> DecodeLedgerMemoResult { Decode!( &assert_reply( - self.env - .query( - self.minter_id, - "decode_ledger_memo", - Encode!(&DecodeLedgerMemoArgs { - memo_type, - encoded_memo - }) - .unwrap() - ) - .expect("failed to call decode_ledger_memo") + self.env.query_call( + self.minter_id, + Principal::anonymous(), + "decode_ledger_memo", + Encode!(&DecodeLedgerMemoArgs { + memo_type, + encoded_memo + }) + .unwrap() + ) ), DecodeLedgerMemoResult ) @@ -711,10 +728,12 @@ impl CkEthSetup { } pub fn minter_canister_logs(&self) -> Vec { - let log = self.env.canister_log(self.minter_id); + let mut records = self + .env + .fetch_canister_logs(self.minter_id, Principal::anonymous()) + .expect("failed to fetch canister logs"); - let mut records = log.records().iter().collect::>(); - records.sort_by_key(|a| a.idx); + records.sort_by_key(|record| record.idx); records .into_iter() .map(|log| CanisterLog { @@ -730,10 +749,13 @@ pub fn format_ethereum_address_to_eip_55(address: &str) -> String { Address::from_str(address).unwrap().to_string() } -fn new_state_machine() -> StateMachine { - StateMachineBuilder::new() - .with_master_ecdsa_public_key() - .with_default_canister_range() +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() } @@ -768,41 +790,64 @@ fn evm_rpc_wasm() -> Vec { } fn install_minter( - env: &StateMachine, - ledger_id: CanisterId, - minter_id: CanisterId, - evm_rpc_id: CanisterId, -) -> CanisterId { + env: &PocketIc, + ledger_id: Principal, + minter_id: Principal, + evm_rpc_id: Principal, +) -> Principal { let args = MinterInitArgs { - ecdsa_key_name: "master_ecdsa_public_key".parse().unwrap(), + ecdsa_key_name: "key_1".parse().unwrap(), ethereum_network: EthereumNetwork::Mainnet, - ledger_id: ledger_id.get().0, + ledger_id, next_transaction_nonce: 0_u8.into(), ethereum_block_height: CandidBlockTag::Finalized, 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.into()), + evm_rpc_id: Some(evm_rpc_id), }; let minter_arg = MinterArg::InitArg(args); - env.install_existing_canister(minter_id, minter_wasm(), Encode!(&minter_arg).unwrap()) - .unwrap(); + env.install_canister( + minter_id, + minter_wasm(), + Encode!(&minter_arg).unwrap(), + None, + ); minter_id } -fn install_evm_rpc(env: &StateMachine, evm_rpc_id: CanisterId) { +fn install_evm_rpc(env: &PocketIc, evm_rpc_id: Principal) { let args = evm_rpc_types::InstallArgs::default(); - env.install_existing_canister(evm_rpc_id, evm_rpc_wasm(), Encode!(&args).unwrap()) - .unwrap(); + env.install_canister(evm_rpc_id, evm_rpc_wasm(), Encode!(&args).unwrap(), None); } -fn assert_reply(result: WasmResult) -> Vec { - match result { - WasmResult::Reply(bytes) => bytes, - WasmResult::Reject(reject) => { - panic!("Expected a successful reply, got a reject: {reject}") - } - } +fn fail_as_timed_out(env: &PocketIc, request: &CanisterHttpRequest) { + env.mock_canister_http_response(MockCanisterHttpResponse { + subnet_id: request.subnet_id, + request_id: request.request_id, + response: CanisterHttpResponse::CanisterHttpReject(CanisterHttpReject { + reject_code: REJECT_CODE_SYS_TRANSIENT, + message: "Canister http request timed out".to_string(), + }), + additional_responses: vec![], + }); +} + +fn reply_500(env: &PocketIc, request: &CanisterHttpRequest) { + env.mock_canister_http_response(MockCanisterHttpResponse { + subnet_id: request.subnet_id, + request_id: request.request_id, + response: CanisterHttpResponse::CanisterHttpReply(CanisterHttpReply { + status: 500, + headers: vec![], + body: vec![], + }), + additional_responses: vec![], + }); +} + +fn assert_reply(result: Result, RejectResponse>) -> Vec { + result.unwrap_or_else(|reject| panic!("Expected a successful reply, got a reject: {reject}")) } pub struct LedgerBalance { diff --git a/rs/ethereum/cketh/test_utils/src/mock.rs b/rs/ethereum/cketh/test_utils/src/mock.rs index d312e6dda086..2ba709b9c337 100644 --- a/rs/ethereum/cketh/test_utils/src/mock.rs +++ b/rs/ethereum/cketh/test_utils/src/mock.rs @@ -1,11 +1,9 @@ -use crate::{CkEthSetup, JsonRpcProvider, MAX_TICKS, assert_reply}; -use candid::{Decode, Encode}; -use ic_cdk::management_canister::{HttpRequestResult as OutCallHttpResponse, TransformArgs}; -use ic_error_types::RejectCode; -use ic_management_canister_types_private::CanisterHttpResponsePayload; -use ic_state_machine_tests::{PayloadBuilder, StateMachine}; -use ic_types::canister_http::{CanisterHttpMethod, CanisterHttpRequestContext}; -use ic_types::messages::CallbackId; +use crate::{CkEthSetup, JsonRpcProvider, MAX_TICKS}; +use pocket_ic::PocketIc; +use pocket_ic::common::rest::{ + CanisterHttpMethod, CanisterHttpReject, CanisterHttpReply, CanisterHttpRequest, + CanisterHttpResponse, MockCanisterHttpResponse, +}; use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::json; @@ -15,7 +13,7 @@ use std::time::Duration; use strum::IntoEnumIterator; trait Matcher { - fn matches(&self, context: &CanisterHttpRequestContext) -> bool; + fn matches(&self, request: &CanisterHttpRequest) -> bool; } pub struct MockJsonRpcProviders { @@ -110,12 +108,12 @@ impl JsonRpcRequestMatcher { self } - fn tick_until_next_http_request(&self, env: &StateMachine) { + fn tick_until_next_http_request(&self, env: &PocketIc) { for _ in 0..MAX_TICKS { let has_matching_request = env - .canister_http_request_contexts() - .values() - .any(|context| self.matches(context)); + .get_canister_http() + .iter() + .any(|request| self.matches(request)); if has_matching_request { break; } @@ -124,38 +122,31 @@ impl JsonRpcRequestMatcher { } } - pub fn find_rpc_call( - &self, - env: &StateMachine, - ) -> Option<(CallbackId, CanisterHttpRequestContext)> { + pub fn find_rpc_call(&self, env: &PocketIc) -> Option { self.tick_until_next_http_request(env); - env.canister_http_request_contexts() + env.get_canister_http() .into_iter() - .find(|(_id, context)| self.matches(context)) + .find(|request| self.matches(request)) } } impl Matcher for JsonRpcRequestMatcher { - fn matches(&self, context: &CanisterHttpRequestContext) -> bool { - let has_json_content_type_header = context.headers.iter().any(|header| { + fn matches(&self, request: &CanisterHttpRequest) -> bool { + let has_json_content_type_header = request.headers.iter().any(|header| { header.name.to_lowercase() == "content-type" && header.value == "application/json" }); let has_expected_max_response_bytes = - match (self.max_response_bytes, context.max_response_bytes) { - (Some(expected), Some(actual)) => expected == actual.get(), + match (self.max_response_bytes, request.max_response_bytes) { + (Some(expected), Some(actual)) => expected == actual, (Some(_), None) => false, (None, _) => true, }; - let request_body = context - .body - .as_ref() - .map(|body| std::str::from_utf8(body).unwrap()) - .expect("BUG: missing request body"); + let request_body = std::str::from_utf8(&request.body).unwrap(); let json_rpc_request = JsonRpcRequest::from_str(request_body).expect("BUG: invalid JSON RPC request"); - self.http_method == context.http_method - && self.provider.url() == context.url + self.http_method == request.http_method + && self.provider.url() == request.url && has_expected_max_response_bytes && has_json_content_type_header && self.json_rpc_method == json_rpc_request.method @@ -174,14 +165,17 @@ struct StubOnce { } impl StubOnce { - fn expect_no_matching_rpc_call(self, env: &StateMachine) { - if let Some((id, _)) = self.matcher.find_rpc_call(env) { - panic!("expect no request matching the stub {self:?} but found one {id}"); + fn expect_no_matching_rpc_call(self, env: &PocketIc) { + if let Some(request) = self.matcher.find_rpc_call(env) { + panic!( + "expect no request matching the stub {self:?} but found one {}", + request.request_id + ); } } - fn expect_rpc_call(self, env: &StateMachine) { - let (id, context) = self.matcher.find_rpc_call(env).unwrap_or_else(|| { + fn expect_rpc_call(self, env: &PocketIc) { + let request = self.matcher.find_rpc_call(env).unwrap_or_else(|| { panic!( "no request found matching the stub {:?}. Current requests {}", self, @@ -189,11 +183,7 @@ impl StubOnce { ) }); let request_id = { - let request_body = context - .body - .as_ref() - .map(|body| std::str::from_utf8(body).unwrap()) - .expect("BUG: missing request body"); + let request_body = std::str::from_utf8(&request.body).unwrap(); JsonRpcRequest::from_str(request_body) .expect("BUG: invalid JSON RPC request") .id @@ -206,80 +196,37 @@ impl StubOnce { })) .unwrap(); - if let Some(max_response_bytes) = context.max_response_bytes - && (response_body.len() as u64) > max_response_bytes.get() - { - let mut payload = PayloadBuilder::new(); - payload = payload.http_response_failure( - id, - RejectCode::SysFatal, - format!("Http body exceeds size limit of {max_response_bytes} bytes."), - ); - env.execute_payload(payload); - return; - } - - let clean_up_context = match context.transform.clone() { - Some(transform) => transform.context, - None => vec![], - }; - let transform_arg = TransformArgs { - response: OutCallHttpResponse { - status: 200_u8.into(), + let response = match request.max_response_bytes { + Some(max_response_bytes) if (response_body.len() as u64) > max_response_bytes => { + CanisterHttpResponse::CanisterHttpReject(CanisterHttpReject { + reject_code: crate::REJECT_CODE_SYS_FATAL, + message: format!("Http body exceeds size limit of {max_response_bytes} bytes."), + }) + } + _ => CanisterHttpResponse::CanisterHttpReply(CanisterHttpReply { + status: 200, headers: vec![], body: response_body, - }, - context: clean_up_context.to_vec(), + }), }; - let canister_id_cleanup_response = context.request.sender; - let clean_up_response = Decode!( - &assert_reply( - env.execute_ingress( - canister_id_cleanup_response, - "cleanup_response", - Encode!(&transform_arg).unwrap(), - ) - .expect("failed to query transform http response") - ), - OutCallHttpResponse - ) - .unwrap(); - if let Some(max_response_bytes) = context.max_response_bytes - && (clean_up_response.body.len() as u64) > max_response_bytes.get() - { - let mut payload = PayloadBuilder::new(); - payload = payload.http_response_failure( - id, - RejectCode::SysFatal, - format!("Http body exceeds size limit of {max_response_bytes} bytes."), - ); - env.execute_payload(payload); - return; - } - - let http_response = CanisterHttpResponsePayload { - status: 200_u128, - headers: vec![], - body: clean_up_response.body, - }; - let mut payload = PayloadBuilder::new(); - payload = payload.http_response(id, &http_response); - env.execute_payload(payload); + env.mock_canister_http_response(MockCanisterHttpResponse { + subnet_id: request.subnet_id, + request_id: request.request_id, + response, + additional_responses: vec![], + }); + env.tick(); } } -pub fn debug_http_outcalls(env: &StateMachine) -> String { +pub fn debug_http_outcalls(env: &PocketIc) -> String { let mut debug_str = vec![]; - for context in env.canister_http_request_contexts().values() { - let request_body = context - .body - .as_ref() - .map(|body| std::str::from_utf8(body).unwrap()) - .expect("BUG: missing request body"); + for request in env.get_canister_http() { + let request_body = std::str::from_utf8(&request.body).unwrap(); debug_str.push(format!( "{:?} {} (max_response_bytes={:?}) {}", - context.http_method, context.url, context.max_response_bytes, request_body + request.http_method, request.url, request.max_response_bytes, request_body )); } debug_str.join("\n") diff --git a/rs/ethereum/cketh/test_utils/src/response.rs b/rs/ethereum/cketh/test_utils/src/response.rs index c72f64f38630..e3486f7b3622 100644 --- a/rs/ethereum/cketh/test_utils/src/response.rs +++ b/rs/ethereum/cketh/test_utils/src/response.rs @@ -135,11 +135,11 @@ pub fn default_signed_eip_1559_transaction() -> ( .chain_id(1_u64); let sig = ethers_core::types::Signature { r: ethers_core::types::U256::from_dec_str( - "80728915039673634151963281987194499535727562641034879173530654129915839382129", + "78378320896144165623306772901883835881146801392437857873186382435197931981331", ) .unwrap(), s: ethers_core::types::U256::from_dec_str( - "54281815563936592133007646348951747532427232100340298742740287107883437683286", + "11573774696968647626294885286453366440757932248598190533848223153778081228091", ) .unwrap(), v: 1, @@ -165,12 +165,12 @@ pub fn default_erc20_signed_eip_1559_transaction() -> ( .chain_id(1_u64); let sig = ethers_core::types::Signature { r: ethers_core::types::U256::from_str_radix( - "bb694aec6175b489523a55d5fce39452368e97096d4afa2cdcc35cf2d805152f", + "da4f476ede0aaf7da633371a938d5e2525a65a23699b55761779871a313f8cb3", 16, ) .unwrap(), s: ethers_core::types::U256::from_str_radix( - "0112b26a028af84dd397d23549844efdaf761d90cdcfdbe6c3608239648a85a3", + "45833d409eba50e3e9b145d04ea294ee791c14465503818f8b325a881938ddc1", 16, ) .unwrap(),