From 91580abd2698790a05f2c93e11d1af2e2e550e9f Mon Sep 17 00:00:00 2001 From: Daniel Wong <97631336+daniel-wong-dfinity-org@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:09:05 +0200 Subject: [PATCH 01/22] fix(registry): Do not ignore the error field in get_changes_since response. (#2217) When the err field is populated, deserialize_get_changes_since_response now returns Err. Whereas, before, the only time it returned Err is when deserialization itself failed, not when the message is properly encoded, but bears information about an error (such as authorization fail). --- rs/registry/transport/src/lib.rs | 46 ++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/rs/registry/transport/src/lib.rs b/rs/registry/transport/src/lib.rs index 2f68f9b56a9..db9c35804f0 100644 --- a/rs/registry/transport/src/lib.rs +++ b/rs/registry/transport/src/lib.rs @@ -5,7 +5,7 @@ use std::{fmt, str}; use crate::pb::v1::{ registry_error::Code, registry_mutation::Type, Precondition, RegistryDelta, RegistryError, - RegistryMutation, + RegistryGetChangesSinceResponse, RegistryMutation, }; use prost::Message; use serde::{Deserialize, Serialize}; @@ -69,7 +69,7 @@ impl From for Error { 1 => Error::KeyNotPresent(error.key), 2 => Error::KeyAlreadyPresent(error.key), 3 => Error::VersionNotLatest(error.key), - _ => Error::UnknownError(error.reason), + _ => Error::UnknownError(format!("{}: {}", error.code, error.reason)), } } } @@ -239,10 +239,22 @@ pub fn serialize_get_changes_since_request(version: u64) -> Result, Erro pub fn deserialize_get_changes_since_response( response: Vec, ) -> Result<(Vec, u64), Error> { - match pb::v1::RegistryGetChangesSinceResponse::decode(&response[..]) { - Ok(response) => Ok((response.deltas, response.version)), - Err(error) => Err(Error::MalformedMessage(error.to_string())), + let response = match pb::v1::RegistryGetChangesSinceResponse::decode(&response[..]) { + Ok(ok) => ok, + Err(error) => return Err(Error::MalformedMessage(error.to_string())), + }; + + let RegistryGetChangesSinceResponse { + error, + version, + deltas, + } = response; + + if let Some(error) = error { + return Err(Error::from(error)); } + + Ok((deltas, version)) } /// Serializes the arguments for a request to the insert() function in the @@ -446,4 +458,28 @@ mod tests { preconditions on keys: [africa, asia] }" ) } + + #[test] + fn test_deserialize_get_changes_since_response_with_error() { + let response = RegistryGetChangesSinceResponse { + error: Some(RegistryError { + code: Code::Authorization as i32, + reason: "You are not welcome here.".to_string(), + key: vec![], + }), + deltas: vec![], + version: 0, + }; + + let response = response.encode_to_vec(); + + let result = deserialize_get_changes_since_response(response); + + assert_eq!( + result, + Err(Error::UnknownError( + "5: You are not welcome here.".to_string() + )), + ); + } } From 5a4c5ca4fa86fa22f86f54c0a59c9d46478b4ab0 Mon Sep 17 00:00:00 2001 From: Carly Gundy <47304080+cgundy@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:36:55 +0200 Subject: [PATCH 02/22] chore(IDX): inline icrc1_agent_test (#2206) Help FinInt team with inlining effort. --------- Co-authored-by: IDX GitHub Automation Co-authored-by: IDX GitHub Automation Co-authored-by: Bas van Dijk --- Cargo.lock | 21 + Cargo.toml | 1 + rs/tests/Cargo.toml | 4 - rs/tests/financial_integrations/BUILD.bazel | 19 +- rs/tests/financial_integrations/Cargo.toml | 28 ++ .../icrc1_agent_test.rs | 418 +++++++++++++++++- rs/tests/src/icrc1_agent_test/mod.rs | 408 ----------------- rs/tests/src/lib.rs | 1 - 8 files changed, 478 insertions(+), 422 deletions(-) create mode 100644 rs/tests/financial_integrations/Cargo.toml delete mode 100644 rs/tests/src/icrc1_agent_test/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e34c445a0e5..b7b2079b6f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4088,6 +4088,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "financial_integrations-system-tests" +version = "0.9.0" +dependencies = [ + "anyhow", + "assert_matches", + "candid", + "canister-test", + "dfn_candid", + "ic-crypto-tree-hash", + "ic-icrc1-ledger", + "ic-ledger-core", + "ic-nns-test-utils", + "ic-registry-subnet-type", + "ic-system-test-driver", + "icrc-ledger-agent", + "icrc-ledger-types", + "on_wire", + "serde_cbor", +] + [[package]] name = "findshlibs" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index e424d11839d..bd88b55dea9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -370,6 +370,7 @@ members = [ "rs/tests/crypto", "rs/tests/driver", "rs/tests/execution", + "rs/tests/financial_integrations", "rs/tests/message_routing", "rs/tests/message_routing/common", "rs/tests/message_routing/rejoin_test_lib", diff --git a/rs/tests/Cargo.toml b/rs/tests/Cargo.toml index 523a0202c74..ffc98bc08bf 100644 --- a/rs/tests/Cargo.toml +++ b/rs/tests/Cargo.toml @@ -227,10 +227,6 @@ path = "financial_integrations/rosetta/rosetta_neuron_spawn_test.rs" name = "ic-systest-rosetta-neuron-staking-test" path = "financial_integrations/rosetta/rosetta_neuron_staking_test.rs" -[[bin]] -name = "ic-systest-icrc1-agent-test" -path = "financial_integrations/icrc1_agent_test.rs" - [[bin]] name = "test-driver-e2e-scenarios" path = "testing_verification/test_driver_e2e_scenarios.rs" diff --git a/rs/tests/financial_integrations/BUILD.bazel b/rs/tests/financial_integrations/BUILD.bazel index a3605fa96eb..3b8dd309404 100644 --- a/rs/tests/financial_integrations/BUILD.bazel +++ b/rs/tests/financial_integrations/BUILD.bazel @@ -14,14 +14,29 @@ system_test( "LEDGER_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/ledger:ledger_canister)", }, flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "k8s", ], target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS runtime_deps = GUESTOS_RUNTIME_DEPS + LEDGER_CANISTER_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], + deps = [ + "//packages/icrc-ledger-agent:icrc_ledger_agent", + "//packages/icrc-ledger-types:icrc_ledger_types", + "//rs/crypto/tree_hash", + "//rs/ledger_suite/common/ledger_core", + "//rs/ledger_suite/icrc1/ledger", + "//rs/nns/test_utils", + "//rs/registry/subnet_type", + "//rs/rust_canisters/canister_test", + "//rs/rust_canisters/dfn_candid", + "//rs/rust_canisters/on_wire", + "//rs/tests/driver:ic-system-test-driver", + "@crate_index//:anyhow", + "@crate_index//:assert_matches", + "@crate_index//:candid", + "@crate_index//:serde_cbor", + ], ) system_test_nns( diff --git a/rs/tests/financial_integrations/Cargo.toml b/rs/tests/financial_integrations/Cargo.toml new file mode 100644 index 00000000000..0df5014ccd4 --- /dev/null +++ b/rs/tests/financial_integrations/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "financial_integrations-system-tests" +version.workspace = true +authors.workspace = true +edition.workspace = true +description.workspace = true +documentation.workspace = true + +[dependencies] +anyhow = { workspace = true } +assert_matches = { workspace = true } +candid = { workspace = true } +canister-test = { path = "../../rust_canisters/canister_test" } +dfn_candid = { path = "../../rust_canisters/dfn_candid" } +ic-crypto-tree-hash = { path = "../../crypto/tree_hash" } +ic-icrc1-ledger = { path = "../../ledger_suite/icrc1/ledger" } +ic-ledger-core = { path = "../../ledger_suite/common/ledger_core" } +ic-nns-test-utils = { path = "../../nns/test_utils" } +ic-registry-subnet-type = { path = "../../registry/subnet_type" } +ic-system-test-driver = { path = "../driver" } +icrc-ledger-agent = { path = "../../../packages/icrc-ledger-agent" } +icrc-ledger-types = { path = "../../../packages/icrc-ledger-types" } +on_wire = { path = "../../rust_canisters/on_wire" } +serde_cbor = { workspace = true } + +[[bin]] +name = "icrc1-agent-test" +path = "icrc1_agent_test.rs" diff --git a/rs/tests/financial_integrations/icrc1_agent_test.rs b/rs/tests/financial_integrations/icrc1_agent_test.rs index 431922cb252..922cacd2053 100644 --- a/rs/tests/financial_integrations/icrc1_agent_test.rs +++ b/rs/tests/financial_integrations/icrc1_agent_test.rs @@ -1,15 +1,419 @@ -#[rustfmt::skip] - use anyhow::Result; +use assert_matches::assert_matches; +use candid::{Encode, Nat, Principal}; +use canister_test::{Canister, PrincipalId}; +use dfn_candid::CandidOne; +use ic_crypto_tree_hash::{LookupStatus, MixedHashTree}; +use ic_icrc1_ledger::{ArchiveOptions, FeatureFlags, InitArgsBuilder, LedgerArgument, UpgradeArgs}; +use ic_nns_test_utils::itest_helpers::install_rust_canister_from_path; +use ic_registry_subnet_type::SubnetType; +use ic_system_test_driver::driver::test_env_api::get_dependency_path; +use ic_system_test_driver::util::{agent_with_identity, random_ed25519_identity}; +use icrc_ledger_agent::{CallMode, Icrc1Agent}; +use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::transfer::TransferArg; +use icrc_ledger_types::icrc2::approve::ApproveArgs; +use icrc_ledger_types::icrc2::transfer_from::TransferFromArgs; +use icrc_ledger_types::{ + icrc::generic_metadata_value::MetadataValue as Value, icrc3::blocks::GetBlocksRequest, +}; +use on_wire::IntoWire; +use std::env; + +use ic_system_test_driver::{ + driver::{ + group::SystemTestGroup, + ic::{InternetComputer, Subnet}, + test_env::TestEnv, + test_env_api::{HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer}, + }, + systest, + util::{assert_create_agent, block_on, runtime_from_url}, +}; + +use std::convert::TryFrom; + +pub fn setup(env: TestEnv) { + InternetComputer::new() + .add_subnet(Subnet::fast_single_node(SubnetType::Application)) + .setup_and_start(&env) + .expect("failed to setup IC under test"); + env.topology_snapshot().subnets().for_each(|subnet| { + subnet + .nodes() + .for_each(|node| node.await_status_is_healthy().unwrap()) + }); +} + +pub fn test(env: TestEnv) { + let nns_node = env + .topology_snapshot() + .root_subnet() + .nodes() + .next() + .unwrap(); + let nns_runtime = runtime_from_url(nns_node.get_public_url(), nns_node.effective_canister_id()); + let nns_agent = nns_node.with_default_agent(|agent| async move { agent }); + block_on(async move { + let minting_user = PrincipalId::new_user_test_id(100); + let user1 = PrincipalId::try_from(nns_agent.get_principal().unwrap().as_ref()).unwrap(); + let user2 = PrincipalId::new_user_test_id(102); + let user3 = PrincipalId::new_user_test_id(270); + + let mut empty_ledger = nns_runtime + .create_canister_max_cycles_with_retries() + .await + .expect("Unable to create canister"); + + let empty_ledger_agent = Icrc1Agent { + agent: assert_create_agent(nns_node.get_public_url().as_str()).await, + ledger_canister_id: Principal::try_from_slice(empty_ledger.canister_id().as_ref()) + .unwrap(), + }; + install_icrc1_ledger( + &mut empty_ledger, + &LedgerArgument::Init(InitArgsBuilder::for_tests().build()), + ) + .await; + + let empty_tip = empty_ledger_agent + .get_certified_chain_tip() + .await + .expect("failed to get certified tip"); + assert_eq!(empty_tip, None); + + empty_ledger + .upgrade_to_self_binary(CandidOne(UpgradeArgs::default()).into_bytes().unwrap()) + .await + .unwrap(); + + let empty_tip = empty_ledger_agent + .get_certified_chain_tip() + .await + .expect("failed to get certified tip"); + assert_eq!(empty_tip, None); + + let mut ledger = nns_runtime + .create_canister_max_cycles_with_retries() + .await + .expect("Unable to create canister"); + + let agent = Icrc1Agent { + agent: assert_create_agent(nns_node.get_public_url().as_str()).await, + ledger_canister_id: Principal::try_from_slice(ledger.canister_id().as_ref()).unwrap(), + }; + + let other_agent = Icrc1Agent { + agent: agent_with_identity( + nns_node.get_public_url().as_str(), + random_ed25519_identity(), + ) + .await + .unwrap(), + ledger_canister_id: Principal::try_from_slice(ledger.canister_id().as_ref()).unwrap(), + }; + + let other_agent_principal = other_agent.agent.get_principal().unwrap(); + + let account1 = Account { + owner: user1.0, + subaccount: None, + }; + let account2 = Account { + owner: user2.0, + subaccount: None, + }; + let account3 = Account { + owner: user3.0, + subaccount: None, + }; + let minting_account = Account { + owner: minting_user.0, + subaccount: None, + }; + + let init_args = InitArgsBuilder::for_tests() + .with_minting_account(minting_account) + .with_initial_balance(account1, 1_000_000_000u64) + .with_transfer_fee(1_000_u16) + .with_feature_flags(FeatureFlags { icrc2: true }) + .with_archive_options(ArchiveOptions { + trigger_threshold: 2, + num_blocks_to_archive: 4, + node_max_memory_size_bytes: Some(1024 * 1024 * 1024), + max_message_size_bytes: Some(128 * 1024), + controller_id: agent.ledger_canister_id.into(), + more_controller_ids: None, + cycles_for_archive_creation: Some(10_000_000_000_000), + max_transactions_per_response: None, + }) + .build(); + install_icrc1_ledger(&mut ledger, &LedgerArgument::Init(init_args.clone())).await; + + // name + assert_eq!( + init_args.token_name, + agent.name(CallMode::Query).await.unwrap() + ); + assert_eq!( + init_args.token_name, + agent.name(CallMode::Update).await.unwrap() + ); + + // symbol + assert_eq!( + init_args.token_symbol, + agent.symbol(CallMode::Query).await.unwrap() + ); + assert_eq!( + init_args.token_symbol, + agent.symbol(CallMode::Update).await.unwrap() + ); + + // decimal + assert_eq!( + ic_ledger_core::tokens::DECIMAL_PLACES as u8, + agent.decimals(CallMode::Query).await.unwrap() + ); + assert_eq!( + ic_ledger_core::tokens::DECIMAL_PLACES as u8, + agent.decimals(CallMode::Update).await.unwrap() + ); + + // total_supply + assert_eq!( + Nat::from(1_000_000_000_u64), + agent.total_supply(CallMode::Query).await.unwrap() + ); + assert_eq!( + Nat::from(1_000_000_000_u64), + agent.total_supply(CallMode::Update).await.unwrap() + ); + + // fee + assert_eq!( + init_args.transfer_fee, + agent.fee(CallMode::Query).await.unwrap() + ); + assert_eq!( + init_args.transfer_fee, + agent.fee(CallMode::Update).await.unwrap() + ); -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::systest; -use ic_tests::icrc1_agent_test; + // minting account + assert_eq!( + Some(&init_args.minting_account), + agent + .minting_account(CallMode::Query) + .await + .unwrap() + .as_ref() + ); + assert_eq!( + Some(&init_args.minting_account), + agent + .minting_account(CallMode::Update) + .await + .unwrap() + .as_ref() + ); + + // metadata + let expected_metadata = vec![ + Value::entry( + "icrc1:decimals", + ic_ledger_core::tokens::DECIMAL_PLACES as u64, + ), + Value::entry("icrc1:name", init_args.token_name), + Value::entry("icrc1:symbol", init_args.token_symbol), + Value::entry("icrc1:fee", init_args.transfer_fee.clone()), + Value::entry("icrc1:max_memo_length", 32u64), + ]; + assert_eq!( + expected_metadata, + agent.metadata(CallMode::Query).await.unwrap() + ); + assert_eq!( + expected_metadata, + agent.metadata(CallMode::Update).await.unwrap() + ); + // balance_of + assert_eq!( + Nat::from(1_000_000_000u64), + agent.balance_of(account1, CallMode::Query).await.unwrap() + ); + assert_eq!( + Nat::from(1_000_000_000u64), + agent.balance_of(account1, CallMode::Update).await.unwrap() + ); + + // transfer + let amount = 10_000_000u64; + let _block = agent + .transfer(TransferArg { + from_subaccount: None, + to: account2, + fee: None, + created_at_time: None, + amount: Nat::from(amount), + memo: None, + }) + .await + .unwrap() + .unwrap(); + + assert_eq!( + Nat::from(1_000_000_000u64 - amount) - init_args.transfer_fee.clone(), + agent.balance_of(account1, CallMode::Query).await.unwrap() + ); + assert_eq!( + Nat::from(amount), + agent.balance_of(account2, CallMode::Query).await.unwrap() + ); + + let blocks_request = GetBlocksRequest { + start: Nat::from(0_u8), + length: Nat::from(10_u8), + }; + let blocks_response = agent.get_blocks(blocks_request).await.unwrap(); + assert_eq!(Nat::from(2_u8), blocks_response.chain_length); + assert_eq!(blocks_response.archived_blocks.len(), 1); + assert_eq!(blocks_response.archived_blocks[0].start, Nat::from(0_u8)); + assert_eq!(blocks_response.archived_blocks[0].length, Nat::from(2_u8)); + let archived_blocks = agent + .get_blocks_from_archive(blocks_response.archived_blocks[0].clone()) + .await + .unwrap(); + + let (last_block_hash, last_block_index) = agent + .get_certified_chain_tip() + .await + .expect("failed to get certified tip") + .unwrap(); + assert_eq!(archived_blocks.blocks[1].hash(), last_block_hash); + assert_eq!(Nat::from(1_u8), last_block_index); + + let data_certificate = agent.get_data_certificate().await.unwrap(); + assert!(data_certificate.certificate.is_some()); + + use LookupStatus::Found; + let hash_tree: MixedHashTree = serde_cbor::from_slice(&data_certificate.hash_tree).unwrap(); + + assert_eq!( + hash_tree.lookup(&[b"last_block_index"]), + Found(&mleaf((1_u64).to_be_bytes())) + ); + + assert_eq!( + hash_tree.lookup(&[b"tip_hash"]), + Found(&mleaf(archived_blocks.blocks[1].hash())) + ); + + let cert = serde_cbor::from_slice(&data_certificate.certificate.unwrap()).unwrap(); + assert_matches!( + agent.verify_root_hash(&cert, &hash_tree.digest().0).await, + Ok(_) + ); + let spender = Account { + owner: other_agent_principal, + subaccount: None, + }; + + let _block = agent + .approve(ApproveArgs { + from_subaccount: None, + spender, + amount: Nat::from(u64::MAX), + expected_allowance: None, + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }) + .await + .unwrap() + .unwrap(); + + // Test get_blocks directly from the ledger. + let (last_block_hash, last_block_index) = agent + .get_certified_chain_tip() + .await + .expect("failed to get certified tip") + .unwrap(); + let blocks_request = GetBlocksRequest { + start: Nat::from(2_u8), + length: Nat::from(1_u8), + }; + let blocks_response = agent.get_blocks(blocks_request).await.unwrap(); + assert_eq!(last_block_hash, blocks_response.blocks[0].hash()); + assert_eq!(last_block_index, 2u8); + + ledger + .upgrade_to_self_binary(CandidOne(UpgradeArgs::default()).into_bytes().unwrap()) + .await + .unwrap(); + + let (last_block_hash, last_block_index) = agent + .get_certified_chain_tip() + .await + .expect("failed to get certified tip") + .unwrap(); + assert_eq!(last_block_hash, blocks_response.blocks[0].hash()); + assert_eq!(last_block_index, 2_u8); + + let allowance = agent + .allowance(account1, spender, CallMode::Query) + .await + .unwrap(); + assert_eq!(allowance.allowance, Nat::from(u64::MAX)); + + const TRANSFER_FROM_AMOUNT: u64 = 10_000; + let _block = other_agent + .transfer_from(TransferFromArgs { + spender_subaccount: None, + from: account1, + to: account3, + amount: Nat::from(TRANSFER_FROM_AMOUNT), + fee: None, + memo: None, + created_at_time: None, + }) + .await + .unwrap() + .unwrap(); + + assert_eq!( + Nat::from(TRANSFER_FROM_AMOUNT), + agent.balance_of(account3, CallMode::Query).await.unwrap() + ); + let allowance = agent + .allowance(account1, spender, CallMode::Query) + .await + .unwrap(); + assert_eq!( + allowance.allowance, + Nat::from(u64::MAX - TRANSFER_FROM_AMOUNT) - init_args.transfer_fee + ); + }); +} + +fn mleaf>(blob: B) -> MixedHashTree { + MixedHashTree::Leaf(blob.as_ref().to_vec()) +} + +pub async fn install_icrc1_ledger<'a>(canister: &mut Canister<'a>, args: &LedgerArgument) { + install_rust_canister_from_path( + canister, + get_dependency_path(env::var("LEDGER_WASM_PATH").expect("LEDGER_WASM_PATH not set")), + Some(Encode!(&args).unwrap()), + ) + .await +} fn main() -> Result<()> { SystemTestGroup::new() - .with_setup(icrc1_agent_test::config) - .add_test(systest!(icrc1_agent_test::test)) + .with_setup(setup) + .add_test(systest!(test)) .execute_from_args()?; Ok(()) } diff --git a/rs/tests/src/icrc1_agent_test/mod.rs b/rs/tests/src/icrc1_agent_test/mod.rs deleted file mode 100644 index 25909226186..00000000000 --- a/rs/tests/src/icrc1_agent_test/mod.rs +++ /dev/null @@ -1,408 +0,0 @@ -use std::convert::TryFrom; - -use assert_matches::assert_matches; -use candid::{Encode, Nat, Principal}; -use canister_test::{Canister, PrincipalId}; -use dfn_candid::CandidOne; -use ic_crypto_tree_hash::{LookupStatus, MixedHashTree}; -use ic_icrc1_ledger::{ArchiveOptions, FeatureFlags, InitArgsBuilder, LedgerArgument, UpgradeArgs}; -use ic_nns_test_utils::itest_helpers::install_rust_canister_from_path; -use ic_registry_subnet_type::SubnetType; -use ic_system_test_driver::driver::test_env_api::get_dependency_path; -use ic_system_test_driver::util::{agent_with_identity, random_ed25519_identity}; -use icrc_ledger_agent::{CallMode, Icrc1Agent}; -use icrc_ledger_types::icrc1::account::Account; -use icrc_ledger_types::icrc1::transfer::TransferArg; -use icrc_ledger_types::icrc2::approve::ApproveArgs; -use icrc_ledger_types::icrc2::transfer_from::TransferFromArgs; -use icrc_ledger_types::{ - icrc::generic_metadata_value::MetadataValue as Value, icrc3::blocks::GetBlocksRequest, -}; -use on_wire::IntoWire; -use std::env; - -use ic_system_test_driver::{ - driver::{ - ic::{InternetComputer, Subnet}, - test_env::TestEnv, - test_env_api::{HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer}, - }, - util::{assert_create_agent, block_on, runtime_from_url}, -}; - -pub fn config(env: TestEnv) { - InternetComputer::new() - .add_subnet(Subnet::fast_single_node(SubnetType::Application)) - .setup_and_start(&env) - .expect("failed to setup IC under test"); - env.topology_snapshot().subnets().for_each(|subnet| { - subnet - .nodes() - .for_each(|node| node.await_status_is_healthy().unwrap()) - }); -} - -pub fn test(env: TestEnv) { - let nns_node = env - .topology_snapshot() - .root_subnet() - .nodes() - .next() - .unwrap(); - let nns_runtime = runtime_from_url(nns_node.get_public_url(), nns_node.effective_canister_id()); - let nns_agent = nns_node.with_default_agent(|agent| async move { agent }); - block_on(async move { - let minting_user = PrincipalId::new_user_test_id(100); - let user1 = PrincipalId::try_from(nns_agent.get_principal().unwrap().as_ref()).unwrap(); - let user2 = PrincipalId::new_user_test_id(102); - let user3 = PrincipalId::new_user_test_id(270); - - let mut empty_ledger = nns_runtime - .create_canister_max_cycles_with_retries() - .await - .expect("Unable to create canister"); - - let empty_ledger_agent = Icrc1Agent { - agent: assert_create_agent(nns_node.get_public_url().as_str()).await, - ledger_canister_id: Principal::try_from_slice(empty_ledger.canister_id().as_ref()) - .unwrap(), - }; - install_icrc1_ledger( - &mut empty_ledger, - &LedgerArgument::Init(InitArgsBuilder::for_tests().build()), - ) - .await; - - let empty_tip = empty_ledger_agent - .get_certified_chain_tip() - .await - .expect("failed to get certified tip"); - assert_eq!(empty_tip, None); - - empty_ledger - .upgrade_to_self_binary(CandidOne(UpgradeArgs::default()).into_bytes().unwrap()) - .await - .unwrap(); - - let empty_tip = empty_ledger_agent - .get_certified_chain_tip() - .await - .expect("failed to get certified tip"); - assert_eq!(empty_tip, None); - - let mut ledger = nns_runtime - .create_canister_max_cycles_with_retries() - .await - .expect("Unable to create canister"); - - let agent = Icrc1Agent { - agent: assert_create_agent(nns_node.get_public_url().as_str()).await, - ledger_canister_id: Principal::try_from_slice(ledger.canister_id().as_ref()).unwrap(), - }; - - let other_agent = Icrc1Agent { - agent: agent_with_identity( - nns_node.get_public_url().as_str(), - random_ed25519_identity(), - ) - .await - .unwrap(), - ledger_canister_id: Principal::try_from_slice(ledger.canister_id().as_ref()).unwrap(), - }; - - let other_agent_principal = other_agent.agent.get_principal().unwrap(); - - let account1 = Account { - owner: user1.0, - subaccount: None, - }; - let account2 = Account { - owner: user2.0, - subaccount: None, - }; - let account3 = Account { - owner: user3.0, - subaccount: None, - }; - let minting_account = Account { - owner: minting_user.0, - subaccount: None, - }; - - let init_args = InitArgsBuilder::for_tests() - .with_minting_account(minting_account) - .with_initial_balance(account1, 1_000_000_000u64) - .with_transfer_fee(1_000_u16) - .with_feature_flags(FeatureFlags { icrc2: true }) - .with_archive_options(ArchiveOptions { - trigger_threshold: 2, - num_blocks_to_archive: 4, - node_max_memory_size_bytes: Some(1024 * 1024 * 1024), - max_message_size_bytes: Some(128 * 1024), - controller_id: agent.ledger_canister_id.into(), - more_controller_ids: None, - cycles_for_archive_creation: Some(10_000_000_000_000), - max_transactions_per_response: None, - }) - .build(); - install_icrc1_ledger(&mut ledger, &LedgerArgument::Init(init_args.clone())).await; - - // name - assert_eq!( - init_args.token_name, - agent.name(CallMode::Query).await.unwrap() - ); - assert_eq!( - init_args.token_name, - agent.name(CallMode::Update).await.unwrap() - ); - - // symbol - assert_eq!( - init_args.token_symbol, - agent.symbol(CallMode::Query).await.unwrap() - ); - assert_eq!( - init_args.token_symbol, - agent.symbol(CallMode::Update).await.unwrap() - ); - - // decimal - assert_eq!( - ic_ledger_core::tokens::DECIMAL_PLACES as u8, - agent.decimals(CallMode::Query).await.unwrap() - ); - assert_eq!( - ic_ledger_core::tokens::DECIMAL_PLACES as u8, - agent.decimals(CallMode::Update).await.unwrap() - ); - - // total_supply - assert_eq!( - Nat::from(1_000_000_000_u64), - agent.total_supply(CallMode::Query).await.unwrap() - ); - assert_eq!( - Nat::from(1_000_000_000_u64), - agent.total_supply(CallMode::Update).await.unwrap() - ); - - // fee - assert_eq!( - init_args.transfer_fee, - agent.fee(CallMode::Query).await.unwrap() - ); - assert_eq!( - init_args.transfer_fee, - agent.fee(CallMode::Update).await.unwrap() - ); - - // minting account - assert_eq!( - Some(&init_args.minting_account), - agent - .minting_account(CallMode::Query) - .await - .unwrap() - .as_ref() - ); - assert_eq!( - Some(&init_args.minting_account), - agent - .minting_account(CallMode::Update) - .await - .unwrap() - .as_ref() - ); - - // metadata - let expected_metadata = vec![ - Value::entry( - "icrc1:decimals", - ic_ledger_core::tokens::DECIMAL_PLACES as u64, - ), - Value::entry("icrc1:name", init_args.token_name), - Value::entry("icrc1:symbol", init_args.token_symbol), - Value::entry("icrc1:fee", init_args.transfer_fee.clone()), - Value::entry("icrc1:max_memo_length", 32u64), - ]; - assert_eq!( - expected_metadata, - agent.metadata(CallMode::Query).await.unwrap() - ); - assert_eq!( - expected_metadata, - agent.metadata(CallMode::Update).await.unwrap() - ); - // balance_of - assert_eq!( - Nat::from(1_000_000_000u64), - agent.balance_of(account1, CallMode::Query).await.unwrap() - ); - assert_eq!( - Nat::from(1_000_000_000u64), - agent.balance_of(account1, CallMode::Update).await.unwrap() - ); - - // transfer - let amount = 10_000_000u64; - let _block = agent - .transfer(TransferArg { - from_subaccount: None, - to: account2, - fee: None, - created_at_time: None, - amount: Nat::from(amount), - memo: None, - }) - .await - .unwrap() - .unwrap(); - - assert_eq!( - Nat::from(1_000_000_000u64 - amount) - init_args.transfer_fee.clone(), - agent.balance_of(account1, CallMode::Query).await.unwrap() - ); - assert_eq!( - Nat::from(amount), - agent.balance_of(account2, CallMode::Query).await.unwrap() - ); - - let blocks_request = GetBlocksRequest { - start: Nat::from(0_u8), - length: Nat::from(10_u8), - }; - let blocks_response = agent.get_blocks(blocks_request).await.unwrap(); - assert_eq!(Nat::from(2_u8), blocks_response.chain_length); - assert_eq!(blocks_response.archived_blocks.len(), 1); - assert_eq!(blocks_response.archived_blocks[0].start, Nat::from(0_u8)); - assert_eq!(blocks_response.archived_blocks[0].length, Nat::from(2_u8)); - let archived_blocks = agent - .get_blocks_from_archive(blocks_response.archived_blocks[0].clone()) - .await - .unwrap(); - - let (last_block_hash, last_block_index) = agent - .get_certified_chain_tip() - .await - .expect("failed to get certified tip") - .unwrap(); - assert_eq!(archived_blocks.blocks[1].hash(), last_block_hash); - assert_eq!(Nat::from(1_u8), last_block_index); - - let data_certificate = agent.get_data_certificate().await.unwrap(); - assert!(data_certificate.certificate.is_some()); - - use LookupStatus::Found; - let hash_tree: MixedHashTree = serde_cbor::from_slice(&data_certificate.hash_tree).unwrap(); - - assert_eq!( - hash_tree.lookup(&[b"last_block_index"]), - Found(&mleaf((1_u64).to_be_bytes())) - ); - - assert_eq!( - hash_tree.lookup(&[b"tip_hash"]), - Found(&mleaf(archived_blocks.blocks[1].hash())) - ); - - let cert = serde_cbor::from_slice(&data_certificate.certificate.unwrap()).unwrap(); - assert_matches!( - agent.verify_root_hash(&cert, &hash_tree.digest().0).await, - Ok(_) - ); - let spender = Account { - owner: other_agent_principal, - subaccount: None, - }; - - let _block = agent - .approve(ApproveArgs { - from_subaccount: None, - spender, - amount: Nat::from(u64::MAX), - expected_allowance: None, - expires_at: None, - fee: None, - memo: None, - created_at_time: None, - }) - .await - .unwrap() - .unwrap(); - - // Test get_blocks directly from the ledger. - let (last_block_hash, last_block_index) = agent - .get_certified_chain_tip() - .await - .expect("failed to get certified tip") - .unwrap(); - let blocks_request = GetBlocksRequest { - start: Nat::from(2_u8), - length: Nat::from(1_u8), - }; - let blocks_response = agent.get_blocks(blocks_request).await.unwrap(); - assert_eq!(last_block_hash, blocks_response.blocks[0].hash()); - assert_eq!(last_block_index, 2u8); - - ledger - .upgrade_to_self_binary(CandidOne(UpgradeArgs::default()).into_bytes().unwrap()) - .await - .unwrap(); - - let (last_block_hash, last_block_index) = agent - .get_certified_chain_tip() - .await - .expect("failed to get certified tip") - .unwrap(); - assert_eq!(last_block_hash, blocks_response.blocks[0].hash()); - assert_eq!(last_block_index, 2_u8); - - let allowance = agent - .allowance(account1, spender, CallMode::Query) - .await - .unwrap(); - assert_eq!(allowance.allowance, Nat::from(u64::MAX)); - - const TRANSFER_FROM_AMOUNT: u64 = 10_000; - let _block = other_agent - .transfer_from(TransferFromArgs { - spender_subaccount: None, - from: account1, - to: account3, - amount: Nat::from(TRANSFER_FROM_AMOUNT), - fee: None, - memo: None, - created_at_time: None, - }) - .await - .unwrap() - .unwrap(); - - assert_eq!( - Nat::from(TRANSFER_FROM_AMOUNT), - agent.balance_of(account3, CallMode::Query).await.unwrap() - ); - let allowance = agent - .allowance(account1, spender, CallMode::Query) - .await - .unwrap(); - assert_eq!( - allowance.allowance, - Nat::from(u64::MAX - TRANSFER_FROM_AMOUNT) - init_args.transfer_fee - ); - }); -} - -fn mleaf>(blob: B) -> MixedHashTree { - MixedHashTree::Leaf(blob.as_ref().to_vec()) -} - -pub async fn install_icrc1_ledger<'a>(canister: &mut Canister<'a>, args: &LedgerArgument) { - install_rust_canister_from_path( - canister, - get_dependency_path(env::var("LEDGER_WASM_PATH").expect("LEDGER_WASM_PATH not set")), - Some(Encode!(&args).unwrap()), - ) - .await -} diff --git a/rs/tests/src/lib.rs b/rs/tests/src/lib.rs index ddc8e94af86..3ce34589a29 100644 --- a/rs/tests/src/lib.rs +++ b/rs/tests/src/lib.rs @@ -1,5 +1,4 @@ pub mod api_test; -pub mod icrc1_agent_test; pub mod ledger_tests; pub mod networking; pub mod nns_tests; From 102306234dd99f209ffc64991cc9bdd309eed8b5 Mon Sep 17 00:00:00 2001 From: oggy-dfin <89794951+oggy-dfin@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:11:28 +0200 Subject: [PATCH 03/22] feat(RES-151): support for multiple calls of the same function (#2079) When the same function (e.g., ledger transfer) is called multiple times from an update method (e.g., disbursing a neuron, where we first burn the fees, and then transfer the disbursed amount), we need to allow the TLA model to distinguish in which context the function is being called. The contexts are defined through Pluscal labels. Thus: 1. we add a new annotation, `tla_function`, which can (and should) be used in functions called in multiple contexts within the same update method 2. we add a `log_label` macro to allow changing the context in the parent method 3. we adopt a convention where we stack labels by concatenating them (separated by an underscore). So if the transfer function uses the `Transfer` label, we can have, e.g., `Burn_Fees_Transfer` in the `Burn_Fees` context and `Transfer_Disbursed_Transfer` in `Transfer_Disbursed` context) The PR also includes some unrelated improvements to TLA error reporting (including reporting the source locations). --------- Co-authored-by: IDX GitHub Automation --- Cargo.lock | 1 - rs/nns/governance/src/governance/tla/mod.rs | 1 + rs/tla_instrumentation/BUILD.bazel | 41 ++- rs/tla_instrumentation/tla/Counter.tla | 15 +- rs/tla_instrumentation/tla/Multiple_Calls.tla | 100 ++++++ .../tla/Multiple_Calls_Apalache.tla | 40 +++ .../tla_instrumentation/Cargo.toml | 3 +- .../tla_instrumentation/src/checker.rs | 55 ++- .../tla_instrumentation/src/lib.rs | 78 ++++- .../tla_instrumentation/src/tla_state.rs | 10 +- .../tla_instrumentation/src/tla_value.rs | 5 +- .../tla_instrumentation/tests/basic_tests.rs | 23 ++ .../tla_instrumentation/tests/common.rs | 62 ++++ .../tests/multiple_calls.rs | 328 ++++++++++++++++++ .../tla_instrumentation/tests/structs.rs | 80 +---- .../src/lib.rs | 80 ++++- 16 files changed, 812 insertions(+), 110 deletions(-) create mode 100644 rs/tla_instrumentation/tla/Multiple_Calls.tla create mode 100644 rs/tla_instrumentation/tla/Multiple_Calls_Apalache.tla create mode 100644 rs/tla_instrumentation/tla_instrumentation/tests/basic_tests.rs create mode 100644 rs/tla_instrumentation/tla_instrumentation/tests/common.rs create mode 100644 rs/tla_instrumentation/tla_instrumentation/tests/multiple_calls.rs diff --git a/Cargo.lock b/Cargo.lock index b7b2079b6f5..91f394c6c78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20407,7 +20407,6 @@ version = "0.1.0" dependencies = [ "candid", "local_key", - "serde", "sha2 0.10.8", "tla_instrumentation_proc_macros", "tokio-test", diff --git a/rs/nns/governance/src/governance/tla/mod.rs b/rs/nns/governance/src/governance/tla/mod.rs index 4d1194de14f..f4c7c8b5e4a 100644 --- a/rs/nns/governance/src/governance/tla/mod.rs +++ b/rs/nns/governance/src/governance/tla/mod.rs @@ -149,6 +149,7 @@ fn post_process_trace(trace: &mut Vec) { for ResolvedStatePair { ref mut start, ref mut end, + .. } in trace { for state in &mut [start, end] { diff --git a/rs/tla_instrumentation/BUILD.bazel b/rs/tla_instrumentation/BUILD.bazel index 4c99175594d..372be6df33f 100644 --- a/rs/tla_instrumentation/BUILD.bazel +++ b/rs/tla_instrumentation/BUILD.bazel @@ -24,8 +24,45 @@ rust_library( ) rust_test( - name = "tla_instrumentation_test", - srcs = glob(["tla_instrumentation/tests/**/*.rs"]), + name = "structs_test", + srcs = [ + "tla_instrumentation/tests/common.rs", + "tla_instrumentation/tests/structs.rs", + ], + crate_root = "tla_instrumentation/tests/structs.rs", + data = [ + ":tla_models", + "@bazel_tools//tools/jdk:current_java_runtime", + "@tla_apalache//:bin/apalache-mc", + ], + env = { + "JAVABASE": "$(JAVABASE)", + "TLA_APALACHE_BIN": "$(rootpath @tla_apalache//:bin/apalache-mc)", + "TLA_MODULES": "$(locations :tla_models)", + }, + proc_macro_deps = [":proc_macros"], + toolchains = ["@bazel_tools//tools/jdk:current_java_runtime"], + deps = [ + ":local_key", + ":tla_instrumentation", + "@crate_index//:candid", + "@crate_index//:tokio-test", + ], +) + +rust_test( + name = "basic_tests", + srcs = ["tla_instrumentation/tests/basic_tests.rs"], + deps = [":tla_instrumentation"], +) + +rust_test( + name = "multiple_calls_test", + srcs = [ + "tla_instrumentation/tests/common.rs", + "tla_instrumentation/tests/multiple_calls.rs", + ], + crate_root = "tla_instrumentation/tests/multiple_calls.rs", data = [ ":tla_models", "@bazel_tools//tools/jdk:current_java_runtime", diff --git a/rs/tla_instrumentation/tla/Counter.tla b/rs/tla_instrumentation/tla/Counter.tla index f54f1bd9c44..ddd16b16aef 100644 --- a/rs/tla_instrumentation/tla/Counter.tla +++ b/rs/tla_instrumentation/tla/Counter.tla @@ -12,7 +12,7 @@ target(value) == Variant("Target_Method", value) variables counter = 0; - empty_fun = [x \in {} |-> {}]; + empty_fun = [x \in {} |-> CHOOSE y \in {}: TRUE]; mycan_to_othercan = <<>>; othercan_to_mycan = {}; @@ -32,15 +32,18 @@ process ( My_Method \in My_Method_Process_Ids ) } } *) -\* BEGIN TRANSLATION (chksum(pcal) = "87a0b4ea" /\ chksum(tla) = "d0aa893") -VARIABLES counter, mycan_to_othercan, othercan_to_mycan, pc, my_local +\* BEGIN TRANSLATION (chksum(pcal) = "f0c5512a" /\ chksum(tla) = "5fadb447") +VARIABLES pc, counter, empty_fun, mycan_to_othercan, othercan_to_mycan, + my_local -vars == << counter, mycan_to_othercan, othercan_to_mycan, pc, my_local >> +vars == << pc, counter, empty_fun, mycan_to_othercan, othercan_to_mycan, + my_local >> ProcSet == (My_Method_Process_Ids) Init == (* Global variables *) /\ counter = 0 + /\ empty_fun = [x \in {} |-> CHOOSE y \in {}: TRUE] /\ mycan_to_othercan = <<>> /\ othercan_to_mycan = {} (* Process My_Method *) @@ -52,7 +55,7 @@ Start_Label(self) == /\ pc[self] = "Start_Label" /\ my_local' = [my_local EXCEPT ![self] = counter'] /\ mycan_to_othercan' = Append(mycan_to_othercan, request(self, target(2))) /\ pc' = [pc EXCEPT ![self] = "WaitForResponse"] - /\ UNCHANGED othercan_to_mycan + /\ UNCHANGED << empty_fun, othercan_to_mycan >> WaitForResponse(self) == /\ pc[self] = "WaitForResponse" /\ \E resp \in { r \in othercan_to_mycan: r.caller = self }: @@ -60,7 +63,7 @@ WaitForResponse(self) == /\ pc[self] = "WaitForResponse" /\ counter' = counter + 1 /\ my_local' = [my_local EXCEPT ![self] = counter'] /\ pc' = [pc EXCEPT ![self] = "Done"] - /\ UNCHANGED mycan_to_othercan + /\ UNCHANGED << empty_fun, mycan_to_othercan >> My_Method(self) == Start_Label(self) \/ WaitForResponse(self) diff --git a/rs/tla_instrumentation/tla/Multiple_Calls.tla b/rs/tla_instrumentation/tla/Multiple_Calls.tla new file mode 100644 index 00000000000..10fd697fb96 --- /dev/null +++ b/rs/tla_instrumentation/tla/Multiple_Calls.tla @@ -0,0 +1,100 @@ +---- MODULE Multiple_Calls ---- +EXTENDS TLC, Naturals, Variants, Sequences + +CONSTANTS + My_Method_Process_Ids, + MAX_COUNTER + +request(caller, request_args) == [caller |-> caller, method_and_args |-> request_args] +target(value) == Variant("Target_Method", value) + +(* --algorithm Multiple_Calls { + +variables + counter = 0; + empty_fun = [x \in {} |-> CHOOSE y \in {}: TRUE]; + mycan_to_othercan = <<>>; + othercan_to_mycan = {}; + +process ( My_Method \in My_Method_Process_Ids ) + variable my_local = 0; +{ + Start_Label: + counter := counter + 1; + my_local := counter; + mycan_to_othercan := Append(mycan_to_othercan, request(self, target(2))); + Phase1_WaitForResponse: + with(resp \in { r \in othercan_to_mycan: r.caller = self }) { + othercan_to_mycan := othercan_to_mycan \ {resp}; + counter := counter + 1; + my_local := counter; + }; + mycan_to_othercan := Append(mycan_to_othercan, request(self, target(2))); + Phase2_WaitForResponse: + with(resp \in { r \in othercan_to_mycan: r.caller = self }) { + othercan_to_mycan := othercan_to_mycan \ {resp}; + counter := counter + 1; + my_local := counter; + } +} + +} *) +\* BEGIN TRANSLATION (chksum(pcal) = "348f0bcb" /\ chksum(tla) = "b5d92e54") +VARIABLES pc, counter, empty_fun, mycan_to_othercan, othercan_to_mycan, + my_local + +vars == << pc, counter, empty_fun, mycan_to_othercan, othercan_to_mycan, + my_local >> + +ProcSet == (My_Method_Process_Ids) + +Init == (* Global variables *) + /\ counter = 0 + /\ empty_fun = [x \in {} |-> CHOOSE y \in {}: TRUE] + /\ mycan_to_othercan = <<>> + /\ othercan_to_mycan = {} + (* Process My_Method *) + /\ my_local = [self \in My_Method_Process_Ids |-> 0] + /\ pc = [self \in ProcSet |-> "Start_Label"] + +Start_Label(self) == /\ pc[self] = "Start_Label" + /\ counter' = counter + 1 + /\ my_local' = [my_local EXCEPT ![self] = counter'] + /\ mycan_to_othercan' = Append(mycan_to_othercan, request(self, target(2))) + /\ pc' = [pc EXCEPT ![self] = "Phase1_WaitForResponse"] + /\ UNCHANGED << empty_fun, othercan_to_mycan >> + +Phase1_WaitForResponse(self) == /\ pc[self] = "Phase1_WaitForResponse" + /\ \E resp \in { r \in othercan_to_mycan: r.caller = self }: + /\ othercan_to_mycan' = othercan_to_mycan \ {resp} + /\ counter' = counter + 1 + /\ my_local' = [my_local EXCEPT ![self] = counter'] + /\ mycan_to_othercan' = Append(mycan_to_othercan, request(self, target(2))) + /\ pc' = [pc EXCEPT ![self] = "Phase2_WaitForResponse"] + /\ UNCHANGED empty_fun + +Phase2_WaitForResponse(self) == /\ pc[self] = "Phase2_WaitForResponse" + /\ \E resp \in { r \in othercan_to_mycan: r.caller = self }: + /\ othercan_to_mycan' = othercan_to_mycan \ {resp} + /\ counter' = counter + 1 + /\ my_local' = [my_local EXCEPT ![self] = counter'] + /\ pc' = [pc EXCEPT ![self] = "Done"] + /\ UNCHANGED << empty_fun, mycan_to_othercan >> + +My_Method(self) == Start_Label(self) \/ Phase1_WaitForResponse(self) + \/ Phase2_WaitForResponse(self) + +(* Allow infinite stuttering to prevent deadlock on termination. *) +Terminating == /\ \A self \in ProcSet: pc[self] = "Done" + /\ UNCHANGED vars + +Next == (\E self \in My_Method_Process_Ids: My_Method(self)) + \/ Terminating + +Spec == Init /\ [][Next]_vars + +Termination == <>(\A self \in ProcSet: pc[self] = "Done") + +\* END TRANSLATION + +==== diff --git a/rs/tla_instrumentation/tla/Multiple_Calls_Apalache.tla b/rs/tla_instrumentation/tla/Multiple_Calls_Apalache.tla new file mode 100644 index 00000000000..d9d0b744160 --- /dev/null +++ b/rs/tla_instrumentation/tla/Multiple_Calls_Apalache.tla @@ -0,0 +1,40 @@ +---- MODULE Multiple_Calls_Apalache ---- + +EXTENDS TLC, Sequences, Variants + +\* The constants similar to the ones below will be inserted by the code link +\* at the CODE_LINK_INSERT_CONSTANTS marker. +(* +MAX_COUNTER == 2 +My_Method_Process_Ids == {"Counter"} +*) + +\* CODE_LINK_INSERT_CONSTANTS + + +(* +@typeAlias: proc = Str; +@typeAlias: methodCall = Target_Method(Int); +@typeAlias: methodResponse = Fail(UNIT) | Ok(Int); +*) +_type_alias_dummy == TRUE + +VARIABLES + \* @type: Int; + counter, + \* @type: Int -> Int; + empty_fun, + \* @type: $proc -> Int; + my_local, + \* @type: Seq({caller : $proc, method_and_args: $methodCall }); + mycan_to_othercan, + \* @type: Set({caller: $proc, response: $methodResponse }); + othercan_to_mycan, + \* @type: $proc -> Str; + pc + +MOD == INSTANCE Multiple_Calls + +Next == [MOD!Next]_MOD!vars + +==== \ No newline at end of file diff --git a/rs/tla_instrumentation/tla_instrumentation/Cargo.toml b/rs/tla_instrumentation/tla_instrumentation/Cargo.toml index 3f26161a1a9..de5c01c8661 100644 --- a/rs/tla_instrumentation/tla_instrumentation/Cargo.toml +++ b/rs/tla_instrumentation/tla_instrumentation/Cargo.toml @@ -7,11 +7,10 @@ edition = "2021" [dependencies] candid = { workspace = true } -serde = { workspace = true } -tla_instrumentation_proc_macros = { path = "../tla_instrumentation_proc_macros" } sha2 = { workspace = true } uuid = { workspace = true } [dev-dependencies] tokio-test = { workspace = true } local_key = { path = "../local_key" } +tla_instrumentation_proc_macros = { path = "../tla_instrumentation_proc_macros" } diff --git a/rs/tla_instrumentation/tla_instrumentation/src/checker.rs b/rs/tla_instrumentation/tla_instrumentation/src/checker.rs index 0e95c8e080f..c71de8ed423 100644 --- a/rs/tla_instrumentation/tla_instrumentation/src/checker.rs +++ b/rs/tla_instrumentation/tla_instrumentation/src/checker.rs @@ -1,6 +1,7 @@ // use ic_state_machine_tests::StateMachine; // use ic_test_utilities_load_wasm::load_wasm; use std::collections::HashMap; +use std::fmt::Formatter; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; @@ -13,19 +14,66 @@ pub trait HasTlaRepr { fn to_tla_state(&self) -> HashMap; } -#[derive(Debug)] pub enum ApalacheError { - CheckFailed(String), + CheckFailed(Option, String), SetupError(String), } -#[derive(Debug)] +impl std::fmt::Debug for ApalacheError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ApalacheError::SetupError(e) => f.write_str(&format!("Apalache setup error: {}", e)), + ApalacheError::CheckFailed(Some(code), s) => { + f.write_str(&format!("{}\n", s))?; + match *code { + 12 => + // code used to signal deadlocks + { + f.write_str("This is most likely a mismatch between the code and the model") + } + _ => f.write_str("This is most likely a problem with the model itself."), + } + } + ApalacheError::CheckFailed(None, s) => { + f.write_str(s)?; + f.write_str( + "The error code was not available - this is not expected, please report.", + ) + } + } + } +} + pub struct TlaCheckError { pub apalache_error: ApalacheError, pub pair: ResolvedStatePair, pub constants: TlaConstantAssignment, } +impl std::fmt::Debug for TlaCheckError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str( + &format!( + "Apalache returned the error: {:?}\nThe error occured while checking the transition between:\n", + self.apalache_error, + ) + )?; + f.debug_map() + .entries(self.pair.start.0 .0.iter()) + .finish()?; + f.write_str("\nand\n")?; + f.debug_map().entries(self.pair.end.0 .0.iter()).finish()?; + f.write_str(&format!( + "\nThe start and end locations in the code are:\n{}\nand\n{}", + self.pair.start_source_location, self.pair.end_source_location + ))?; + f.write_str("\nThe constants are:\n")?; + f.debug_map() + .entries(self.constants.constants.iter()) + .finish() + } +} + const INIT_PREDICATE_NAME: &str = "Check_Code_Link_Init"; const NEXT_PREDICATE_NAME: &str = "Check_Code_Link_Next"; @@ -121,6 +169,7 @@ fn run_apalache( Ok(()) } else { Err(ApalacheError::CheckFailed( + e.code(), format!( "When checking file\n{:?}\nApalache returned the error: {}", tla_module, e diff --git a/rs/tla_instrumentation/tla_instrumentation/src/lib.rs b/rs/tla_instrumentation/tla_instrumentation/src/lib.rs index 9ad8a9de608..6b754008bcd 100644 --- a/rs/tla_instrumentation/tla_instrumentation/src/lib.rs +++ b/rs/tla_instrumentation/tla_instrumentation/src/lib.rs @@ -2,12 +2,25 @@ pub mod checker; pub mod tla_state; pub mod tla_value; use std::cell::RefCell; +use std::fmt::Formatter; use std::mem; use std::rc::Rc; pub use tla_state::*; pub use tla_value::*; +#[derive(Clone, Debug)] +pub struct SourceLocation { + pub file: String, + pub line: String, +} + +impl std::fmt::Display for SourceLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{}: Line {}", self.file, self.line)) + } +} + #[derive(Clone, Debug)] pub struct Update { // TODO: do we want checks that only declared variables are set? @@ -76,11 +89,11 @@ impl Context { } } - fn call_function(&mut self) { + pub fn call_function(&mut self) { self.location.0.push(LocationStackElem::Placeholder); } - fn return_from_function(&mut self) { + pub fn return_from_function(&mut self) { let _f = self.location.0.pop().expect("No function in call stack"); } @@ -122,7 +135,7 @@ pub struct MessageHandlerState { } impl MessageHandlerState { - pub fn new(update: Update, global: GlobalState) -> Self { + pub fn new(update: Update, global: GlobalState, source_location: SourceLocation) -> Self { let locals = update.default_start_locals.clone(); let label = update.start_label.clone(); Self { @@ -131,6 +144,7 @@ impl MessageHandlerState { global, local: LocalState { locals, label }, responses: Vec::new(), + source_location, }), } } @@ -148,8 +162,9 @@ impl InstrumentationState { update: Update, global: GlobalState, globals_snapshotter: Rc GlobalState>, + source_location: SourceLocation, ) -> Self { - let state = MessageHandlerState::new(update, global); + let state = MessageHandlerState::new(update, global, source_location); Self { handler_state: Rc::new(RefCell::new(state)), state_pairs: Rc::new(RefCell::new(Vec::new())), @@ -177,9 +192,15 @@ pub fn log_request( method: &str, args: TlaValue, global: GlobalState, + source_location: SourceLocation, ) -> ResolvedStatePair { - // TODO: do we want to push the label to the location stack here, or just replace it? - state.context.location.0 = vec![LocationStackElem::Label(Label::new(label))]; + *state + .context + .location + .0 + .last_mut() + .expect("Asked to log a request, but the location stack is empty.") = + LocationStackElem::Label(Label::new(label)); let old_stage = mem::replace(&mut state.stage, Stage::Start); let start_state = match old_stage { Stage::End(start) => start, @@ -195,6 +216,7 @@ pub fn log_request( method: method.to_string(), args, }], + source_location, }, }; ResolvedStatePair::resolve( @@ -209,6 +231,7 @@ pub fn log_response( from: Destination, message: TlaValue, global: GlobalState, + source_location: SourceLocation, ) { let local = state.context.get_state(); let stage = &mut state.stage; @@ -222,6 +245,7 @@ pub fn log_response( global, local, responses: vec![ResponseBuffer { from, message }], + source_location, }); state.context.global = GlobalState::new(); state.context.locals = VarAssignment::new(); @@ -235,14 +259,10 @@ pub fn log_fn_return(state: &mut MessageHandlerState) { state.context.return_from_function() } -// TODO: Does this work for modeling arguments as non-deterministically chosen locals? -pub fn log_method_call(function: Update, global: GlobalState) -> MessageHandlerState { - MessageHandlerState::new(function, global) -} - pub fn log_method_return( state: &mut MessageHandlerState, global: GlobalState, + source_location: SourceLocation, ) -> ResolvedStatePair { let local = state.context.end_update(); @@ -256,6 +276,7 @@ pub fn log_method_return( global, local, requests: Vec::new(), + source_location, }, }; ResolvedStatePair::resolve( @@ -265,6 +286,16 @@ pub fn log_method_return( ) } +pub fn log_label(state: &mut MessageHandlerState, label: &str) { + *state + .context + .location + .0 + .last_mut() + .unwrap_or_else(|| panic!("Asked to log label {}, but the location stack empty", label)) = + LocationStackElem::Label(Label::new(label)); +} + /// Logs the value of local variables at the end of the current message handler. /// This might be called multiple times in a single message handler, in particular /// if the message handler is implemented through several functions, each of which @@ -340,7 +371,8 @@ macro_rules! tla_log_request { let res = TLA_INSTRUMENTATION_STATE.try_with(|state| { let mut handler_state = state.handler_state.borrow_mut(); let globals = (*state.globals_snapshotter)(); - let new_state_pair = $crate::log_request(&mut handler_state, $label, $to, $method, message.clone(), globals); + let location = $crate::SourceLocation { file: file!().to_string(), line: line!().to_string() }; + let new_state_pair = $crate::log_request(&mut handler_state, $label, $to, $method, message.clone(), globals, location); let mut state_pairs = state.state_pairs.borrow_mut(); state_pairs.push(new_state_pair); }); @@ -362,10 +394,11 @@ macro_rules! tla_log_request { macro_rules! tla_log_response { ($from:expr, $message:expr) => {{ let message = $message.to_tla_value(); + let location = $crate::SourceLocation { file: file!().to_string(), line: line!().to_string() }; let res = TLA_INSTRUMENTATION_STATE.try_with(|state| { let mut handler_state = state.handler_state.borrow_mut(); let globals = (*state.globals_snapshotter)(); - $crate::log_response(&mut handler_state, $from, message.clone(), globals); + $crate::log_response(&mut handler_state, $from, message.clone(), globals, location); }); match res { Ok(_) => (), @@ -389,3 +422,22 @@ macro_rules! tla_log_method_call { $crate::log_method_call($update, $global) }}; } + +#[macro_export] +macro_rules! tla_log_label { + ($label:expr) => {{ + let res = TLA_INSTRUMENTATION_STATE.try_with(|state| { + let mut handler_state = state.handler_state.borrow_mut(); + $crate::log_label(&mut handler_state, $label); + }); + match res { + Ok(_) => (), + Err(_) => { + println!( + "Asked to log label {}, but instrumentation not initialized", + $label + ); + } + }; + }}; +} diff --git a/rs/tla_instrumentation/tla_instrumentation/src/tla_state.rs b/rs/tla_instrumentation/tla_instrumentation/src/tla_state.rs index d9338b2f088..108dda76d35 100644 --- a/rs/tla_instrumentation/tla_instrumentation/src/tla_state.rs +++ b/rs/tla_instrumentation/tla_instrumentation/src/tla_state.rs @@ -1,13 +1,13 @@ use crate::tla_value::{TlaValue, ToTla}; +use crate::SourceLocation; use candid::CandidType; -use serde::Deserialize; use std::{ collections::{BTreeMap, BTreeSet}, fmt, fmt::{Display, Formatter}, }; -#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, CandidType, Deserialize)] +#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, CandidType)] pub struct VarAssignment(pub BTreeMap); impl VarAssignment { @@ -152,6 +152,7 @@ pub struct StartState { pub global: GlobalState, pub local: LocalState, pub responses: Vec, + pub source_location: SourceLocation, } #[derive(Debug)] @@ -159,6 +160,7 @@ pub struct EndState { pub global: GlobalState, pub local: LocalState, pub requests: Vec, + pub source_location: SourceLocation, } #[derive(Debug)] @@ -172,6 +174,8 @@ pub struct StatePair { pub struct ResolvedStatePair { pub start: GlobalState, pub end: GlobalState, + pub start_source_location: SourceLocation, + pub end_source_location: SourceLocation, } fn resolve_local_variable(name: &str, value: &TlaValue, process_id: &str) -> VarAssignment { @@ -280,6 +284,8 @@ impl ResolvedStatePair { .merge(resolved_requests) .merge(end_pc), ), + start_source_location: unresolved.start.source_location, + end_source_location: unresolved.end.source_location, } } } diff --git a/rs/tla_instrumentation/tla_instrumentation/src/tla_value.rs b/rs/tla_instrumentation/tla_instrumentation/src/tla_value.rs index 9031811750d..f346cf85728 100644 --- a/rs/tla_instrumentation/tla_instrumentation/src/tla_value.rs +++ b/rs/tla_instrumentation/tla_instrumentation/src/tla_value.rs @@ -1,12 +1,11 @@ use candid::{CandidType, Nat, Principal}; -use serde::Deserialize; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::{ fmt, fmt::{Display, Formatter}, }; -#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, CandidType, Deserialize)] +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, CandidType)] pub enum TlaValue { Set(BTreeSet), Record(BTreeMap), @@ -114,7 +113,7 @@ impl fmt::Debug for TlaValue { } } -#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, CandidType, Deserialize, Debug)] +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, CandidType, Debug)] pub struct TlaConstantAssignment { pub constants: BTreeMap, } diff --git a/rs/tla_instrumentation/tla_instrumentation/tests/basic_tests.rs b/rs/tla_instrumentation/tla_instrumentation/tests/basic_tests.rs new file mode 100644 index 00000000000..55c72a5f423 --- /dev/null +++ b/rs/tla_instrumentation/tla_instrumentation/tests/basic_tests.rs @@ -0,0 +1,23 @@ +use std::collections::BTreeMap; +use tla_instrumentation::{TlaValue, ToTla}; + +#[test] +fn size_test() { + let myval = TlaValue::Record(BTreeMap::from([ + ( + "field1".to_string(), + TlaValue::Function(BTreeMap::from([( + 1_u64.to_tla_value(), + true.to_tla_value(), + )])), + ), + ( + "field2".to_string(), + TlaValue::Variant { + tag: "tag".to_string(), + value: Box::new("abc".to_tla_value()), + }, + ), + ])); + assert_eq!(myval.size(), 6); +} diff --git a/rs/tla_instrumentation/tla_instrumentation/tests/common.rs b/rs/tla_instrumentation/tla_instrumentation/tests/common.rs new file mode 100644 index 00000000000..f891915dcba --- /dev/null +++ b/rs/tla_instrumentation/tla_instrumentation/tests/common.rs @@ -0,0 +1,62 @@ +use std::path::PathBuf; +use tla_instrumentation::checker::{check_tla_code_link, PredicateDescription}; +use tla_instrumentation::UpdateTrace; + +// Add JAVABASE/bin to PATH to make the Bazel-provided JRE available to scripts +fn set_java_path() { + let current_path = std::env::var("PATH").unwrap(); + let bazel_java = std::env::var("JAVABASE").unwrap(); + std::env::set_var("PATH", format!("{current_path}:{bazel_java}/bin")); +} + +/// Returns the path to the TLA module (e.g. `Foo.tla` -> `/home/me/tla/Foo.tla`). +/// TLA modules are read from $TLA_MODULES (space-separated list) +/// NOTE: this assumes unique basenames amongst the modules +pub fn get_tla_module_path(module: &str) -> PathBuf { + let modules = std::env::var("TLA_MODULES").expect( + "environment variable 'TLA_MODULES' should be a space-separated list of TLA modules", + ); + + modules + .split(" ") + .map(|f| f.into()) /* str -> PathBuf */ + .find(|f: &PathBuf| f.file_name().is_some_and(|file_name| file_name == module)) + .unwrap_or_else(|| { + panic!("Could not find TLA module {module}, check 'TLA_MODULES' is set correctly") + }) +} + +pub fn get_apalache_path() -> PathBuf { + let apalache = std::env::var("TLA_APALACHE_BIN") + .expect("environment variable 'TLA_APALACHE_BIN' should point to the apalache binary"); + let apalache = PathBuf::from(apalache); + + if !apalache.as_path().is_file() { + panic!("bad apalache bin from 'TLA_APALACHE_BIN': '{:?}'", apalache); + } + + apalache +} + +pub fn check_tla_trace(trace: &UpdateTrace) { + set_java_path(); + let update = trace.update.clone(); + for pair in &trace.state_pairs { + let constants = trace.constants.clone(); + println!("Constants: {:?}", constants); + // NOTE: the 'process_id" is actually the tla module name + let tla_module = format!("{}_Apalache.tla", update.process_id); + let tla_module = get_tla_module_path(&tla_module); + check_tla_code_link( + &get_apalache_path(), + PredicateDescription { + tla_module, + transition_predicate: "Next".to_string(), + predicate_parameters: Vec::new(), + }, + pair.clone(), + constants, + ) + .expect("TLA link check failed"); + } +} diff --git a/rs/tla_instrumentation/tla_instrumentation/tests/multiple_calls.rs b/rs/tla_instrumentation/tla_instrumentation/tests/multiple_calls.rs new file mode 100644 index 00000000000..9430adb11a5 --- /dev/null +++ b/rs/tla_instrumentation/tla_instrumentation/tests/multiple_calls.rs @@ -0,0 +1,328 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + ptr::addr_of_mut, +}; + +// Also possible to define a wrapper macro, in order to ensure that logging is only +// done when certain crate features are enabled +use tla_instrumentation::{ + tla_log_label, tla_log_locals, tla_log_request, tla_log_response, + tla_value::{TlaValue, ToTla}, + Destination, InstrumentationState, +}; +use tla_instrumentation_proc_macros::{tla_function, tla_update_method}; + +mod common; +use common::check_tla_trace; + +// Example of how to separate as much of the instrumentation code as possible from the main code +#[macro_use] +mod tla_stuff { + use crate::StructCanister; + use std::collections::BTreeSet; + + use candid::Nat; + + pub const PID: &str = "Multiple_Calls"; + pub const CAN_NAME: &str = "mycan"; + + use local_key::task_local; + use std::{collections::BTreeMap, sync::RwLock}; + use tla_instrumentation::{ + GlobalState, InstrumentationState, Label, TlaConstantAssignment, TlaValue, ToTla, Update, + UpdateTrace, VarAssignment, + }; + + task_local! { + pub static TLA_INSTRUMENTATION_STATE: InstrumentationState; + } + + pub static TLA_TRACES: RwLock> = RwLock::new(Vec::new()); + + pub fn tla_get_globals(c: &StructCanister) -> GlobalState { + let mut state = GlobalState::new(); + state.add("counter", c.counter.to_tla_value()); + state.add("empty_fun", TlaValue::Function(BTreeMap::new())); + state + } + + // #[macro_export] + macro_rules! tla_get_globals { + ($self:expr) => { + tla_stuff::tla_get_globals($self) + }; + } + + pub fn my_f_desc() -> Update { + Update { + default_start_locals: VarAssignment::new().add("my_local", 0_u64.to_tla_value()), + default_end_locals: VarAssignment::new(), + start_label: Label::new("Start_Label"), + end_label: Label::new("Done"), + process_id: PID.to_string(), + canister_name: CAN_NAME.to_string(), + post_process: |trace| { + let max_counter = trace + .iter() + .map( + |pair| match (pair.start.get("counter"), pair.end.get("counter")) { + ( + Some(TlaValue::Int(start_counter)), + Some(TlaValue::Int(end_counter)), + ) => start_counter.max(end_counter).clone(), + _ => Nat::from(0_u64), + }, + ) + .max(); + let constants = BTreeMap::from([ + ( + "MAX_COUNTER".to_string(), + max_counter.unwrap_or(Nat::from(0_u64)).to_tla_value(), + ), + ( + "My_Method_Process_Ids".to_string(), + BTreeSet::from([PID.to_string()]).to_tla_value(), + ), + ]); + let outgoing = format!("{}_to_{}", CAN_NAME, "othercan"); + let outgoing = outgoing.as_str(); + let incoming = format!("{}_to_{}", "othercan", CAN_NAME); + let incoming = incoming.as_str(); + for pair in trace { + for s in [&mut pair.start, &mut pair.end] { + if !s.0 .0.contains_key(outgoing) { + s.0 .0.insert( + outgoing.to_string(), + Vec::::new().to_tla_value(), + ); + } + if !s.0 .0.contains_key(incoming) { + s.0 .0.insert( + incoming.to_string(), + BTreeSet::::new().to_tla_value(), + ); + } + } + } + TlaConstantAssignment { constants } + }, + } + } +} + +use tla_stuff::{my_f_desc, CAN_NAME, PID, TLA_INSTRUMENTATION_STATE, TLA_TRACES}; + +struct StructCanister { + pub counter: u64, +} + +static mut GLOBAL: StructCanister = StructCanister { counter: 0 }; + +#[tla_function] +async fn call_maker() { + tla_log_request!( + "WaitForResponse", + Destination::new("othercan"), + "Target_Method", + 2_u64 + ); + tla_log_response!( + Destination::new("othercan"), + TlaValue::Variant { + tag: "Ok".to_string(), + value: Box::new(3_u64.to_tla_value()) + } + ); +} + +impl StructCanister { + #[tla_update_method(my_f_desc())] + pub async fn my_method(&mut self) { + self.counter += 1; + let mut my_local: u64 = self.counter; + tla_log_locals! {my_local: my_local}; + tla_log_label!("Phase1"); + call_maker().await; + self.counter += 1; + my_local = self.counter; + tla_log_locals! {my_local: my_local}; + tla_log_label!("Phase2"); + call_maker().await; + self.counter += 1; + my_local = self.counter; + // Note that this would not be necessary (and would be an error) if + // we defined my_local in default_end_locals in my_f_desc + tla_log_locals! {my_local: my_local}; + } +} + +#[test] +fn multiple_calls_test() { + unsafe { + let canister = &mut *addr_of_mut!(GLOBAL); + tokio_test::block_on(canister.my_method()); + } + let trace = &TLA_TRACES.read().unwrap()[0]; + assert_eq!( + trace.constants.to_map().get("MAX_COUNTER"), + Some(&3_u64.to_string()) + ); + let pairs = &trace.state_pairs; + println!("----------------"); + println!("State pairs:"); + for pair in pairs.iter() { + println!("{:?}", pair.start); + println!("{:?}", pair.end); + } + println!("----------------"); + assert_eq!(pairs.len(), 3); + let first = &pairs[0]; + assert_eq!(first.start.get("counter"), Some(&0_u64.to_tla_value())); + assert_eq!(first.end.get("counter"), Some(&1_u64.to_tla_value())); + + assert_eq!( + first.start.get("my_local"), + Some(BTreeMap::from([(PID, 0_u64)]).to_tla_value()).as_ref() + ); + assert_eq!( + first.end.get("my_local"), + Some(BTreeMap::from([(PID, 1_u64)]).to_tla_value()).as_ref() + ); + + let outgoing = format!("{}_to_{}", CAN_NAME, "othercan"); + let outgoing = outgoing.as_str(); + let incoming = format!("{}_to_{}", "othercan", CAN_NAME); + let incoming = incoming.as_str(); + + assert_eq!( + first.start.get(outgoing), + Some(&Vec::::new().to_tla_value()) + ); + assert_eq!( + first.end.get(outgoing), + Some( + &vec![TlaValue::Record(BTreeMap::from([ + ("caller".to_string(), PID.to_tla_value()), + ( + "method_and_args".to_string(), + TlaValue::Variant { + tag: "Target_Method".to_string(), + value: Box::new(2_u64.to_tla_value()) + } + ) + ]))] + .to_tla_value() + ) + ); + + assert_eq!( + first.start.get(incoming), + Some(&BTreeSet::::new().to_tla_value()) + ); + assert_eq!( + first.end.get(incoming), + Some(&BTreeSet::::new().to_tla_value()) + ); + + let second = &pairs[1]; + + assert_eq!(second.start.get("counter"), Some(&1_u64.to_tla_value())); + assert_eq!(second.end.get("counter"), Some(&2_u64.to_tla_value())); + + assert_eq!( + second.start.get("my_local"), + Some(BTreeMap::from([(PID, 1_u64)]).to_tla_value()).as_ref() + ); + assert_eq!( + second.end.get("my_local"), + Some(BTreeMap::from([(PID, 2_u64)]).to_tla_value()).as_ref() + ); + + assert_eq!( + second.start.get(incoming), + Some( + &BTreeSet::from([TlaValue::Record(BTreeMap::from([ + ("caller".to_string(), PID.to_tla_value()), + ( + "response".to_string(), + TlaValue::Variant { + tag: "Ok".to_string(), + value: Box::new(3_u64.to_tla_value()) + } + ) + ]))]) + .to_tla_value() + ) + ); + assert_eq!( + second.end.get(incoming), + Some(&BTreeSet::::new().to_tla_value()) + ); + + assert_eq!( + second.start.get(outgoing), + Some(&Vec::::new().to_tla_value()) + ); + assert_eq!( + second.end.get(outgoing), + Some( + &vec![TlaValue::Record(BTreeMap::from([ + ("caller".to_string(), PID.to_tla_value()), + ( + "method_and_args".to_string(), + TlaValue::Variant { + tag: "Target_Method".to_string(), + value: Box::new(2_u64.to_tla_value()) + } + ) + ]))] + .to_tla_value() + ) + ); + + let third = &pairs[2]; + + assert_eq!(third.start.get("counter"), Some(&2_u64.to_tla_value())); + assert_eq!(third.end.get("counter"), Some(&3_u64.to_tla_value())); + + assert_eq!( + third.start.get("my_local"), + Some(BTreeMap::from([(PID, 2_u64)]).to_tla_value()).as_ref() + ); + assert_eq!( + third.end.get("my_local"), + Some(BTreeMap::from([(PID, 3_u64)]).to_tla_value()).as_ref() + ); + + assert_eq!( + third.start.get(incoming), + Some( + &BTreeSet::from([TlaValue::Record(BTreeMap::from([ + ("caller".to_string(), PID.to_tla_value()), + ( + "response".to_string(), + TlaValue::Variant { + tag: "Ok".to_string(), + value: Box::new(3_u64.to_tla_value()) + } + ) + ]))]) + .to_tla_value() + ) + ); + assert_eq!( + third.end.get(incoming), + Some(&BTreeSet::::new().to_tla_value()) + ); + + assert_eq!( + third.start.get(outgoing), + Some(&Vec::::new().to_tla_value()) + ); + assert_eq!( + third.end.get(outgoing), + Some(&Vec::::new().to_tla_value()) + ); + + check_tla_trace(trace); +} diff --git a/rs/tla_instrumentation/tla_instrumentation/tests/structs.rs b/rs/tla_instrumentation/tla_instrumentation/tests/structs.rs index 375fad20ff9..9cae519c109 100644 --- a/rs/tla_instrumentation/tla_instrumentation/tests/structs.rs +++ b/rs/tla_instrumentation/tla_instrumentation/tests/structs.rs @@ -1,19 +1,20 @@ use std::{ collections::{BTreeMap, BTreeSet}, - path::PathBuf, ptr::addr_of_mut, }; // Also possible to define a wrapper macro, in order to ensure that logging is only // done when certain crate features are enabled use tla_instrumentation::{ - checker::{check_tla_code_link, PredicateDescription}, tla_log_locals, tla_log_request, tla_log_response, tla_value::{TlaValue, ToTla}, Destination, InstrumentationState, }; use tla_instrumentation_proc_macros::tla_update_method; +mod common; +use common::check_tla_trace; + // Example of how to separate as much of the instrumentation code as possible from the main code #[macro_use] mod tla_stuff { @@ -148,51 +149,6 @@ impl StructCanister { } } -// Add JAVABASE/bin to PATH to make the Bazel-provided JRE available to scripts -fn set_java_path() { - let current_path = std::env::var("PATH").unwrap(); - let bazel_java = std::env::var("JAVABASE").unwrap(); - std::env::set_var("PATH", format!("{current_path}:{bazel_java}/bin")); -} - -/// Returns the path to the TLA module (e.g. `Foo.tla` -> `/home/me/tla/Foo.tla`). -/// TLA modules are read from $TLA_MODULES (space-separated list) -/// NOTE: this assumes unique basenames amongst the modules -fn get_tla_module_path(module: &str) -> PathBuf { - let modules = std::env::var("TLA_MODULES").expect( - "environment variable 'TLA_MODULES' should be a space-separated list of TLA modules", - ); - - modules - .split(" ") - .map(|f| f.into()) /* str -> PathBuf */ - .find(|f: &PathBuf| f.file_name().is_some_and(|file_name| file_name == module)) - .unwrap_or_else(|| { - panic!("Could not find TLA module {module}, check 'TLA_MODULES' is set correctly") - }) -} - -#[test] -fn size_test() { - let myval = TlaValue::Record(BTreeMap::from([ - ( - "field1".to_string(), - TlaValue::Function(BTreeMap::from([( - 1_u64.to_tla_value(), - true.to_tla_value(), - )])), - ), - ( - "field2".to_string(), - TlaValue::Variant { - tag: "tag".to_string(), - value: Box::new("abc".to_tla_value()), - }, - ), - ])); - assert_eq!(myval.size(), 6); -} - #[test] fn struct_test() { unsafe { @@ -305,33 +261,5 @@ fn struct_test() { Some(&Vec::::new().to_tla_value()) ); - set_java_path(); - - let apalache = std::env::var("TLA_APALACHE_BIN") - .expect("environment variable 'TLA_APALACHE_BIN' should point to the apalache binary"); - let apalache = PathBuf::from(apalache); - - if !apalache.as_path().is_file() { - panic!("bad apalache bin from 'TLA_APALACHE_BIN': '{:?}'", apalache); - } - - let update = trace.update.clone(); - for pair in &trace.state_pairs { - let constants = trace.constants.clone(); - println!("Constants: {:?}", constants); - // NOTE: the 'process_id" is actually the tla module name - let tla_module = format!("{}_Apalache.tla", update.process_id); - let tla_module = get_tla_module_path(&tla_module); - check_tla_code_link( - &apalache, - PredicateDescription { - tla_module, - transition_predicate: "Next".to_string(), - predicate_parameters: Vec::new(), - }, - pair.clone(), - constants, - ) - .expect("TLA link check failed"); - } + check_tla_trace(trace); } diff --git a/rs/tla_instrumentation/tla_instrumentation_proc_macros/src/lib.rs b/rs/tla_instrumentation/tla_instrumentation_proc_macros/src/lib.rs index bf21e053388..3d5fd641186 100644 --- a/rs/tla_instrumentation/tla_instrumentation_proc_macros/src/lib.rs +++ b/rs/tla_instrumentation/tla_instrumentation_proc_macros/src/lib.rs @@ -94,6 +94,7 @@ pub fn tla_update_method(attr: TokenStream, item: TokenStream) -> TokenStream { block: _, } = input_fn; + let original_name = sig.ident.to_string(); let mangled_name = syn::Ident::new(&format!("_tla_impl_{}", sig.ident), sig.ident.span()); modified_fn.sig.ident = mangled_name.clone(); @@ -125,14 +126,16 @@ pub fn tla_update_method(attr: TokenStream, item: TokenStream) -> TokenStream { let raw_ptr = self as *const _; let snapshotter = Rc::new(move || { unsafe { tla_get_globals!(&*raw_ptr) } }); let update = #attr2; + let start_location = tla_instrumentation::SourceLocation { file: "Unknown file".to_string(), line: format!("Start of {}", #original_name) }; + let end_location = tla_instrumentation::SourceLocation { file: "Unknown file".to_string(), line: format!("End of {}", #original_name) }; let mut pinned = Box::pin(TLA_INSTRUMENTATION_STATE.scope( - tla_instrumentation::InstrumentationState::new(update.clone(), globals, snapshotter), + tla_instrumentation::InstrumentationState::new(update.clone(), globals, snapshotter, start_location), async move { let res = self.#mangled_name(#(#args),*).await; let globals = tla_get_globals!(self); let state: InstrumentationState = TLA_INSTRUMENTATION_STATE.get(); let mut handler_state = state.handler_state.borrow_mut(); - let state_pair = tla_instrumentation::log_method_return(&mut handler_state, globals); + let state_pair = tla_instrumentation::log_method_return(&mut handler_state, globals, end_location); let mut state_pairs = state.state_pairs.borrow_mut(); state_pairs.push(state_pair); res @@ -173,3 +176,76 @@ pub fn tla_update_method(attr: TokenStream, item: TokenStream) -> TokenStream { output.into() } + +#[proc_macro_attribute] +pub fn tla_function(_attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the input tokens of the attribute and the function + let input_fn = parse_macro_input!(item as ItemFn); + + let mut modified_fn = input_fn.clone(); + + // Deconstruct the function elements + let ItemFn { + attrs, + vis, + sig, + block: _, + } = input_fn; + + let mangled_name = syn::Ident::new(&format!("_tla_impl_{}", sig.ident), sig.ident.span()); + modified_fn.sig.ident = mangled_name.clone(); + + let has_receiver = sig.inputs.iter().any(|arg| match arg { + syn::FnArg::Receiver(_) => true, + syn::FnArg::Typed(_) => false, + }); + // Creating the modified original function which calls f_impl + let args: Vec<_> = sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(pat_type) => Some(&*pat_type.pat), + }) + .collect(); + + let asyncness = sig.asyncness; + + let call = match (asyncness.is_some(), has_receiver) { + (true, true) => quote! { self.#mangled_name(#(#args),*).await }, + (true, false) => quote! { #mangled_name(#(#args),*).await }, + (false, true) => quote! { self.#mangled_name(#(#args),*) }, + (false, false) => quote! { #mangled_name(#(#args),*) }, + }; + + let output = quote! { + #modified_fn + + #(#attrs)* #vis #sig { + TLA_INSTRUMENTATION_STATE.try_with(|state| { + { + let mut handler_state = state.handler_state.borrow_mut(); + handler_state.context.call_function(); + } + }).unwrap_or_else(|e| + // TODO(RES-152): fail if there's an error and if we're in some kind of strict mode? + () + ); + + + let res = #call; + TLA_INSTRUMENTATION_STATE.try_with(|state| { + { + let mut handler_state = state.handler_state.borrow_mut(); + handler_state.context.return_from_function(); + } + }).unwrap_or_else(|e| + // TODO(RES-152): fail if there's an error and if we're in some kind of strict mode? + () + ); + res + } + }; + + output.into() +} From 3006ab867a117d5c88f14f7b940d590905e279aa Mon Sep 17 00:00:00 2001 From: Andriy Berestovskyy Date: Thu, 24 Oct 2024 13:12:26 +0200 Subject: [PATCH 04/22] feat: EXC-1758: Evict sandboxes based on their RSS (#2197) This allows to increase the number of sandbox processes without risking the OOM as the total RSS size of sandboxes is still limited. This PR also increases the number of sandbox processes to 5k. --- Cargo.lock | 1 + rs/canister_sandbox/BUILD.bazel | 1 + rs/canister_sandbox/Cargo.toml | 1 + .../sandbox_process_eviction.rs | 108 ++++++++++++++--- .../sandboxed_execution_controller.rs | 112 ++++++++++++++---- rs/config/src/embedders.rs | 19 ++- rs/pocket_ic_server/src/pocket_ic.rs | 4 +- 7 files changed, 194 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91f394c6c78..bfef318334f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6034,6 +6034,7 @@ dependencies = [ "memory_tracker", "mockall 0.13.0", "nix 0.24.3", + "num-traits", "once_cell", "prometheus", "rayon", diff --git a/rs/canister_sandbox/BUILD.bazel b/rs/canister_sandbox/BUILD.bazel index f48d7d2265f..4ac4e74205b 100644 --- a/rs/canister_sandbox/BUILD.bazel +++ b/rs/canister_sandbox/BUILD.bazel @@ -28,6 +28,7 @@ DEPENDENCIES = [ "@crate_index//:libc", "@crate_index//:libflate", "@crate_index//:nix", + "@crate_index//:num-traits", "@crate_index//:once_cell", "@crate_index//:prometheus", "@crate_index//:rayon", diff --git a/rs/canister_sandbox/Cargo.toml b/rs/canister_sandbox/Cargo.toml index 06b276d9fff..34107001e3c 100644 --- a/rs/canister_sandbox/Cargo.toml +++ b/rs/canister_sandbox/Cargo.toml @@ -32,6 +32,7 @@ libc = { workspace = true } libflate = { workspace = true } memory_tracker = { path = "../memory_tracker" } nix = { workspace = true } +num-traits = { workspace = true } once_cell = "1.8" prometheus = { workspace = true } rayon = { workspace = true } diff --git a/rs/canister_sandbox/src/replica_controller/sandbox_process_eviction.rs b/rs/canister_sandbox/src/replica_controller/sandbox_process_eviction.rs index 433c86239ac..624a7ee495c 100644 --- a/rs/canister_sandbox/src/replica_controller/sandbox_process_eviction.rs +++ b/rs/canister_sandbox/src/replica_controller/sandbox_process_eviction.rs @@ -1,11 +1,13 @@ +use num_traits::ops::saturating::SaturatingAdd; use std::time::Instant; -use ic_types::CanisterId; +use ic_types::{CanisterId, NumBytes}; #[derive(Clone, Eq, PartialEq, Debug)] pub(crate) struct EvictionCandidate { pub id: CanisterId, pub last_used: Instant, + pub rss: NumBytes, } /// Evicts the least recently used candidates in order to bring the number of @@ -26,30 +28,30 @@ pub(crate) struct EvictionCandidate { /// 4. Return the evicted candidates. pub(crate) fn evict( mut candidates: Vec, - min_count_threshold: usize, + total_rss: NumBytes, max_count_threshold: usize, last_used_threshold: Instant, + max_sandboxes_rss: NumBytes, ) -> Vec { candidates.sort_by_key(|x| x.last_used); let evict_at_least = candidates.len().saturating_sub(max_count_threshold); - let evict_at_most = candidates.len().saturating_sub(min_count_threshold); let mut evicted = vec![]; + let mut evicted_rss = NumBytes::new(0); for candidate in candidates.into_iter() { - if evicted.len() >= evict_at_most { - // Cannot evict anymore because at least `min_count_threshold` - // should remain not evicted. - break; - } - if candidate.last_used >= last_used_threshold && evicted.len() >= evict_at_least { + if candidate.last_used >= last_used_threshold + && evicted.len() >= evict_at_least + && total_rss <= max_sandboxes_rss.saturating_add(&evicted_rss) + { // We have already evicted the minimum required number of candidates // and all the remaining candidates were not idle the recent // `last_used_threshold` time window. No need to evict more. break; } - evicted.push(candidate) + evicted_rss = evicted_rss.saturating_add(&candidate.rss); + evicted.push(candidate); } evicted @@ -60,12 +62,13 @@ mod tests { use std::time::{Duration, Instant}; use ic_test_utilities_types::ids::canister_test_id; + use ic_types::NumBytes; use super::{evict, EvictionCandidate}; #[test] fn evict_empty() { - assert_eq!(evict(vec![], 0, 0, Instant::now()), vec![],); + assert_eq!(evict(vec![], 0.into(), 0, Instant::now(), 0.into()), vec![],); } #[test] @@ -76,9 +79,10 @@ mod tests { candidates.push(EvictionCandidate { id: canister_test_id(i), last_used: now, + rss: 0.into(), }); } - assert_eq!(evict(candidates, 0, 10, now,), vec![],); + assert_eq!(evict(candidates, 0.into(), 10, now, 0.into()), vec![],); } #[test] @@ -89,10 +93,11 @@ mod tests { candidates.push(EvictionCandidate { id: canister_test_id(i), last_used: now + Duration::from_secs(100 - i), + rss: 0.into(), }); } assert_eq!( - evict(candidates.clone(), 0, 90, now,), + evict(candidates.clone(), 0.into(), 90, now, 0.into()), candidates.into_iter().rev().take(10).collect::>() ); } @@ -105,10 +110,17 @@ mod tests { candidates.push(EvictionCandidate { id: canister_test_id(i), last_used: now - Duration::from_secs(i), + rss: 0.into(), }); } assert_eq!( - evict(candidates.clone(), 0, 100, now - Duration::from_secs(50)), + evict( + candidates.clone(), + 0.into(), + 100, + now - Duration::from_secs(50), + 0.into() + ), candidates.into_iter().rev().take(49).collect::>() ); } @@ -120,15 +132,73 @@ mod tests { for i in 0..100 { candidates.push(EvictionCandidate { id: canister_test_id(i), - last_used: now - Duration::from_secs(i + 1), + last_used: now - Duration::from_secs(i + 1) + Duration::from_secs(10), + rss: 0.into(), }); } assert_eq!( - evict(candidates.clone(), 10, 100, now), + evict(candidates.clone(), 0.into(), 100, now, 0.into()), candidates.into_iter().rev().take(90).collect::>() ); } + #[test] + fn evict_none_due_to_rss() { + let mut candidates = vec![]; + let now = Instant::now(); + let mut total_rss = NumBytes::new(0); + for i in 0..100 { + candidates.push(EvictionCandidate { + id: canister_test_id(i), + last_used: now, + rss: 50.into(), + }); + total_rss += 50.into(); + } + assert_eq!( + evict(candidates.clone(), total_rss, 100, now, total_rss), + vec![] + ); + } + + #[test] + fn evict_some_due_to_rss() { + let mut candidates = vec![]; + let now = Instant::now(); + let mut total_rss = NumBytes::new(0); + for i in 0..100 { + candidates.push(EvictionCandidate { + id: canister_test_id(i), + last_used: now, + rss: 50.into(), + }); + total_rss += 50.into(); + } + assert_eq!( + evict(candidates.clone(), total_rss, 100, now, total_rss / 2), + candidates.into_iter().take(50).collect::>() + ); + } + + #[test] + fn evict_all_due_to_rss() { + let mut candidates = vec![]; + let now = Instant::now(); + let mut total_rss = NumBytes::new(0); + for i in 0..100 { + candidates.push(EvictionCandidate { + id: canister_test_id(i), + last_used: now, + rss: 50.into(), + }); + total_rss += 50.into(); + } + assert_eq!( + evict(candidates.clone(), total_rss, 100, now, 0.into()), + candidates + ); + } + #[test] fn evict_all() { let mut candidates = vec![]; @@ -137,8 +207,12 @@ mod tests { candidates.push(EvictionCandidate { id: canister_test_id(i), last_used: now - Duration::from_secs(i + 1), + rss: 0.into(), }); } - assert_eq!(evict(candidates.clone(), 0, 100, now).len(), 100); + assert_eq!( + evict(candidates.clone(), 0.into(), 100, now, 0.into()).len(), + 100 + ); } } diff --git a/rs/canister_sandbox/src/replica_controller/sandboxed_execution_controller.rs b/rs/canister_sandbox/src/replica_controller/sandboxed_execution_controller.rs index 5efc85d2358..a0173756d5d 100644 --- a/rs/canister_sandbox/src/replica_controller/sandboxed_execution_controller.rs +++ b/rs/canister_sandbox/src/replica_controller/sandboxed_execution_controller.rs @@ -27,8 +27,9 @@ use ic_replicated_state::canister_state::execution_state::{ use ic_replicated_state::{EmbedderCache, ExecutionState, ExportedFunctions, Memory, PageMap}; use ic_types::ingress::WasmResult; use ic_types::methods::{FuncRef, WasmMethod}; -use ic_types::{CanisterId, NumInstructions}; +use ic_types::{CanisterId, NumBytes, NumInstructions}; use ic_wasm_types::CanisterModule; +use num_traits::ops::saturating::SaturatingSub; #[cfg(target_os = "linux")] use prometheus::IntGauge; use prometheus::{Histogram, HistogramVec, IntCounter, IntCounterVec}; @@ -59,9 +60,20 @@ use ic_types::ExecutionRound; const SANDBOX_PROCESS_UPDATE_INTERVAL: Duration = Duration::from_secs(10); -// The percentage of sandbox processes to evict in one go in order to amortize -// for the eviction cost. -const SANDBOX_PROCESS_EVICTION_PERCENT: usize = 20; +/// The number of sandbox processes to evict in one go in order to amortize +/// for the eviction cost. A large number could lead to the eviction +/// of many sandboxes and increased system load. The number was chosen +/// based on the assumption of 800 canister executions per round +/// distributed across 4 execution cores. +const SANDBOX_PROCESSES_TO_EVICT: usize = 200; + +/// The RSS to evict in one go in order to amortize for the eviction cost (1 GiB). +const SANDBOX_PROCESSES_RSS_TO_EVICT: NumBytes = NumBytes::new(1024 * 1024 * 1024); + +/// By default, assume each sandbox process consumes 50 MiB of RSS. +/// The actual memory usage is updated asynchronously. +/// See `monitor_and_evict_sandbox_processes` +const DEFAULT_SANDBOX_PROCESS_RSS: NumBytes = NumBytes::new(50 * 1024 * 1024); const SANDBOXED_EXECUTION_INVALID_MEMORY_SIZE: &str = "sandboxed_execution_invalid_memory_size"; @@ -460,6 +472,7 @@ enum Backend { #[derive(Clone)] struct SandboxProcessStats { last_used: std::time::Instant, + rss: NumBytes, } enum SandboxProcessStatus { @@ -637,9 +650,9 @@ pub struct SandboxedExecutionController { /// - An entry is removed from the registry only if it is in the `evicted` /// state and the strong reference count reaches zero. backends: Arc>>, - min_sandbox_count: usize, max_sandbox_count: usize, max_sandbox_idle_time: Duration, + max_sandboxes_rss: NumBytes, trace_execution: FlagStatus, logger: ReplicaLogger, /// Executable and arguments to be passed to `canister_sandbox` which are @@ -659,7 +672,7 @@ impl Drop for SandboxedExecutionController { // Evict all the sandbox processes. let mut guard = self.backends.lock().unwrap(); - evict_sandbox_processes(&mut guard, 0, 0, Duration::default()); + evict_sandbox_processes(&mut guard, 0, Duration::default(), 0.into()); // Terminate the Sandbox Launcher process. self.launcher_service @@ -1060,9 +1073,9 @@ impl SandboxedExecutionController { ) -> std::io::Result { let launcher_exec_argv = create_launcher_argv(embedder_config).expect("No sandbox_launcher binary found"); - let min_sandbox_count = embedder_config.min_sandbox_count; let max_sandbox_count = embedder_config.max_sandbox_count; let max_sandbox_idle_time = embedder_config.max_sandbox_idle_time; + let max_sandboxes_rss = embedder_config.max_sandboxes_rss; let trace_execution = embedder_config.trace_execution; let sandbox_exec_argv = create_sandbox_argv(embedder_config).expect("No canister_sandbox binary found"); @@ -1079,9 +1092,9 @@ impl SandboxedExecutionController { logger_copy, backends_copy, metrics_copy, - min_sandbox_count, max_sandbox_count, max_sandbox_idle_time, + max_sandboxes_rss, rx, ); }); @@ -1108,9 +1121,9 @@ impl SandboxedExecutionController { Ok(Self { backends, - min_sandbox_count, max_sandbox_count, max_sandbox_idle_time, + max_sandboxes_rss, trace_execution, logger, sandbox_exec_argv, @@ -1129,13 +1142,15 @@ impl SandboxedExecutionController { #[allow(unused_variables)] logger: ReplicaLogger, backends: Arc>>, metrics: Arc, - min_sandbox_count: usize, max_sandbox_count: usize, max_sandbox_idle_time: Duration, + max_sandboxes_rss: NumBytes, stop_request: Receiver, ) { loop { let sandbox_processes = get_sandbox_process_stats(&backends); + #[allow(unused_mut)] // for MacOS + let mut sandbox_processes_rss = HashMap::with_capacity(sandbox_processes.len()); #[cfg(target_os = "linux")] { @@ -1145,7 +1160,7 @@ impl SandboxedExecutionController { // For all processes requested, get their memory usage and report // it keyed by pid. Ignore processes failures to get - for (sandbox_process, stats, status) in &sandbox_processes { + for (canister_id, sandbox_process, stats, status) in &sandbox_processes { let pid = sandbox_process.pid; let mut process_rss = 0; if let Ok(kib) = process_os_metrics::get_anon_rss(pid) { @@ -1154,6 +1169,8 @@ impl SandboxedExecutionController { metrics .sandboxed_execution_subprocess_anon_rss .observe(kib as f64); + let bytes = NumBytes::new(kib * 1024); + sandbox_processes_rss.insert(*canister_id, bytes); } else { warn!(logger, "Unable to get anon RSS for pid {}", pid); } @@ -1203,7 +1220,7 @@ impl SandboxedExecutionController { let now = std::time::Instant::now(); // For all processes requested, get their memory usage and report // it keyed by pid. Ignore processes failures to get - for (_sandbox_process, stats, status) in &sandbox_processes { + for (_canister_id, _sandbox_process, stats, status) in &sandbox_processes { let time_since_last_usage = now .checked_duration_since(stats.last_used) .unwrap_or_else(|| std::time::Duration::from_secs(0)); @@ -1224,11 +1241,12 @@ impl SandboxedExecutionController { { let mut guard = backends.lock().unwrap(); + update_sandbox_processes_rss(&mut guard, sandbox_processes_rss); evict_sandbox_processes( &mut guard, - min_sandbox_count, max_sandbox_count, max_sandbox_idle_time, + max_sandboxes_rss, ); } @@ -1259,17 +1277,23 @@ impl SandboxedExecutionController { } => sandbox_process.upgrade().map(|p| (p, stats)), Backend::Empty => None, }; - if let Some((sandbox_process, _stats)) = sandbox_process_and_stats { + if let Some((sandbox_process, old_stats)) = sandbox_process_and_stats { let now = std::time::Instant::now(); if self.max_sandbox_count > 0 { *backend = Backend::Active { sandbox_process: Arc::clone(&sandbox_process), - stats: SandboxProcessStats { last_used: now }, + stats: SandboxProcessStats { + last_used: now, + rss: old_stats.rss, + }, }; } else { *backend = Backend::Evicted { sandbox_process: Arc::downgrade(&sandbox_process), - stats: SandboxProcessStats { last_used: now }, + stats: SandboxProcessStats { + last_used: now, + rss: old_stats.rss, + }, }; } return sandbox_process; @@ -1277,14 +1301,22 @@ impl SandboxedExecutionController { } let _timer = self.metrics.sandboxed_execution_spawn_process.start_timer(); - if guard.len() > self.max_sandbox_count { - let to_evict = self.max_sandbox_count * SANDBOX_PROCESS_EVICTION_PERCENT / 100; - let max_active_sandboxes = self.max_sandbox_count.saturating_sub(to_evict); + let total_rss = total_sandboxes_rss(&guard); + if guard.len() > self.max_sandbox_count || total_rss > self.max_sandboxes_rss { + // Make room for a few sandboxes in one go, assuming each sandbox + // takes `DEFAULT_SANDBOX_PROCESS_RSS`. + let max_active_sandboxes = self + .max_sandbox_count + .saturating_sub(SANDBOX_PROCESSES_TO_EVICT); + let max_sandboxes_rss = self + .max_sandboxes_rss + .saturating_sub(&SANDBOX_PROCESSES_RSS_TO_EVICT); + evict_sandbox_processes( &mut guard, - self.min_sandbox_count, max_active_sandboxes, self.max_sandbox_idle_time, + max_sandboxes_rss, ); } @@ -1310,7 +1342,10 @@ impl SandboxedExecutionController { let now = std::time::Instant::now(); let backend = Backend::Active { sandbox_process: Arc::clone(&sandbox_process), - stats: SandboxProcessStats { last_used: now }, + stats: SandboxProcessStats { + last_used: now, + rss: DEFAULT_SANDBOX_PROCESS_RSS, + }, }; (*guard).insert(canister_id, backend); @@ -1639,14 +1674,38 @@ fn wrap_remote_memory( SandboxMemoryHandle::new(Arc::new(opened_memory)) } +/// Updates sandbox processes RSS. +fn update_sandbox_processes_rss( + backends: &mut HashMap, + sandbox_processes_rss: HashMap, +) { + for (id, rss) in sandbox_processes_rss { + backends.entry(id).and_modify(|backend| match backend { + Backend::Active { stats, .. } | Backend::Evicted { stats, .. } => stats.rss = rss, + Backend::Empty => {} + }); + } +} + +/// Returns the total RSS for active sandboxes. +fn total_sandboxes_rss(backends: &HashMap) -> NumBytes { + backends + .values() + .map(|backend| match backend { + Backend::Active { stats, .. } => stats.rss, + Backend::Evicted { .. } | Backend::Empty => 0.into(), + }) + .sum() +} + // Evicts some sandbox process backends according to the heuristics of the // `sandbox_process_eviction::evict()` function. See the comments of that // function for the explanation of the threshold parameters. fn evict_sandbox_processes( backends: &mut HashMap, - min_active_sandboxes: usize, max_active_sandboxes: usize, max_sandbox_idle_time: Duration, + max_sandboxes_rss: NumBytes, ) { // Remove the already terminated processes. backends.retain(|_id, backend| match backend { @@ -1668,6 +1727,7 @@ fn evict_sandbox_processes( Backend::Active { stats, .. } => Some(EvictionCandidate { id: *id, last_used: stats.last_used, + rss: stats.rss, }), Backend::Evicted { .. } | Backend::Empty => None, }) @@ -1688,9 +1748,10 @@ fn evict_sandbox_processes( let evicted = sandbox_process_eviction::evict( candidates, - min_active_sandboxes, + total_sandboxes_rss(backends), max_active_sandboxes, last_used_threshold, + max_sandboxes_rss, ); // Actually evict all the selected eviction candidates. @@ -1716,19 +1777,21 @@ fn evict_sandbox_processes( fn get_sandbox_process_stats( backends: &Arc>>, ) -> Vec<( + CanisterId, Arc, SandboxProcessStats, SandboxProcessStatus, )> { let guard = backends.lock().unwrap(); let mut result = vec![]; - for backend in guard.values() { + for (canister_id, backend) in guard.iter() { match backend { Backend::Active { sandbox_process, stats, } => { result.push(( + *canister_id, Arc::clone(sandbox_process), stats.clone(), SandboxProcessStatus::Active, @@ -1740,6 +1803,7 @@ fn get_sandbox_process_stats( } => { if let Some(strong_reference) = sandbox_process.upgrade() { result.push(( + *canister_id, strong_reference, stats.clone(), SandboxProcessStatus::Evicted, diff --git a/rs/config/src/embedders.rs b/rs/config/src/embedders.rs index 6bfc647c90e..441e91b8477 100644 --- a/rs/config/src/embedders.rs +++ b/rs/config/src/embedders.rs @@ -42,18 +42,17 @@ const DEFAULT_WASMTIME_RAYON_COMPILATION_THREADS: usize = 10; /// The number of rayon threads use for the parallel page copying optimization. const DEFAULT_PAGE_ALLOCATOR_THREADS: usize = 8; -/// Sandbox process eviction does not activate if the number of sandbox -/// processes is below this threshold. -pub(crate) const DEFAULT_MIN_SANDBOX_COUNT: usize = 500; - /// Sandbox process eviction ensures that the number of sandbox processes is /// always below this threshold. -pub(crate) const DEFAULT_MAX_SANDBOX_COUNT: usize = 2_000; +pub(crate) const DEFAULT_MAX_SANDBOX_COUNT: usize = 5_000; /// A sandbox process may be evicted after it has been idle for this /// duration and sandbox process eviction is activated. pub(crate) const DEFAULT_MAX_SANDBOX_IDLE_TIME: Duration = Duration::from_secs(30 * 60); +/// Sandbox processes may be evicted if their total RSS exceeds 50 GiB. +pub(crate) const DEFAULT_MAX_SANDBOXES_RSS: NumBytes = NumBytes::new(50 * 1024 * 1024 * 1024); + /// The maximum number of pages that a message dirties without optimizing dirty /// page copying by triggering a new execution slice for copying pages. /// This default is 1 GiB. @@ -197,10 +196,6 @@ pub struct Config { /// execution is allowed to produce. pub stable_memory_dirty_page_limit: StableMemoryPageLimit, - /// Sandbox process eviction does not activate if the number of sandbox - /// processes is below this threshold. - pub min_sandbox_count: usize, - /// Sandbox process eviction ensures that the number of sandbox processes is /// always below this threshold. pub max_sandbox_count: usize, @@ -209,6 +204,10 @@ pub struct Config { /// duration and sandbox process eviction is activated. pub max_sandbox_idle_time: Duration, + /// Sandbox processes may be evicted if their total RSS exceeds + /// the specified amount in bytes. + pub max_sandboxes_rss: NumBytes, + /// The type of the local subnet. The default value here should be replaced /// with the correct value at runtime when the hypervisor is created. pub subnet_type: SubnetType, @@ -265,9 +264,9 @@ impl Config { upgrade: STABLE_MEMORY_ACCESSED_PAGE_LIMIT_UPGRADE, query: STABLE_MEMORY_ACCESSED_PAGE_LIMIT_QUERY, }, - min_sandbox_count: DEFAULT_MIN_SANDBOX_COUNT, max_sandbox_count: DEFAULT_MAX_SANDBOX_COUNT, max_sandbox_idle_time: DEFAULT_MAX_SANDBOX_IDLE_TIME, + max_sandboxes_rss: DEFAULT_MAX_SANDBOXES_RSS, subnet_type: SubnetType::Application, dirty_page_overhead: NumInstructions::new(0), trace_execution: FlagStatus::Disabled, diff --git a/rs/pocket_ic_server/src/pocket_ic.rs b/rs/pocket_ic_server/src/pocket_ic.rs index 88d35cef9a3..b2ff44996eb 100644 --- a/rs/pocket_ic_server/src/pocket_ic.rs +++ b/rs/pocket_ic_server/src/pocket_ic.rs @@ -51,6 +51,7 @@ use ic_state_machine_tests::{ StateMachineConfig, StateMachineStateDir, SubmitIngressError, Time, }; use ic_test_utilities_registry::add_subnet_list_record; +use ic_types::NumBytes; use ic_types::{ artifact::UnvalidatedArtifactMutation, canister_http::{CanisterHttpReject, CanisterHttpRequestId, CanisterHttpResponseContent}, @@ -356,9 +357,10 @@ impl PocketIc { hypervisor_config.max_query_call_graph_instructions = instruction_limit; } // bound PocketIc resource consumption - hypervisor_config.embedders_config.min_sandbox_count = 0; hypervisor_config.embedders_config.max_sandbox_count = 64; hypervisor_config.embedders_config.max_sandbox_idle_time = Duration::from_secs(30); + hypervisor_config.embedders_config.max_sandboxes_rss = + NumBytes::new(2 * 1024 * 1024 * 1024); // shorter query stats epoch length for faster query stats aggregation hypervisor_config.query_stats_epoch_length = 60; // enable canister debug prints From 361d09aeb56660503f9c8e3736845f925b540790 Mon Sep 17 00:00:00 2001 From: David Frank Date: Thu, 24 Oct 2024 14:47:22 +0200 Subject: [PATCH 05/22] fix: Improve prestorecon performance (#2218) Calling selabel_open from each thread lead to bad performance, we can share a single selabel handle. Also log the elapsed time. --- cpp/prestorecon-cpp/prestorecon.cc | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/cpp/prestorecon-cpp/prestorecon.cc b/cpp/prestorecon-cpp/prestorecon.cc index b3ca2c8a3e3..74bf9deb2e3 100644 --- a/cpp/prestorecon-cpp/prestorecon.cc +++ b/cpp/prestorecon-cpp/prestorecon.cc @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -473,6 +474,7 @@ parse_args(int argc, char** argv) int main(int argc, char** argv) { + auto start_time = std::chrono::steady_clock::now(); auto args = parse_args(argc, argv); GlobalWorkPool global_pool; @@ -480,17 +482,17 @@ int main(int argc, char** argv) std::vector threads; std::vector stats(args.jobs); + auto selabel_hdl = selabel_open(SELABEL_CTX_FILE, nullptr, 0); + for (std::size_t n = 0; n < args.jobs; ++n) { threads.emplace_back( - [&global_pool, &args, &stats, n] () { - auto hdl = selabel_open(SELABEL_CTX_FILE, nullptr ,0); + [&global_pool, &args, &stats, n, selabel_hdl] () { parallel_work( global_pool, list_dir, - [hdl, &args, &stats, n] (const std::vector>& paths) { - apply_labels(paths, hdl, args.verbosity, args.dry_run, stats[n]); + [selabel_hdl, &args, &stats, n] (const std::vector>& paths) { + apply_labels(paths, selabel_hdl, args.verbosity, args.dry_run, stats[n]); }); - selabel_close(hdl); } ); } @@ -503,7 +505,14 @@ int main(int argc, char** argv) global_stats.inodes_relabeled += stats[n].inodes_relabeled; } - std::cout << "Processed: " << global_stats.inodes_processed << " Relabeled: " << global_stats.inodes_relabeled << "\n"; + selabel_close(selabel_hdl); + + auto end_time = std::chrono::steady_clock::now(); + long long elapsed_ms = std::chrono::duration_cast(end_time - start_time).count(); + + std::cout << "Processed: " << global_stats.inodes_processed + << " Relabeled: " << global_stats.inodes_relabeled + << " Elapsed: " << elapsed_ms << "ms\n"; return 0; } From f44d18f83f6fc15a7427f738d053738faaa4b7ca Mon Sep 17 00:00:00 2001 From: NikolasHai <113891786+NikolasHai@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:01:38 +0200 Subject: [PATCH 06/22] chore(ICP-Rosetta): add functionality to increase staking amount (#2199) This PR proposes the following changes: 1. Add a function for the ICP Rosetta Client to increase the stake of an existing neuron 2. Remove the legacy system tests for staking through rosetta --- rs/rosetta-api/icp/client/src/lib.rs | 70 ++ .../src/blocks.rs | 6 +- .../icp/tests/system_tests/common/utils.rs | 20 + .../test_cases/neuron_management.rs | 113 +++ rs/tests/Cargo.toml | 4 - .../rosetta/BUILD.bazel | 21 - .../rosetta/rosetta_neuron_staking_test.rs | 21 - rs/tests/src/rosetta_tests/tests.rs | 1 - .../src/rosetta_tests/tests/neuron_staking.rs | 787 ------------------ 9 files changed, 208 insertions(+), 835 deletions(-) delete mode 100644 rs/tests/financial_integrations/rosetta/rosetta_neuron_staking_test.rs delete mode 100644 rs/tests/src/rosetta_tests/tests/neuron_staking.rs diff --git a/rs/rosetta-api/icp/client/src/lib.rs b/rs/rosetta-api/icp/client/src/lib.rs index ed24817f8f8..e5e0fe455f9 100644 --- a/rs/rosetta-api/icp/client/src/lib.rs +++ b/rs/rosetta-api/icp/client/src/lib.rs @@ -17,6 +17,7 @@ use ic_rosetta_api::request_types::SetDissolveTimestampMetadata; use icp_ledger::AccountIdentifier; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc1::account::Subaccount; +use icrc_ledger_types::icrc1::account::DEFAULT_SUBACCOUNT; use num_bigint::BigInt; use reqwest::{Client, Url}; use rosetta_core::identifiers::NetworkIdentifier; @@ -761,6 +762,28 @@ impl RosettaClient { .await } + /// You can increase the amount of ICP that is staked in a neuron. + pub async fn increase_neuron_stake( + &self, + network_identifier: NetworkIdentifier, + signer_keypair: &T, + args: RosettaIncreaseNeuronStakeArgs, + ) -> anyhow::Result + where + T: RosettaSupportedKeyPair, + { + // Create Neuron and Increase Stake are functionally identical + self.create_neuron( + network_identifier, + signer_keypair, + RosettaCreateNeuronArgs::builder(args.additional_stake) + .with_neuron_index(args.neuron_index.unwrap_or(0)) + .with_from_subaccount(args.from_subaccount.unwrap_or(*DEFAULT_SUBACCOUNT)) + .build(), + ) + .await + } + /// The amount of rewards you can expect to receive are amongst other factors dependent on the amount of time a neuron is locked up for. /// If the dissolve timestamp is set to a value that is before 6 months in the future you will not be getting any rewards for the locked period. /// This is because the last 6 months of a dissolving neuron, the neuron will not get any rewards. @@ -1048,6 +1071,18 @@ impl RosettaSetNeuronDissolveDelayArgsBuilder { } } +pub struct RosettaIncreaseNeuronStakeArgs { + pub neuron_index: Option, + pub additional_stake: Nat, + pub from_subaccount: Option, +} + +impl RosettaIncreaseNeuronStakeArgs { + pub fn builder(additional_stake: Nat) -> RosettaIncreaseNeuronStakeArgsBuilder { + RosettaIncreaseNeuronStakeArgsBuilder::new(additional_stake) + } +} + pub struct RosettaChangeAutoStakeMaturityArgs { pub neuron_index: Option, pub requested_setting_for_auto_stake_maturity: bool, @@ -1087,3 +1122,38 @@ impl RosettaChangeAutoStakeMaturityArgsBuilder { } } } + +pub struct RosettaIncreaseNeuronStakeArgsBuilder { + additional_stake: Nat, + neuron_index: Option, + // The subaccount from which the ICP should be transferred + from_subaccount: Option<[u8; 32]>, +} + +impl RosettaIncreaseNeuronStakeArgsBuilder { + pub fn new(additional_stake: Nat) -> Self { + Self { + additional_stake, + neuron_index: None, + from_subaccount: None, + } + } + + pub fn with_neuron_index(mut self, neuron_index: u64) -> Self { + self.neuron_index = Some(neuron_index); + self + } + + pub fn with_from_subaccount(mut self, from_subaccount: Subaccount) -> Self { + self.from_subaccount = Some(from_subaccount); + self + } + + pub fn build(self) -> RosettaIncreaseNeuronStakeArgs { + RosettaIncreaseNeuronStakeArgs { + additional_stake: self.additional_stake, + neuron_index: self.neuron_index, + from_subaccount: self.from_subaccount, + } + } +} diff --git a/rs/rosetta-api/icp/ledger_canister_blocks_synchronizer/src/blocks.rs b/rs/rosetta-api/icp/ledger_canister_blocks_synchronizer/src/blocks.rs index 6f26c44f5d9..b0266ca0e51 100644 --- a/rs/rosetta-api/icp/ledger_canister_blocks_synchronizer/src/blocks.rs +++ b/rs/rosetta-api/icp/ledger_canister_blocks_synchronizer/src/blocks.rs @@ -477,7 +477,11 @@ mod database_access { .map_err(|e| { BlockStoreError::Other( e.to_string() - + format!(" | Block IDX: {} , Account {}", hb.index, account).as_str(), + + format!( + " | Block IDX: {} , Account {}, Tokens {}", + hb.index, account, tokens + ) + .as_str(), ) })?; } diff --git a/rs/rosetta-api/icp/tests/system_tests/common/utils.rs b/rs/rosetta-api/icp/tests/system_tests/common/utils.rs index 9b1774ad69f..ecaf0867c0e 100644 --- a/rs/rosetta-api/icp/tests/system_tests/common/utils.rs +++ b/rs/rosetta-api/icp/tests/system_tests/common/utils.rs @@ -48,6 +48,26 @@ pub async fn get_custom_agent(basic_identity: Arc, port: u16) -> A agent } +pub async fn wait_for_rosetta_to_catch_up_with_icp_ledger( + rosetta_client: &RosettaClient, + network_identifier: NetworkIdentifier, + agent: &Agent, +) { + let chain_length = query_encoded_blocks(agent, u64::MAX, 1).await.chain_length; + let last_block = wait_for_rosetta_to_sync_up_to_block( + rosetta_client, + network_identifier, + chain_length.saturating_sub(1), + ) + .await + .unwrap(); + assert_eq!( + chain_length.saturating_sub(1), + last_block, + "Failed to sync with the ledger" + ); +} + pub async fn wait_for_rosetta_to_sync_up_to_block( rosetta_client: &RosettaClient, network_identifier: NetworkIdentifier, diff --git a/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs b/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs index 07aea329301..248b1e8b8b6 100644 --- a/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs +++ b/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs @@ -1,15 +1,19 @@ +use crate::common::utils::wait_for_rosetta_to_catch_up_with_icp_ledger; use crate::common::{ system_test_environment::RosettaTestingEnvironment, utils::{get_test_agent, list_neurons, test_identity}, }; use ic_agent::{identity::BasicIdentity, Identity}; use ic_icp_rosetta_client::RosettaChangeAutoStakeMaturityArgs; +use ic_icp_rosetta_client::RosettaIncreaseNeuronStakeArgs; use ic_icp_rosetta_client::{RosettaCreateNeuronArgs, RosettaSetNeuronDissolveDelayArgs}; use ic_nns_governance::pb::v1::neuron::DissolveState; use ic_rosetta_api::request::transaction_operation_results::TransactionOperationResults; use ic_types::PrincipalId; use icp_ledger::AccountIdentifier; +use icp_ledger::DEFAULT_TRANSFER_FEE; use lazy_static::lazy_static; +use rosetta_core::request_types::AccountBalanceRequest; use std::{ sync::Arc, time::{SystemTime, UNIX_EPOCH}, @@ -67,6 +71,115 @@ fn test_create_neuron() { }); } +#[test] +fn test_increase_neuron_stake() { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let initial_balance = 100_000_000_000; + let env = RosettaTestingEnvironment::builder() + .with_initial_balances( + vec![( + AccountIdentifier::from(TEST_IDENTITY.sender().unwrap()), + // A hundred million ICP should be enough + icp_ledger::Tokens::from_e8s(initial_balance), + )] + .into_iter() + .collect(), + ) + .with_governance_canister() + .build() + .await; + + // Stake the minimum amount 100 million e8s + let staked_amount = initial_balance / 10; + let neuron_index = 0; + let from_subaccount = [0; 32]; + + env.rosetta_client + .create_neuron( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + RosettaCreateNeuronArgs::builder(staked_amount.into()) + .with_from_subaccount(from_subaccount) + .with_neuron_index(neuron_index) + .build(), + ) + .await + .unwrap(); + + // Try to stake more than the amount of ICP in the account + match env + .rosetta_client + .increase_neuron_stake( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + RosettaIncreaseNeuronStakeArgs::builder(u64::MAX.into()) + .with_from_subaccount(from_subaccount) + .with_neuron_index(neuron_index) + .build(), + ) + .await + { + Err(e) + if e.to_string().contains( + "the debit account doesn't have enough funds to complete the transaction", + ) => {} + Err(e) => panic!("Unexpected error: {}", e), + Ok(ok) => panic!("Expected an errorm but got: {:?}", ok), + } + + // Now we try with a valid amount + let additional_stake = initial_balance / 10; + env.rosetta_client + .increase_neuron_stake( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + RosettaIncreaseNeuronStakeArgs::builder(additional_stake.into()) + .with_from_subaccount(from_subaccount) + .with_neuron_index(neuron_index) + .build(), + ) + .await + .unwrap(); + + let agent = get_test_agent(env.pocket_ic.url().unwrap().port().unwrap()).await; + let neuron = list_neurons(&agent).await.full_neurons[0].to_owned(); + assert_eq!( + neuron.cached_neuron_stake_e8s, + staked_amount + additional_stake + ); + + wait_for_rosetta_to_catch_up_with_icp_ledger( + &env.rosetta_client, + env.network_identifier.clone(), + &agent, + ) + .await; + + let balance = env + .rosetta_client + .account_balance( + AccountBalanceRequest::builder( + env.network_identifier.clone(), + AccountIdentifier::from(TEST_IDENTITY.sender().unwrap()).into(), + ) + .build(), + ) + .await + .unwrap() + .balances + .first() + .unwrap() + .value + .parse::() + .unwrap(); + assert_eq!( + balance, + initial_balance - staked_amount - additional_stake - DEFAULT_TRANSFER_FEE.get_e8s() * 2 + ); + }); +} + #[test] fn test_set_neuron_dissolve_delay_timestamp() { let rt = Runtime::new().unwrap(); diff --git a/rs/tests/Cargo.toml b/rs/tests/Cargo.toml index ffc98bc08bf..49eb5b53ddb 100644 --- a/rs/tests/Cargo.toml +++ b/rs/tests/Cargo.toml @@ -223,10 +223,6 @@ path = "financial_integrations/rosetta/rosetta_neuron_maturity_test.rs" name = "ic-systest-rosetta-neuron-spawn-test" path = "financial_integrations/rosetta/rosetta_neuron_spawn_test.rs" -[[bin]] -name = "ic-systest-rosetta-neuron-staking-test" -path = "financial_integrations/rosetta/rosetta_neuron_staking_test.rs" - [[bin]] name = "test-driver-e2e-scenarios" path = "testing_verification/test_driver_e2e_scenarios.rs" diff --git a/rs/tests/financial_integrations/rosetta/BUILD.bazel b/rs/tests/financial_integrations/rosetta/BUILD.bazel index d178e18e50c..e8792bcc3cb 100644 --- a/rs/tests/financial_integrations/rosetta/BUILD.bazel +++ b/rs/tests/financial_integrations/rosetta/BUILD.bazel @@ -224,27 +224,6 @@ system_test_nns( deps = DEPENDENCIES + ["//rs/tests"], ) -system_test_nns( - name = "rosetta_neuron_staking_test", - extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. - flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, - tags = [ - "k8s", - "long_test", # since it takes longer than 5 minutes. - ], - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = - GUESTOS_RUNTIME_DEPS + - UNIVERSAL_VM_RUNTIME_DEPS + [ - "//rs/rosetta-api/icp:ic-rosetta-api", - "//rs/rosetta-api/icp:rosetta_image.tar", - "//rs/tests:rosetta_workspace", - "@rosetta-cli//:rosetta-cli", - ], - deps = DEPENDENCIES + ["//rs/tests"], -) - system_test_nns( name = "rosetta_neuron_voting_test", extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. diff --git a/rs/tests/financial_integrations/rosetta/rosetta_neuron_staking_test.rs b/rs/tests/financial_integrations/rosetta/rosetta_neuron_staking_test.rs deleted file mode 100644 index 06ddbf06cb0..00000000000 --- a/rs/tests/financial_integrations/rosetta/rosetta_neuron_staking_test.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[rustfmt::skip] -use anyhow::Result; - -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::driver::test_env::TestEnv; -use ic_system_test_driver::systest; -use ic_tests::rosetta_tests; -use rosetta_tests::setup::{ROSETTA_TESTS_OVERALL_TIMEOUT, ROSETTA_TESTS_PER_TEST_TIMEOUT}; -use rosetta_tests::tests; - -fn main() -> Result<()> { - SystemTestGroup::new() - .with_setup(group_setup) - .with_overall_timeout(ROSETTA_TESTS_OVERALL_TIMEOUT) - .with_timeout_per_test(ROSETTA_TESTS_PER_TEST_TIMEOUT) - .add_test(systest!(tests::neuron_staking::test)) - .execute_from_args()?; - Ok(()) -} - -fn group_setup(_env: TestEnv) {} diff --git a/rs/tests/src/rosetta_tests/tests.rs b/rs/tests/src/rosetta_tests/tests.rs index e4c28f314b3..4e6485e0135 100644 --- a/rs/tests/src/rosetta_tests/tests.rs +++ b/rs/tests/src/rosetta_tests/tests.rs @@ -10,5 +10,4 @@ pub mod neuron_hotkey; pub mod neuron_info; pub mod neuron_maturity; pub mod neuron_spawn; -pub mod neuron_staking; pub mod neuron_voting; diff --git a/rs/tests/src/rosetta_tests/tests/neuron_staking.rs b/rs/tests/src/rosetta_tests/tests/neuron_staking.rs deleted file mode 100644 index 75852c685fd..00000000000 --- a/rs/tests/src/rosetta_tests/tests/neuron_staking.rs +++ /dev/null @@ -1,787 +0,0 @@ -use crate::rosetta_tests::{ - ledger_client::LedgerClient, - lib::{ - assert_canister_error, check_balance, create_ledger_client, do_multiple_txn, - make_user_ed25519, one_day_from_now_nanos, raw_construction, sign, to_public_key, - }, - rosetta_client::RosettaApiClient, - setup::setup, - test_neurons::TestNeurons, -}; -use assert_json_diff::{assert_json_eq, assert_json_include}; -use ic_ledger_core::{ - tokens::{CheckedAdd, CheckedSub}, - Tokens, -}; -use ic_nns_constants::GOVERNANCE_CANISTER_ID; -use ic_rosetta_api::{ - convert::{from_model_account_identifier, neuron_account_from_public_key}, - models::{seconds::Seconds, NeuronInfoResponse}, - request::Request, - request_types::{SetDissolveTimestamp, Stake, StartDissolve, StopDissolve}, -}; -use ic_rosetta_test_utils::{EdKeypair, RequestInfo}; -use ic_system_test_driver::{driver::test_env::TestEnv, util::block_on}; -use icp_ledger::{AccountIdentifier, Operation, DEFAULT_TRANSFER_FEE}; -use serde_json::{json, Value}; -use slog::info; -use std::{collections::HashMap, sync::Arc, time::UNIX_EPOCH}; - -const PORT: u32 = 8103; -const VM_NAME: &str = "neuron-staking"; - -pub fn test(env: TestEnv) { - let logger = env.logger(); - - let mut ledger_balances = HashMap::new(); - let (acc, _, _, _) = make_user_ed25519(101); - ledger_balances.insert(acc, Tokens::new(1000, 0).unwrap()); - - // Create neurons. - let one_year_from_now = 60 * 60 * 24 * 365 - + std::time::SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let neurons = TestNeurons::new(2000, &mut ledger_balances); - - // Create Rosetta and ledger clients. - let neurons = neurons.get_neurons(); - let client = setup(&env, PORT, VM_NAME, Some(ledger_balances), Some(neurons)); - let ledger_client = create_ledger_client(&env, &client); - - block_on(async { - info!(logger, "Test staking"); - let _ = test_staking(&client).await; - info!(logger, "Test staking (raw JSON)"); - let _ = test_staking_raw(&client).await; - info!(logger, "Test staking failure"); - test_staking_failure(&client).await; - info!(logger, "Test staking flow"); - test_staking_flow(&client, &ledger_client, Seconds(one_year_from_now)).await; - info!(logger, "Test staking flow two transactions"); - test_staking_flow_two_txns(&client, &ledger_client, Seconds(one_year_from_now)).await; - }); -} - -async fn test_staking(client: &RosettaApiClient) -> (AccountIdentifier, Arc) { - let (acc, kp_b, _pk_b, _pid_b) = make_user_ed25519(101); - let key_pair = Arc::new(kp_b); - - let (dst_acc, dst_acc_kp, dst_acc_pk, _pid) = make_user_ed25519(1300); - let dst_acc_kp = Arc::new(dst_acc_kp); - let neuron_index = 2; - - let staked_amount = Tokens::new(10, 0).unwrap(); - - // Could use /construction/derive for this. - let neuron_account = - neuron_account_from_public_key(&GOVERNANCE_CANISTER_ID, &dst_acc_pk, neuron_index).unwrap(); - let neuron_account = from_model_account_identifier(&neuron_account).unwrap(); - - let (_tid, results, _fee) = do_multiple_txn( - client, - &[ - RequestInfo { - request: Request::Transfer(Operation::Transfer { - from: acc, - to: dst_acc, - spender: None, - amount: staked_amount.checked_add(&DEFAULT_TRANSFER_FEE).unwrap(), - fee: DEFAULT_TRANSFER_FEE, - }), - sender_keypair: Arc::clone(&key_pair), - }, - RequestInfo { - request: Request::Transfer(Operation::Transfer { - from: dst_acc, - to: neuron_account, - spender: None, - amount: staked_amount, - fee: DEFAULT_TRANSFER_FEE, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - RequestInfo { - request: Request::Stake(Stake { - account: dst_acc, - neuron_index, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - ], - false, - Some(one_day_from_now_nanos()), - None, - ) - .await - .unwrap(); - - let neuron_id = results.operations.last().unwrap().neuron_id; - assert!( - neuron_id.is_some(), - "NeuronId should have been returned here" - ); - - // Block height is the last block observed. - // In this case the transfer to neuron_account. - assert!(results.last_block_index().is_some()); - - let _neuron_info: NeuronInfoResponse = client - .account_balance_neuron(neuron_account, neuron_id, None, false) - .await - .unwrap() - .unwrap() - .metadata - .try_into() - .unwrap(); - // TODO(NNS1-3390) after https://github.com/dfinity/ic/pull/1982 is deployed, uncomment this line - // assert_eq!(neuron_info.state, NeuronState::NotDissolving); - - let _neuron_info: NeuronInfoResponse = client - .account_balance_neuron( - neuron_account, - None, - Some((dst_acc_pk.clone(), neuron_index)), - false, - ) - .await - .unwrap() - .unwrap() - .metadata - .try_into() - .unwrap(); - // TODO(NNS1-3390) after https://github.com/dfinity/ic/pull/1982 is deployed, uncomment this line - // assert_eq!(neuron_info.state, NeuronState::NotDissolving); - - let _neuron_info: NeuronInfoResponse = client - .account_balance_neuron(neuron_account, None, Some((dst_acc_pk, neuron_index)), true) - .await - .unwrap() - .unwrap() - .metadata - .try_into() - .unwrap(); - // TODO(NNS1-3390) after https://github.com/dfinity/ic/pull/1982 is deployed, uncomment this line - // assert_eq!(neuron_info.state, NeuronState::NotDissolving); - - // Return staked account. - (dst_acc, dst_acc_kp) -} - -async fn test_staking_failure(client: &RosettaApiClient) { - let (acc, kp_b, _pk_b, _pid_b) = make_user_ed25519(101); - let key_pair = Arc::new(kp_b); - - let (dst_acc, dst_acc_kp, dst_acc_pk, _pid) = make_user_ed25519(1301); - let dst_acc_kp = Arc::new(dst_acc_kp); - let neuron_index = 2; - - // This is just below the minimum (NetworkEconomics.neuron_minimum_stake_e8s). - let staked_amount = Tokens::new(1, 0) - .unwrap() - .checked_sub(&Tokens::from_e8s(1)) - .unwrap(); - - // Could use /construction/derive for this. - let neuron_account = - neuron_account_from_public_key(&GOVERNANCE_CANISTER_ID, &dst_acc_pk, neuron_index).unwrap(); - let neuron_account = from_model_account_identifier(&neuron_account).unwrap(); - - let err = do_multiple_txn( - client, - &[ - RequestInfo { - request: Request::Transfer(Operation::Transfer { - from: acc, - to: dst_acc, - spender: None, - amount: staked_amount.checked_add(&DEFAULT_TRANSFER_FEE).unwrap(), - fee: DEFAULT_TRANSFER_FEE, - }), - sender_keypair: Arc::clone(&key_pair), - }, - RequestInfo { - request: Request::Transfer(Operation::Transfer { - from: dst_acc, - to: neuron_account, - spender: None, - amount: staked_amount, - fee: DEFAULT_TRANSFER_FEE, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - RequestInfo { - request: Request::Stake(Stake { - account: dst_acc, - neuron_index, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - ], - false, - Some(one_day_from_now_nanos()), - None, - ) - .await - .unwrap_err(); - - assert_canister_error( - &err, - 750, - "Could not claim neuron: InsufficientFunds: Account does not have enough funds to stake a neuron", - ); -} - -async fn test_staking_raw(client: &RosettaApiClient) -> (AccountIdentifier, Arc) { - let (acc, kp_b, _pk_b, _pid_b) = make_user_ed25519(101); - let key_pair = Arc::new(kp_b); - - let (dst_acc, dst_acc_kp, dst_acc_pk, _pid) = make_user_ed25519(1300); - let dst_acc_kp = Arc::new(dst_acc_kp); - let neuron_index = 2; - - // Could use /construction/derive for this. - let neuron_account = - neuron_account_from_public_key(&GOVERNANCE_CANISTER_ID, &dst_acc_pk, neuron_index).unwrap(); - let neuron_account = from_model_account_identifier(&neuron_account).unwrap(); - - // Key pairs as Json. - let pk1 = serde_json::to_value(to_public_key(&key_pair)).unwrap(); - let pk2 = serde_json::to_value(to_public_key(&dst_acc_kp)).unwrap(); - - // Call /construction/derive. - let req_derive = json!({ - "network_identifier": &client.network_id(), - "public_key": pk1, - "metadata": { - "account_type": "ledger" - } - }); - let res_derive = raw_construction(client, "derive", req_derive).await; - let address = res_derive - .get("account_identifier") - .unwrap() - .get("address") - .unwrap(); - assert_eq!(&acc.to_hex(), address); // 52bef... - - // acc => 52bef... - // dest_acc => 1e31da... - // neuron_account => 79ec2... - - // Call /construction/preprocess - let operations = json!([ - { - "operation_identifier": { - "index": 0 - }, - "type": "TRANSACTION", - "account": { - "address": &acc - }, - "amount": { - "value": "-1000010000", - "currency": { - "symbol": "ICP", - "decimals": 8 - } - }, - }, - { - "operation_identifier": { - "index": 1 - }, - "type": "TRANSACTION", - "account": { - "address": &dst_acc - }, - "amount": { - "value": "1000010000", - "currency": { - "symbol": "ICP", - "decimals": 8 - } - }, - }, - { - "operation_identifier": { - "index": 2 - }, - "type": "FEE", - "account": { - "address": &acc - }, - "amount": { - "value": "-10000", - "currency": { - "symbol": "ICP", - "decimals": 8 - } - }, - }, - { - "operation_identifier": { - "index": 3 - }, - "type": "TRANSACTION", - "account": { - "address": &dst_acc - }, - "amount": { - "value": "-1000000000", - "currency": { - "symbol": "ICP", - "decimals": 8 - } - }, - }, - { - "operation_identifier": { - "index": 4 - }, - "type": "TRANSACTION", - "account": { - "address": &neuron_account - }, - "amount": { - "value": "1000000000", - "currency": { - "symbol": "ICP", - "decimals": 8 - } - }, - }, - { - "operation_identifier": { - "index": 5 - }, - "type": "FEE", - "account": { - "address": &dst_acc - }, - "amount": { - "value": "-10000", - "currency": { - "symbol": "ICP", - "decimals": 8 - } - }, - }, - { - "operation_identifier": { - "index": 6 - }, - "type": "STAKE", - "account": { - "address": &dst_acc - }, - "metadata": { - "neuron_index": &neuron_index - } - } - ]); - let req_preprocess = json!({ - "network_identifier": &client.network_id(), - "operations": operations, - "metadata": {}, - }); - let res_preprocess = raw_construction(client, "preprocess", req_preprocess).await; - let options = res_preprocess.get("options"); - assert_json_eq!( - json!({ - "request_types": [ - "TRANSACTION", - "TRANSACTION", - {"STAKE": {"neuron_index": 2}} - ] - }), - options.unwrap() - ); - - // Call /construction/metadata - let req_metadata = json!({ - "network_identifier": &client.network_id(), - "options": options, - "public_keys": [pk1] - }); - let res_metadata = raw_construction(client, "metadata", req_metadata).await; - assert_json_eq!( - json!([ - { - "currency": {"symbol": "ICP", "decimals": 8}, - "value": format!("{}", DEFAULT_TRANSFER_FEE.get_e8s()) - } - ]), - res_metadata.get("suggested_fee").unwrap() - ); - // NB: metadata response will have to be added to payloads request. - - // Call /construction/payloads - let req_payloads = json!({ - "network_identifier": &client.network_id(), - "operations": operations, - "metadata": res_metadata, - "public_keys": [pk1,pk2] - }); - let res_payloads = raw_construction(client, "payloads", req_payloads).await; - let unsigned_transaction: &Value = res_payloads.get("unsigned_transaction").unwrap(); - let payloads = res_payloads.get("payloads").unwrap(); - let payloads = payloads.as_array().unwrap(); - assert_eq!(6, payloads.len(), "Expecting 6 payloads (3x2)."); - - // Call /construction/parse (unsigned). - let req_parse = json!({ - "network_identifier": &client.network_id(), - "signed": false, - "transaction": &unsigned_transaction - }); - let _res_parse = raw_construction(client, "parse", req_parse).await; - - // Call /construction/combine. - let signatures = json!([ - { - "signing_payload": payloads[0], - "public_key": pk1, - "signature_type": "ed25519", - "hex_bytes": sign(&payloads[0], &key_pair) - },{ - "signing_payload": payloads[1], - "public_key": pk1, - "signature_type": "ed25519", - "hex_bytes": sign(&payloads[1], &key_pair) - },{ - "signing_payload": payloads[2], - "public_key": pk2, - "signature_type": "ed25519", - "hex_bytes": sign(&payloads[2], &dst_acc_kp) - },{ - "signing_payload": payloads[3], - "public_key": pk2, - "signature_type": "ed25519", - "hex_bytes": sign(&payloads[3], &dst_acc_kp) - },{ - "signing_payload": payloads[4], - "public_key": pk2, - "signature_type": "ed25519", - "hex_bytes": sign(&payloads[4], &dst_acc_kp) - },{ - "signing_payload": payloads[5], - "public_key": pk2, - "signature_type": "ed25519", - "hex_bytes": sign(&payloads[5], &dst_acc_kp) - }, - ]); - - let req_combine = json!({ - "network_identifier": &client.network_id(), - "unsigned_transaction": &unsigned_transaction, - "signatures": signatures - }); - let res_combine = raw_construction(client, "combine", req_combine).await; - - // Call /construction/parse (signed). - let signed_transaction: &Value = res_combine.get("signed_transaction").unwrap(); - let req_parse = json!({ - "network_identifier": &client.network_id(), - "signed": true, - "transaction": &signed_transaction - }); - let _res_parse = raw_construction(client, "parse", req_parse).await; - - // Call /construction/hash. - let req_hash = json!({ - "network_identifier": &client.network_id(), - "signed_transaction": &signed_transaction - }); - let _res_hash = raw_construction(client, "hash", req_hash).await; - - // Call /construction/submit. - let req_submit = json!({ - "network_identifier": &client.network_id(), - "signed_transaction": &signed_transaction - }); - let res_submit = raw_construction(client, "submit", req_submit).await; - - // Check proper state after staking. - let operations = res_submit - .get("metadata") - .unwrap() - .get("operations") - .unwrap() - .as_array() - .unwrap(); - assert_eq!( - 7, - operations.len(), - "Expecting 7 operations for the staking transactions." - ); - for op in operations.iter() { - assert_eq!( - op.get("status").unwrap(), - "COMPLETED", - "Operation didn't complete." - ); - } - assert_json_include!( - actual: &operations[0], - expected: json!({ - "amount": {"currency": {"decimals": 8, "symbol": "ICP"}, "value": "-1000010000"}, - "operation_identifier": {"index": 0}, - "status": "COMPLETED", - "type": "TRANSACTION" - }) - ); - - let last_neuron_id = operations - .last() - .unwrap() - .get("metadata") - .expect("Expecting metadata in response") - .get("neuron_id"); - assert!( - last_neuron_id.is_some(), - "NeuronId should have been returned here" - ); - let neuron_id = last_neuron_id.unwrap().as_u64(); - - // Block height is the last block observed. - // In this case the transfer to neuron_account. - let last_block_idx = operations - .iter() - .rev() - .find_map(|r| r.get("metadata").and_then(|r| r.get("block_index"))); - assert!(last_block_idx.is_some()); - - let _neuron_info: NeuronInfoResponse = client - .account_balance_neuron(neuron_account, neuron_id, None, false) - .await - .unwrap() - .unwrap() - .metadata - .try_into() - .unwrap(); - // TODO(NNS1-3390) after https://github.com/dfinity/ic/pull/1982 is deployed, uncomment this line - // assert_eq!(neuron_info.state, NeuronState::NotDissolving); - - // Return staked account. - (dst_acc, dst_acc_kp) -} - -async fn test_staking_flow( - client: &RosettaApiClient, - ledger_client: &LedgerClient, - timestamp: Seconds, -) { - let (test_account, kp_b, _pk_b, _pid_b) = make_user_ed25519(101); - let test_key_pair = Arc::new(kp_b); - - let (_, tip_idx) = ledger_client.get_tip().await; - let balance_before = ledger_client.get_account_balance(test_account).await; - let (dst_acc, dst_acc_kp, dst_acc_pk, _pid) = make_user_ed25519(1400); - let dst_acc_kp = Arc::new(dst_acc_kp); - - let staked_amount = Tokens::new(1, 0).unwrap(); - - let neuron_index = 1; - // Could use /neuron/derive for this. - let neuron_account = - neuron_account_from_public_key(&GOVERNANCE_CANISTER_ID, &dst_acc_pk, neuron_index).unwrap(); - let neuron_account = from_model_account_identifier(&neuron_account).unwrap(); - - let (_tid, res, _fee) = do_multiple_txn( - client, - &[ - RequestInfo { - request: Request::Transfer(Operation::Transfer { - from: test_account, - to: dst_acc, - spender: None, - amount: staked_amount.checked_add(&DEFAULT_TRANSFER_FEE).unwrap(), - fee: DEFAULT_TRANSFER_FEE, - }), - sender_keypair: Arc::clone(&test_key_pair), - }, - RequestInfo { - request: Request::Transfer(Operation::Transfer { - from: dst_acc, - to: neuron_account, - spender: None, - amount: staked_amount, - fee: DEFAULT_TRANSFER_FEE, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - RequestInfo { - request: Request::Stake(Stake { - account: dst_acc, - neuron_index, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - RequestInfo { - request: Request::SetDissolveTimestamp(SetDissolveTimestamp { - account: dst_acc, - neuron_index, - timestamp, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - RequestInfo { - request: Request::StartDissolve(StartDissolve { - account: dst_acc, - neuron_index, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - RequestInfo { - request: Request::StopDissolve(StopDissolve { - account: dst_acc, - neuron_index, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - ], - false, - None, - None, - ) - .await - .unwrap(); - - let expected_idx = tip_idx + 2; - - if let Some(h) = res.last_block_index() { - assert_eq!(h, expected_idx); - } - let _ = client.wait_for_block_at(expected_idx).await.unwrap(); - - check_balance( - client, - ledger_client, - &test_account, - balance_before - .checked_sub(&staked_amount) - .unwrap() - .checked_sub(&DEFAULT_TRANSFER_FEE) - .unwrap() - .checked_sub(&DEFAULT_TRANSFER_FEE) - .unwrap(), - ) - .await; -} - -async fn test_staking_flow_two_txns( - client: &RosettaApiClient, - ledger_client: &LedgerClient, - timestamp: Seconds, -) { - let (test_account, kp_b, _pk_b, _pid_b) = make_user_ed25519(101); - let test_key_pair = Arc::new(kp_b); - - let (_, tip_idx) = ledger_client.get_tip().await; - let balance_before = ledger_client.get_account_balance(test_account).await; - - let (dst_acc, dst_acc_kp, dst_acc_pk, _pid) = make_user_ed25519(1401); - let dst_acc_kp = Arc::new(dst_acc_kp); - - let staked_amount = Tokens::new(1, 0).unwrap(); - let neuron_index = 1; - - // Could use /neuron/derive for this. - let neuron_account = - neuron_account_from_public_key(&GOVERNANCE_CANISTER_ID, &dst_acc_pk, neuron_index).unwrap(); - let neuron_account = from_model_account_identifier(&neuron_account).unwrap(); - - let (_tid, _bh, _fee) = do_multiple_txn( - client, - &[ - RequestInfo { - request: Request::Transfer(Operation::Transfer { - from: test_account, - to: dst_acc, - spender: None, - amount: staked_amount.checked_add(&DEFAULT_TRANSFER_FEE).unwrap(), - fee: DEFAULT_TRANSFER_FEE, - }), - sender_keypair: Arc::clone(&test_key_pair), - }, - RequestInfo { - request: Request::Transfer(Operation::Transfer { - from: dst_acc, - to: neuron_account, - spender: None, - amount: staked_amount, - fee: DEFAULT_TRANSFER_FEE, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - ], - false, - None, - None, - ) - .await - .unwrap(); - - let (_tid, res, _fee) = do_multiple_txn( - client, - &[ - RequestInfo { - request: Request::Stake(Stake { - account: dst_acc, - neuron_index, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - RequestInfo { - request: Request::SetDissolveTimestamp(SetDissolveTimestamp { - account: dst_acc, - neuron_index, - timestamp, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - RequestInfo { - request: Request::StartDissolve(StartDissolve { - account: dst_acc, - neuron_index, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - RequestInfo { - request: Request::StopDissolve(StopDissolve { - account: dst_acc, - neuron_index, - }), - sender_keypair: Arc::clone(&dst_acc_kp), - }, - ], - false, - None, - None, - ) - .await - .unwrap(); - - let expected_idx = tip_idx + 2; - - if let Some(h) = res.last_block_index() { - assert_eq!(h, expected_idx); - } - let _ = client.wait_for_block_at(expected_idx).await.unwrap(); - - check_balance( - client, - ledger_client, - &test_account, - balance_before - .checked_sub(&staked_amount) - .unwrap() - .checked_sub(&DEFAULT_TRANSFER_FEE) - .unwrap() - .checked_sub(&DEFAULT_TRANSFER_FEE) - .unwrap(), - ) - .await; -} From 7d05c7ed735401b86bfe408c19c413e33247c525 Mon Sep 17 00:00:00 2001 From: mihailjianu1 <32812419+mihailjianu1@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:21:59 +0200 Subject: [PATCH 07/22] chore: inline networking system tests (#2046) [Sheet with effort tracking](https://dfinity.atlassian.net/browse/IDX-3144) [context](https://dfinity.atlassian.net/browse/IDX-3144) Still need to be inlined: //rs/tests/networking:canister_http_correctness_test_bin //rs/tests/networking:canister_http_fault_tolerance_test_bin //rs/tests/networking:canister_http_socks_test_bin //rs/tests/networking:canister_http_test_bin //rs/tests/networking:canister_http_time_out_test_bin --------- Co-authored-by: IDX GitHub Automation --- Cargo.lock | 34 +- Cargo.toml | 1 + rs/tests/Cargo.toml | 20 - rs/tests/boundary_nodes/BUILD.bazel | 8 +- rs/tests/boundary_nodes/Cargo.toml | 5 + .../boundary_nodes/bn_update_workload_test.rs | 6 +- rs/tests/networking/BUILD.bazel | 98 ++-- rs/tests/networking/Cargo.toml | 12 +- .../firewall_max_connections_test.rs | 179 ++++++- rs/tests/networking/firewall_priority_test.rs | 484 +++++++++++++++++- rs/tests/networking/network_large_test.rs | 194 ++++++- .../networking/network_reliability_test.rs | 402 ++++++++++++++- rs/tests/networking/p2p_performance.rs | 51 -- .../p2p_performance_test.rs} | 58 ++- .../networking/query_workload_long_test.rs | 140 ++++- .../subnet_update_workload/BUILD.bazel | 30 ++ .../subnet_update_workload/Cargo.toml | 26 + .../subnet_update_workload/src/lib.rs} | 2 +- .../update_workload_large_payload.rs | 6 +- rs/tests/src/lib.rs | 1 - .../networking/firewall_max_connections.rs | 173 ------- rs/tests/src/networking/firewall_priority.rs | 476 ----------------- rs/tests/src/networking/mod.rs | 7 - rs/tests/src/networking/network_large.rs | 188 ------- .../src/networking/network_reliability.rs | 396 -------------- .../src/networking/replica_query_workload.rs | 125 ----- 26 files changed, 1597 insertions(+), 1525 deletions(-) delete mode 100644 rs/tests/networking/p2p_performance.rs rename rs/tests/{src/networking/p2p_performance_workload.rs => networking/p2p_performance_test.rs} (85%) create mode 100644 rs/tests/networking/subnet_update_workload/BUILD.bazel create mode 100644 rs/tests/networking/subnet_update_workload/Cargo.toml rename rs/tests/{src/networking/subnet_update_workload.rs => networking/subnet_update_workload/src/lib.rs} (99%) delete mode 100644 rs/tests/src/networking/firewall_max_connections.rs delete mode 100644 rs/tests/src/networking/firewall_priority.rs delete mode 100644 rs/tests/src/networking/mod.rs delete mode 100644 rs/tests/src/networking/network_large.rs delete mode 100644 rs/tests/src/networking/network_reliability.rs delete mode 100644 rs/tests/src/networking/replica_query_workload.rs diff --git a/Cargo.lock b/Cargo.lock index bfef318334f..90a6ea8321a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9958,6 +9958,29 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ic-networking-subnet-update-workload" +version = "0.9.0" +dependencies = [ + "anyhow", + "ic-agent", + "ic-interfaces-registry", + "ic-protobuf", + "ic-registry-canister-api", + "ic-registry-keys", + "ic-registry-nns-data-provider", + "ic-registry-routing-table", + "ic-registry-subnet-type", + "ic-system-test-driver", + "registry-canister", + "slog", + "slog-async", + "slog-term", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "ic-neurons-fund" version = "0.0.1" @@ -13481,6 +13504,7 @@ dependencies = [ "ic-boundary-nodes-system-test-utils", "ic-canister-client", "ic-nervous-system-common-test-keys", + "ic-networking-subnet-update-workload", "ic-nns-common", "ic-nns-constants", "ic-nns-governance-api", @@ -15534,11 +15558,16 @@ dependencies = [ "canister_http", "cloner-canister-types", "dfn_candid", + "ic-agent", "ic-base-types", "ic-cdk 0.16.0", "ic-limits", "ic-management-canister-types", + "ic-networking-subnet-update-workload", + "ic-nns-governance-api", "ic-prep", + "ic-protobuf", + "ic-registry-keys", "ic-registry-subnet-features", "ic-registry-subnet-type", "ic-system-test-driver", @@ -15547,10 +15576,13 @@ dependencies = [ "ic-types", "ic-utils 0.37.0", "proxy_canister", + "rand 0.8.5", + "rand_chacha 0.3.1", + "registry-canister", "reqwest 0.12.8", "slog", - "tests", "tokio", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bd88b55dea9..3cec060dbc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -380,6 +380,7 @@ members = [ "rs/tests/networking", "rs/tests/networking/canisters", "rs/tests/networking/canister_http", + "rs/tests/networking/subnet_update_workload", "rs/tests/nested", "rs/tests/nns", "rs/tests/nns/sns", diff --git a/rs/tests/Cargo.toml b/rs/tests/Cargo.toml index 49eb5b53ddb..cc9e688fd06 100644 --- a/rs/tests/Cargo.toml +++ b/rs/tests/Cargo.toml @@ -227,10 +227,6 @@ path = "financial_integrations/rosetta/rosetta_neuron_spawn_test.rs" name = "test-driver-e2e-scenarios" path = "testing_verification/test_driver_e2e_scenarios.rs" -[[bin]] -name = "ic-systest-bn-update-workload-test" -path = "boundary_nodes/bn_update_workload_test.rs" - [[bin]] name = "ic-systest-mainnet" path = "testing_verification/mainnet_test.rs" @@ -243,22 +239,6 @@ path = "testing_verification/replicable_mock_test.rs" name = "ic-systest-remote-replicable-mock-test" path = "testing_verification/remote_replicable_mock_test.rs" -[[bin]] -name = "ic-systest-network-reliability" -path = "networking/network_reliability_test.rs" - -[[bin]] -name = "ic-systest-network-large" -path = "networking/network_large_test.rs" - -[[bin]] -name = "ic-systest-query-workload-long-test" -path = "networking/query_workload_long_test.rs" - -[[bin]] -name = "ic-systest-update-workload-large-payload" -path = "networking/update_workload_large_payload.rs" - [[bin]] name = "ic-xc-ledger-suite-orchestrator" path = "cross_chain/ic_xc_ledger_suite_orchestrator_test.rs" diff --git a/rs/tests/boundary_nodes/BUILD.bazel b/rs/tests/boundary_nodes/BUILD.bazel index 6278d652572..5aeb48c33ce 100644 --- a/rs/tests/boundary_nodes/BUILD.bazel +++ b/rs/tests/boundary_nodes/BUILD.bazel @@ -1,4 +1,4 @@ -load("//rs/tests:common.bzl", "BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS", "COUNTER_CANISTER_RUNTIME_DEPS", "DEPENDENCIES", "GRAFANA_RUNTIME_DEPS", "GUESTOS_RUNTIME_DEPS", "MACRO_DEPENDENCIES", "UNIVERSAL_VM_RUNTIME_DEPS") +load("//rs/tests:common.bzl", "BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS", "COUNTER_CANISTER_RUNTIME_DEPS", "GRAFANA_RUNTIME_DEPS", "GUESTOS_RUNTIME_DEPS", "MACRO_DEPENDENCIES", "UNIVERSAL_VM_RUNTIME_DEPS") load("//rs/tests:system_tests.bzl", "system_test", "system_test_nns") package(default_visibility = ["//rs:system-tests-pkg"]) @@ -72,7 +72,11 @@ system_test_nns( target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS test_timeout = "long", runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS + COUNTER_CANISTER_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], + deps = [ + "//rs/tests/driver:ic-system-test-driver", + "//rs/tests/networking/subnet_update_workload", + "@crate_index//:anyhow", + ], ) system_test_nns( diff --git a/rs/tests/boundary_nodes/Cargo.toml b/rs/tests/boundary_nodes/Cargo.toml index 28699b80e3e..3bd3839f0dd 100644 --- a/rs/tests/boundary_nodes/Cargo.toml +++ b/rs/tests/boundary_nodes/Cargo.toml @@ -18,6 +18,7 @@ ic-boundary-nodes-integration-test-common = { path = "integration_test_common" } ic-boundary-nodes-performance-test-common = { path = "performance_test_common" } ic-canister-client = { path = "../../canister_client" } ic-nervous-system-common-test-keys = { path = "../../nervous_system/common/test_keys" } +ic-networking-subnet-update-workload = { path = "../networking/subnet_update_workload" } ic-nns-common = { path = "../../nns/common" } ic-nns-constants = { path = "../../nns/constants" } ic-nns-governance-api = { path = "../../nns/governance/api" } @@ -56,3 +57,7 @@ path = "bn_performance_test.rs" [[bin]] name = "ic-systest-mainnet-bn-performance" path = "mainnet_bn_performance_test.rs" + +[[bin]] +name = "ic-systest-bn-update-workload" +path = "bn_update_workload_test.rs" \ No newline at end of file diff --git a/rs/tests/boundary_nodes/bn_update_workload_test.rs b/rs/tests/boundary_nodes/bn_update_workload_test.rs index 3681017f787..41cb32187cd 100644 --- a/rs/tests/boundary_nodes/bn_update_workload_test.rs +++ b/rs/tests/boundary_nodes/bn_update_workload_test.rs @@ -3,9 +3,9 @@ use anyhow::Result; use std::time::Duration; +use ic_networking_subnet_update_workload::{setup, test}; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::systest; -use ic_tests::networking::subnet_update_workload::{config, test}; // Test parameters const APP_SUBNET_SIZE: usize = 4; @@ -20,7 +20,7 @@ const OVERALL_TIMEOUT_DELTA: Duration = Duration::from_secs(5 * 60); fn main() -> Result<()> { let per_task_timeout: Duration = WORKLOAD_RUNTIME + TASK_TIMEOUT_DELTA; // This should be a bit larger than the workload execution time. let overall_timeout: Duration = per_task_timeout + OVERALL_TIMEOUT_DELTA; // This should be a bit larger than the per_task_timeout. - let config = |env| config(env, APP_SUBNET_SIZE, USE_BOUNDARY_NODE, None); + let setup = |env| setup(env, APP_SUBNET_SIZE, USE_BOUNDARY_NODE, None); let test = |env| { test( env, @@ -31,7 +31,7 @@ fn main() -> Result<()> { ) }; SystemTestGroup::new() - .with_setup(config) + .with_setup(setup) .add_test(systest!(test)) .with_timeout_per_test(per_task_timeout) // each task (including the setup function) may take up to `per_task_timeout`. .with_overall_timeout(overall_timeout) // the entire group may take up to `overall_timeout`. diff --git a/rs/tests/networking/BUILD.bazel b/rs/tests/networking/BUILD.bazel index d6898463753..468fdda5f63 100644 --- a/rs/tests/networking/BUILD.bazel +++ b/rs/tests/networking/BUILD.bazel @@ -1,4 +1,4 @@ -load("//rs/tests:common.bzl", "BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS", "CANISTER_HTTP_RUNTIME_DEPS", "COUNTER_CANISTER_RUNTIME_DEPS", "DEPENDENCIES", "GRAFANA_RUNTIME_DEPS", "GUESTOS_RUNTIME_DEPS", "MACRO_DEPENDENCIES", "UNIVERSAL_VM_RUNTIME_DEPS") +load("//rs/tests:common.bzl", "BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS", "CANISTER_HTTP_RUNTIME_DEPS", "COUNTER_CANISTER_RUNTIME_DEPS", "GRAFANA_RUNTIME_DEPS", "GUESTOS_RUNTIME_DEPS", "UNIVERSAL_VM_RUNTIME_DEPS") load("//rs/tests:system_tests.bzl", "system_test", "system_test_nns") package(default_visibility = ["//rs:system-tests-pkg"]) @@ -12,7 +12,6 @@ CANISTER_HTTP_BASE_DEPS = [ # Keep sorted. "//rs/rust_canisters/dfn_candid", "//rs/rust_canisters/proxy_canister:lib", - "//rs/tests", "//rs/tests/driver:ic-system-test-driver", "//rs/tests/networking/canister_http:canister_http", "//rs/types/management_canister_types", @@ -22,6 +21,17 @@ CANISTER_HTTP_BASE_DEPS = [ "@crate_index//:slog", ] +COMMON_DEPS = [ + # Keep sorted. + "//rs/limits", + "//rs/registry/subnet_type", + "//rs/tests/driver:ic-system-test-driver", + "@crate_index//:anyhow", + "@crate_index//:slog", + "@crate_index//:slog-async", + "@crate_index//:slog-term", +] + system_test_nns( name = "canister_http_test", env = { @@ -29,7 +39,6 @@ system_test_nns( }, extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "k8s", "long_test", # since it takes longer than 5 minutes. @@ -48,7 +57,6 @@ system_test_nns( "PROXY_WASM_PATH": "$(rootpath //rs/rust_canisters/proxy_canister:proxy_canister)", }, flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ # TODO(NET-1710): enable on CI again when the problematic firewall rule in the IC node has been removed. #"system_test_hourly", @@ -73,7 +81,6 @@ system_test_nns( "PROXY_WASM_PATH": "$(rootpath //rs/rust_canisters/proxy_canister:proxy_canister)", }, flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "k8s", ], @@ -99,7 +106,6 @@ system_test_nns( "PROXY_WASM_PATH": "$(rootpath //rs/rust_canisters/proxy_canister:proxy_canister)", }, flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "k8s", "manual", @@ -125,7 +131,6 @@ system_test_nns( "PROXY_WASM_PATH": "$(rootpath //rs/rust_canisters/proxy_canister:proxy_canister)", }, flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "k8s", "system_test_hourly", @@ -141,97 +146,116 @@ system_test_nns( system_test_nns( name = "firewall_max_connections_test", flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "system_test_hourly", ], target_compatible_with = ["@platforms//os:linux"], runtime_deps = GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], + deps = COMMON_DEPS + [ + "@crate_index//:tokio", + "@crate_index//:tokio-util", + ], ) system_test_nns( name = "firewall_priority_test", flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "system_test_hourly", ], target_compatible_with = ["@platforms//os:linux"], runtime_deps = GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], + deps = COMMON_DEPS + [ + "//rs/nns/governance/api", + "//rs/protobuf", + "//rs/registry/canister", + "//rs/registry/keys", + "//rs/types/types", + "@crate_index//:candid", + "@crate_index//:reqwest", + "@crate_index//:url", + ], ) system_test_nns( - name = "network_reliability_test", + name = "network_large_test", extra_head_nns_tags = ["manual"], # only run this test with the mainnet NNS canisters. flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "k8s", "system_test_nightly", ], target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = GUESTOS_RUNTIME_DEPS + COUNTER_CANISTER_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], + test_timeout = "eternal", + runtime_deps = GUESTOS_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS, + deps = COMMON_DEPS + [ + "//rs/types/types", + "@crate_index//:tokio", + "@crate_index//:tokio-util", + ], ) system_test_nns( - name = "network_large_test", + name = "network_reliability_test", extra_head_nns_tags = ["manual"], # only run this test with the mainnet NNS canisters. flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "k8s", "system_test_nightly", ], target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - test_timeout = "eternal", - runtime_deps = GUESTOS_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], + runtime_deps = GUESTOS_RUNTIME_DEPS + COUNTER_CANISTER_RUNTIME_DEPS, + deps = COMMON_DEPS + [ + "//rs/types/base_types", + "@crate_index//:rand", + "@crate_index//:rand_chacha", + ], ) system_test_nns( - name = "query_workload_long_test", + name = "p2p_performance_test", flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ "k8s", - "system_test_hourly", + "manual", ], target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - test_timeout = "long", - runtime_deps = GUESTOS_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS + COUNTER_CANISTER_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], + test_timeout = "eternal", + runtime_deps = GUESTOS_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS + COUNTER_CANISTER_RUNTIME_DEPS + [ + "//rs/tests:jaeger_uvm_config_image", + ], + deps = COMMON_DEPS + [ + "@crate_index//:ic-agent", + ], ) system_test_nns( - name = "update_workload_large_payload", + name = "query_workload_long_test", flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ + "k8s", "system_test_hourly", ], target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS test_timeout = "long", runtime_deps = GUESTOS_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS + COUNTER_CANISTER_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], + deps = COMMON_DEPS + [ + "//rs/tests/networking/subnet_update_workload", + ], ) system_test_nns( - name = "p2p_performance", + name = "update_workload_large_payload", flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, tags = [ - "k8s", - "manual", + "system_test_hourly", ], target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - test_timeout = "eternal", - runtime_deps = GUESTOS_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS + COUNTER_CANISTER_RUNTIME_DEPS + [ - "//rs/tests:jaeger_uvm_config_image", + test_timeout = "long", + runtime_deps = GUESTOS_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS + COUNTER_CANISTER_RUNTIME_DEPS, + deps = COMMON_DEPS + [ + "//rs/tests/networking/subnet_update_workload", ], - deps = DEPENDENCIES + ["//rs/tests"], ) system_test( diff --git a/rs/tests/networking/Cargo.toml b/rs/tests/networking/Cargo.toml index 2bb3bfd3462..295e85713eb 100644 --- a/rs/tests/networking/Cargo.toml +++ b/rs/tests/networking/Cargo.toml @@ -14,11 +14,16 @@ canister-test = { path = "../../rust_canisters/canister_test" } canister_http = { path = "./canister_http" } cloner-canister-types = { path = "./canisters" } dfn_candid = { path = "../../rust_canisters/dfn_candid" } +ic-agent = { workspace = true } ic-base-types = { path = "../../types/base_types" } ic-cdk = { workspace = true } ic-limits = { path = "../../limits" } ic-management-canister-types = { path = "../../types/management_canister_types" } +ic-networking-subnet-update-workload = { path = "./subnet_update_workload" } +ic-nns-governance-api = { path = "../../nns/governance/api" } ic-prep = { path = "../../prep" } +ic-protobuf = { path = "../../protobuf" } +ic-registry-keys = { path = "../../registry/keys" } ic-registry-subnet-features = { path = "../../registry/subnet_features" } ic-registry-subnet-type = { path = "../../registry/subnet_type" } ic-system-test-driver = { path = "../driver" } @@ -27,10 +32,13 @@ ic-test-utilities-types = { path = "../../test_utilities/types" } ic-types = { path = "../../types/types" } ic-utils = { workspace = true } proxy_canister = { path = "../../rust_canisters/proxy_canister" } +rand = { workspace = true } +rand_chacha = { workspace = true } +registry-canister = {path = "../../registry/canister"} reqwest = { workspace = true } slog = { workspace = true } -tests = { path = ".." } tokio = { workspace = true } +url = { workspace = true } [[bin]] name = "ic-systest-canister-http-correctness" @@ -70,7 +78,7 @@ path = "network_reliability_test.rs" [[bin]] name = "ic-systest-p2p-performance" -path = "p2p_performance.rs" +path = "p2p_performance_test.rs" [[bin]] name = "ic-systest-query-workload-long-test" diff --git a/rs/tests/networking/firewall_max_connections_test.rs b/rs/tests/networking/firewall_max_connections_test.rs index 0dd02b01850..cafd27c3817 100644 --- a/rs/tests/networking/firewall_max_connections_test.rs +++ b/rs/tests/networking/firewall_max_connections_test.rs @@ -1,12 +1,183 @@ -#[rustfmt::skip] +/* tag::catalog[] +Title:: Firewall limit connection count. + +Goal:: Verify that nodes set a hard limit on number of simultaneous connections from a single IP addresses as defined in the firewall. + +Runbook:: +. Set up a test net, application typ, with 2 nodes. +. Set up a universal vm with default config. +. Set `max_simultaneous_connections_per_ip_address` to the configured value `max_simultaneous_connections_per_ip_address` in template file +`ic.json5.template`. +. Create `max_simultaneous_connections_per_ip_address` tcp connections from the driver simultaneously to a node and keep the connections alive. +. Verify that the universal vm can create a tcp connection the node. +. Verify the driver is unable to create new tcp connections to the node. +. Terminate one of the active connections the driver has to the node. +. Verify the node now accepts one connection at a time on ports [8080, 9090, 9091, 9100] from the driver. +. All connectivity tests succeed as expected + +end::catalog[] */ use anyhow::Result; -use ic_system_test_driver::{driver::group::SystemTestGroup, systest}; -use ic_tests::networking::firewall_max_connections::{config, connection_count_test}; +use ic_registry_subnet_type::SubnetType; +use ic_system_test_driver::{ + driver::{ + group::SystemTestGroup, + ic::{InternetComputer, Subnet}, + test_env::TestEnv, + test_env_api::{HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer, SshSession}, + universal_vm::{UniversalVm, UniversalVms}, + }, + systest, + util::block_on, +}; +use slog::{debug, info}; +use std::net::IpAddr; +use std::time::Duration; +use tokio::net::TcpStream; + +// This value reflects the value `max_simultaneous_connections_per_ip_address` in the firewall config file. +const MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS: usize = 1000; + +const UNIVERSAL_VM_NAME: &str = "httpbin"; + +const TCP_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2); + +fn setup(env: TestEnv) { + let log = env.logger(); + + info!(log, "Starting new universal VM"); + UniversalVm::new(String::from(UNIVERSAL_VM_NAME)) + .start(&env) + .expect("failed to setup universal VM"); + + info!(log, "Universal VM successfully deployed."); + InternetComputer::new() + .add_subnet(Subnet::fast(SubnetType::Application, 2)) + .setup_and_start(&env) + .expect("failed to setup IC under test"); + + let topology = env.topology_snapshot(); + topology.subnets().for_each(|subnet| { + subnet + .nodes() + .for_each(|node| node.await_status_is_healthy().unwrap()) + }); +} + +fn connection_count_test(env: TestEnv) { + let log = env.logger(); + let topology = env.topology_snapshot(); + + let node_with_firewall = topology + .subnets() + .find(|s| s.subnet_type() == SubnetType::Application) + .unwrap() + .nodes() + .next() + .unwrap(); + + let deployed_universal_vm = env + .get_deployed_universal_vm(UNIVERSAL_VM_NAME) + .expect("unable to get deployed VM."); + + let universal_vm = deployed_universal_vm.get_vm().unwrap(); + + let node_ip_addr = node_with_firewall.get_ip_addr(); + + debug!( + log, + "`max_simultaneous_connections_per_ip_address` = {}, VM IP = {}, node Ip = {}", + MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS, + universal_vm.ipv6, + node_ip_addr + ); + + info!( + log, + "Attempting to create `max_simultaneous_connections_per_ip_address` tcp connections from driver to the node." + ); + + let mut streams = Vec::with_capacity(MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS); + + for connection_number in 0..MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS { + let stream = block_on(create_tcp_connection(node_ip_addr, 9090)); + match stream { + Ok(stream) => streams.push(stream), + Err(_) => { + panic!("Could not create connection {}#. Connection is below the limit of active connections defined in the firewall, and should be accepted", connection_number); + } + } + } + + info!( + log, + "Created `max_simultaneous_connections_per_ip_address` tcp connections successfully. Now attempting to perform tcp handshakes from the virtual vm to the node." + ); + + // Connect to VM with SSH and establish TCP connection to the node. + + let script = format!("nc -z {} {}", node_ip_addr, 9090); + + let result: String = deployed_universal_vm + .block_on_bash_script(&script) + .expect("Couldn't run bash script over ssh."); + + info!( + log, + "Universal VM successfully connected the the node. STDOUT: {}", result + ); + + // Make connections from driver to node that should be rejected. + + info!( + log, + "Making a connection from driver to the node that should be rejected by the firewall!" + ); + + let ports = vec![8080, 9090, 9091, 9100]; + + for port in &ports { + debug!(log, "Attempting connection on port: {}", *port); + let connection = block_on(create_tcp_connection(node_ip_addr, *port)); + assert!( + connection.is_err(), + "Was able to make more requests than the configured firewall limit" + ); + } + info!( + log, + "{} {}", + "All connection attempts over firewall limit were rejected by the node.", + "Terminating an existing connection, to verify new one can be established." + ); + + drop(streams.pop()); + + for port in &ports { + debug!(log, "Attempting connection on port: {}", *port); + let connection = block_on(create_tcp_connection(node_ip_addr, *port)); + assert!( + connection.is_ok(), + "Was not able to make new connection after dropping previous connections", + ); + } +} + +/// Helper function to make a tcp connection where the server +/// can drop incoming connections. +async fn create_tcp_connection(ip_addr: IpAddr, port: u16) -> Result { + let tcp = + tokio::time::timeout(TCP_HANDSHAKE_TIMEOUT, TcpStream::connect((ip_addr, port))).await; + + match tcp { + Ok(Ok(stream)) => Ok(stream), + _ => Err(()), + } +} fn main() -> Result<()> { SystemTestGroup::new() - .with_setup(config) + .with_setup(setup) .add_test(systest!(connection_count_test)) .execute_from_args()?; Ok(()) diff --git a/rs/tests/networking/firewall_priority_test.rs b/rs/tests/networking/firewall_priority_test.rs index 922fb64f306..da323ee251d 100644 --- a/rs/tests/networking/firewall_priority_test.rs +++ b/rs/tests/networking/firewall_priority_test.rs @@ -1,16 +1,486 @@ -#[rustfmt::skip] +/* tag::catalog[] +Title:: Firewall Priority + +Goal:: Checks the precedence levels of the firewall configurations + +Runbook:: +. set up the testnet and startup firewall configuration of replica_nodes +. get the existing rule that allows access at a port +. add a firewall rule that's a copy of above fetched rule +. verify that ports are still reachable +. add another rule that is a copy of the above, but denies access to port 9090, with higher priority +. verify the port is unreachable with the new rule +. add another rule to position 0, but now allowing access to port 9090 +. verify the port is now reachable again +. remove that last added rule +. verify that the port is unreachable +. update the other existing rule to block port 9091 instead of 9090 +. verify that port 9091 is unreachable, and 9090 is reachable +. update the same rule to block port 8080 +. verify that port 8080 is unreachable (from the test machine) +. verify that port 8080 is still reachable from replica nodes + +Success:: +. all connectivity tests succeed as expected + +end::catalog[] */ use anyhow::Result; +use candid::CandidType; +use ic_nns_governance_api::pb::v1::NnsFunction; +use ic_protobuf::registry::firewall::v1::{FirewallAction, FirewallRule, FirewallRuleDirection}; +use ic_registry_keys::FirewallRulesScope; +use ic_registry_subnet_type::SubnetType; +use ic_system_test_driver::{ + driver::{ + group::SystemTestGroup, + ic::{InternetComputer, Subnet}, + test_env::TestEnv, + test_env_api::{ + HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer, IcNodeSnapshot, + NnsInstallationBuilder, SshSession, + }, + }, + nns::{ + await_proposal_execution, submit_external_proposal_with_test_id, + vote_execute_proposal_assert_executed, + }, + systest, + util::{self, block_on}, +}; +use registry_canister::mutations::firewall::{ + compute_firewall_ruleset_hash, AddFirewallRulesPayload, RemoveFirewallRulesPayload, + UpdateFirewallRulesPayload, +}; +use slog::{info, Logger}; +use std::time::Duration; +use url::Url; +const INITIAL_WAIT: Duration = Duration::from_secs(10); +const WAIT_TIMEOUT: Duration = Duration::from_secs(60); +const BACKOFF_DELAY: Duration = Duration::from_secs(5); +const MAX_WAIT: Duration = Duration::from_secs(120); + +enum Proposal { + Add(T, NnsFunction), + Remove(T, NnsFunction), + Update(T, NnsFunction), +} + +pub fn setup(env: TestEnv) { + InternetComputer::new() + .add_subnet(Subnet::fast(SubnetType::System, 1)) + .add_subnet(Subnet::fast(SubnetType::Application, 2)) + .setup_and_start(&env) + .expect("failed to setup IC under test"); + env.topology_snapshot().subnets().for_each(|subnet| { + subnet + .nodes() + .for_each(|node| node.await_status_is_healthy().unwrap()) + }); +} + +pub fn override_firewall_rules_with_priority(env: TestEnv) { + let log = env.logger(); + let topology = env.topology_snapshot(); + let nns_node = env + .topology_snapshot() + .root_subnet() + .nodes() + .next() + .unwrap(); + let toggle_endpoint = topology + .subnets() + .find(|s| s.subnet_type() == SubnetType::Application) + .unwrap() + .nodes() + .next() + .unwrap(); + info!(log, "Installing NNS canisters on the root subnet..."); + NnsInstallationBuilder::new() + .install(&nns_node, &env) + .expect("Could not install NNS canisters"); + info!(&log, "NNS canisters installed successfully."); + + let mut toggle_metrics_url = toggle_endpoint.get_public_url(); + toggle_metrics_url.set_port(Some(9090)).unwrap(); + let mut toggle_9091_url = toggle_endpoint.get_public_url(); + toggle_9091_url.set_port(Some(9091)).unwrap(); + let mut toggle_xnet_url = toggle_endpoint.get_public_url(); + toggle_xnet_url.set_port(Some(2497)).unwrap(); + + info!(log, "Firewall priority test is starting"); + + // assert before a new rule is added, port 9090 is available + assert!(get_request_succeeds(&toggle_metrics_url)); + // assert port 8080 is available + assert!(get_request_succeeds(&toggle_endpoint.get_public_url())); + + info!( + log, + "Firewall priority test is ready. Setting default rules in the registry..." + ); + + // Set the default rules in the registry for the first time + block_on(set_default_registry_rules(&log, &nns_node)); + + info!( + log, + "Default rules set. Testing connectivity with backoff..." + ); + + assert!(await_rule_execution_with_backoff( + &log, + &|| { + // assert that ports 9090 and 8080 are still available + get_request_succeeds(&toggle_metrics_url) + && get_request_succeeds(&toggle_endpoint.get_public_url()) + }, + INITIAL_WAIT, + BACKOFF_DELAY, + MAX_WAIT + )); + + info!( + log, + "Succeeded. Adding a rule to deny port 9090 on node {}...", toggle_endpoint.node_id + ); + + // add a firewall rule that disables 9090 on the first node + let firewall_config = util::get_config().firewall.unwrap(); + + let deny_port = FirewallAction::Deny; + let mut node_rules = vec![FirewallRule { + ipv4_prefixes: vec![], + ipv6_prefixes: firewall_config.default_rules[0].ipv6_prefixes.clone(), + ports: vec![9090], + action: deny_port.into(), + comment: "Test rule".to_string(), + user: None, + direction: Some(FirewallRuleDirection::Inbound as i32), + }]; + let proposal = prepare_add_rules_proposal( + FirewallRulesScope::Node(toggle_endpoint.node_id), + node_rules.clone(), + vec![0], + vec![], + ); + block_on(execute_proposal( + &log, + &nns_node, + Proposal::Add(proposal, NnsFunction::AddFirewallRules), + )); + + info!(log, "New rule is set. Testing connectivity with backoff..."); + assert!(await_rule_execution_with_backoff( + &log, + &|| { + // assert port 9090 is now turned off + !get_request_succeeds(&toggle_metrics_url) + && get_request_succeeds(&toggle_endpoint.get_public_url()) + }, + INITIAL_WAIT, + BACKOFF_DELAY, + MAX_WAIT + )); + + info!( + log, + "Succeeded. Adding a higher priority rule to allow port 9090 on node {}...", + toggle_endpoint.node_id + ); + // add a firewall rule that re-enables port 9090 + let allow_port = FirewallAction::Allow; + let mut new_rule = node_rules[0].clone(); + new_rule.action = allow_port.into(); + let proposal = prepare_add_rules_proposal( + FirewallRulesScope::Node(toggle_endpoint.node_id), + vec![new_rule.clone()], + vec![0], + node_rules.clone(), + ); + node_rules = vec![new_rule, node_rules[0].clone()]; + block_on(execute_proposal( + &log, + &nns_node, + Proposal::Add(proposal, NnsFunction::AddFirewallRules), + )); + + info!(log, "New rule is set. Testing connectivity with backoff..."); + assert!(await_rule_execution_with_backoff( + &log, + &|| { + // assert port 9090 is now restored + get_request_succeeds(&toggle_metrics_url) + }, + INITIAL_WAIT, + BACKOFF_DELAY, + MAX_WAIT + )); + + info!( + log, + "Succeeded. Removing the higher priority rule for node {}...", toggle_endpoint.node_id + ); + + // Remove the last rule we added + let proposal = prepare_remove_rules_proposal( + FirewallRulesScope::Node(toggle_endpoint.node_id), + vec![0], + node_rules.clone(), + ); + node_rules = vec![node_rules[1].clone()]; + block_on(execute_proposal( + &log, + &nns_node, + Proposal::Remove(proposal, NnsFunction::RemoveFirewallRules), + )); + + info!(log, "Rule is removed. Testing connectivity with backoff..."); + assert!(await_rule_execution_with_backoff( + &log, + &|| { + // assert port 9090 is now turned off + !get_request_succeeds(&toggle_metrics_url) + }, + INITIAL_WAIT, + BACKOFF_DELAY, + MAX_WAIT + )); + + info!( + log, + "Succeeded. Updating the existing rule for node {} to block port 9091...", + toggle_endpoint.node_id + ); -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::systest; + // Update the other existing node-specific rule to block port 9091 + let mut updated_rule = node_rules[0].clone(); + updated_rule.ports = vec![9091]; + let proposal = prepare_update_rules_proposal( + FirewallRulesScope::Node(toggle_endpoint.node_id), + vec![updated_rule.clone()], + vec![0], + node_rules, + ); + node_rules = vec![updated_rule]; + block_on(execute_proposal( + &log, + &nns_node, + Proposal::Update(proposal, NnsFunction::UpdateFirewallRules), + )); + + info!(log, "Rule is updated. Testing connectivity with backoff..."); + assert!(await_rule_execution_with_backoff( + &log, + &|| { + // assert port 9091 is now turned off and port 9090 is now turned on + !get_request_succeeds(&toggle_9091_url) && get_request_succeeds(&toggle_metrics_url) + }, + INITIAL_WAIT, + BACKOFF_DELAY, + MAX_WAIT + )); + + info!( + log, + "Succeeded. Updating the existing rule for node {} to block http port...", + toggle_endpoint.node_id + ); + + // Update the existing node-specific rule to block port {http} + let mut updated_rule = node_rules[0].clone(); + updated_rule.ports = vec![toggle_endpoint.get_public_url().port().unwrap().into()]; + let proposal = prepare_update_rules_proposal( + FirewallRulesScope::Node(toggle_endpoint.node_id), + vec![updated_rule], + vec![0], + node_rules, + ); + block_on(execute_proposal( + &log, + &nns_node, + Proposal::Update(proposal, NnsFunction::UpdateFirewallRules), + )); + + info!(log, "Rule is updated. Testing connectivity with backoff..."); + + assert!(await_rule_execution_with_backoff( + &log, + &|| { + // assert port 8080 is now turned off + !get_request_succeeds(&toggle_endpoint.get_public_url()) + }, + INITIAL_WAIT, + BACKOFF_DELAY, + MAX_WAIT + )); + + // Verify that port {xnet} is reachable on this node from other nodes + let node = env + .topology_snapshot() + .subnets() + .find(|s| s.subnet_type() == SubnetType::System) + .unwrap() + .nodes() + .next() + .unwrap(); + + let session = node.block_on_ssh_session().unwrap(); + info!( + log, + "Calling curl {} from node {}", + toggle_endpoint.get_public_url(), + node.node_id + ); + + let res = node.block_on_bash_script_from_session( + &session, + &format!("timeout 10s curl {}", toggle_endpoint.get_public_url()), + ); + assert!(res.is_ok()); + + info!(log, "Firewall priority tests has succeeded.") +} + +async fn set_default_registry_rules(log: &Logger, nns_node: &IcNodeSnapshot) { + let firewall_config = util::get_config().firewall.unwrap(); + let default_rules = firewall_config.default_rules.clone(); + let proposal = prepare_add_rules_proposal( + FirewallRulesScope::ReplicaNodes, + default_rules.clone(), + (0..default_rules.len()).map(|u| u as i32).collect(), + vec![], + ); + execute_proposal( + log, + nns_node, + Proposal::Add(proposal, NnsFunction::AddFirewallRules), + ) + .await; +} + +async fn execute_proposal( + log: &Logger, + nns_node: &IcNodeSnapshot, + proposal: Proposal, +) { + let (proposal_payload, function) = match proposal { + Proposal::Add(payload, func) => (payload, func), + Proposal::Remove(payload, func) => (payload, func), + Proposal::Update(payload, func) => (payload, func), + }; + let nns = util::runtime_from_url(nns_node.get_public_url(), nns_node.effective_canister_id()); + let governance = ic_system_test_driver::nns::get_governance_canister(&nns); + let proposal_id = + submit_external_proposal_with_test_id(&governance, function, proposal_payload.clone()) + .await; + vote_execute_proposal_assert_executed(&governance, proposal_id).await; + + // wait until proposal is executed + await_proposal_execution(log, &governance, proposal_id, BACKOFF_DELAY, WAIT_TIMEOUT).await; +} + +fn get_request_succeeds(url: &Url) -> bool { + let http_client = reqwest::blocking::ClientBuilder::new() + .timeout(BACKOFF_DELAY) + .build() + .expect("Could not build reqwest client."); + + http_client.get(url.clone()).send().is_ok() +} + +fn prepare_add_rules_proposal( + scope: FirewallRulesScope, + new_rules: Vec, + positions_sorted: Vec, + previous_rules: Vec, +) -> AddFirewallRulesPayload { + let mut all_rules = previous_rules; + for (rule, pos) in new_rules.iter().zip(positions_sorted.clone()) { + all_rules.insert(pos as usize, rule.clone()); + } + AddFirewallRulesPayload { + scope, + rules: new_rules, + positions: positions_sorted, + expected_hash: compute_firewall_ruleset_hash(&all_rules), + } +} + +fn prepare_remove_rules_proposal( + scope: FirewallRulesScope, + positions: Vec, + previous_rules: Vec, +) -> RemoveFirewallRulesPayload { + let mut all_rules = previous_rules; + let mut positions_sorted = positions.clone(); + positions_sorted.sort_unstable(); + positions_sorted.reverse(); + for pos in positions_sorted { + all_rules.remove(pos as usize); + } + RemoveFirewallRulesPayload { + scope, + positions, + expected_hash: compute_firewall_ruleset_hash(&all_rules), + } +} + +fn prepare_update_rules_proposal( + scope: FirewallRulesScope, + new_rules: Vec, + positions_sorted: Vec, + previous_rules: Vec, +) -> UpdateFirewallRulesPayload { + let mut all_rules = previous_rules; + for (rule, pos) in new_rules.iter().zip(positions_sorted.clone()) { + all_rules[pos as usize] = rule.clone(); + } + UpdateFirewallRulesPayload { + scope, + rules: new_rules, + positions: positions_sorted, + expected_hash: compute_firewall_ruleset_hash(&all_rules), + } +} + +fn await_rule_execution_with_backoff( + log: &slog::Logger, + test: &dyn Fn() -> bool, + initial_wait: Duration, + linear_backoff: Duration, + max_wait: Duration, +) -> bool { + let mut total_duration = initial_wait; + std::thread::sleep(initial_wait); + if test() { + info!( + log, + "(Waited {} seconds, succeeded)", + total_duration.as_secs() + ); + return true; + } + while total_duration < max_wait { + std::thread::sleep(linear_backoff); + total_duration += linear_backoff; + if test() { + info!( + log, + "(Waited {} seconds, succeeded)", + total_duration.as_secs() + ); + return true; + } + } + info!(log, "(Waited {} seconds, failed)", total_duration.as_secs()); + false +} fn main() -> Result<()> { SystemTestGroup::new() - .with_setup(ic_tests::networking::firewall_priority::config) - .add_test(systest!( - ic_tests::networking::firewall_priority::override_firewall_rules_with_priority - )) + .with_setup(setup) + .add_test(systest!(override_firewall_rules_with_priority)) .execute_from_args()?; Ok(()) diff --git a/rs/tests/networking/network_large_test.rs b/rs/tests/networking/network_large_test.rs index c76bcbd2935..9880d515854 100644 --- a/rs/tests/networking/network_large_test.rs +++ b/rs/tests/networking/network_large_test.rs @@ -1,16 +1,198 @@ -use std::time::Duration; +/* tag::catalog[] +Title:: Subnet makes progress despite one third of the nodes being stressed. -#[rustfmt::skip] +Runbook:: +0. Instantiate an IC with one System subnet larger than the current production NNS. +1. Install NNS canisters on the System subnet. +2. Build and install canister that stores msgs. +3. Let subnet run idle for a few minutes and confirm that it is up and running by storing message. +4. Stop f nodes and confirm subnet still is available. +5. Stop f+1 nodes and confirm that subnet is not making progress. +6. Restart one node such that we have f faulty nodes again and confirm subnet is available again. +7. Let subnet run idle with f faulty nodes and confirm that everything works. -use anyhow::Result; +end::catalog[] */ -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::systest; -use ic_tests::networking::network_large::{setup, test}; +use anyhow::Result; +use ic_registry_subnet_type::SubnetType; +use ic_system_test_driver::{ + driver::{ + group::SystemTestGroup, + ic::{AmountOfMemoryKiB, InternetComputer, NrOfVCPUs, Subnet, VmResources}, + prometheus_vm::{HasPrometheus, PrometheusVm}, + test_env::TestEnv, + test_env_api::{ + HasPublicApiUrl, HasTopologySnapshot, HasVm, IcNodeContainer, NnsInstallationBuilder, + }, + }, + systest, + util::{assert_create_agent, block_on, MessageCanister}, +}; +use ic_types::Height; +use slog::info; +use std::time::Duration; // Timeout parameters const TASK_TIMEOUT: Duration = Duration::from_secs(320 * 60); const OVERALL_TIMEOUT: Duration = Duration::from_secs(350 * 60); +const UPDATE_MSG_1: &str = "This beautiful prose should be persisted for future generations"; +const UPDATE_MSG_2: &str = "I just woke up"; +const UPDATE_MSG_3: &str = "And this beautiful prose should be persisted for future generations"; +const UPDATE_MSG_4: &str = "However this prose will NOT be persisted for future generations"; +const UPDATE_MSG_5: &str = "This will be persisted again!"; +const UPDATE_MSG_6: &str = "Fell asleep again!"; + +const FAULTY: usize = 16; +const NODES: usize = 3 * FAULTY + 1; // 49 + +const IDLE_DURATION: Duration = Duration::from_secs(10 * 60); + +pub fn setup(env: TestEnv) { + let vm_resources = VmResources { + vcpus: Some(NrOfVCPUs::new(8)), + memory_kibibytes: Some(AmountOfMemoryKiB::new(4195000)), // 4GiB + boot_image_minimal_size_gibibytes: None, + }; + PrometheusVm::default() + .start(&env) + .expect("failed to start prometheus VM"); + InternetComputer::new() + .add_subnet( + Subnet::new(SubnetType::System) + .with_default_vm_resources(vm_resources) + // Use low DKG interval to confirm system works across interval boundaries. + .with_dkg_interval_length(Height::from(99)) + .add_nodes(NODES), + ) + .add_subnet( + Subnet::new(SubnetType::Application) + .with_default_vm_resources(vm_resources) + .with_dkg_interval_length(Height::from(49)) + .add_nodes(1), + ) + .setup_and_start(&env) + .expect("Failed to setup IC under test."); + env.sync_with_prometheus(); +} + +pub fn test(env: TestEnv) { + let log = env.logger(); + info!( + &log, + "Step 0: Checking readiness of all nodes after the IC setup ..." + ); + env.topology_snapshot().subnets().for_each(|subnet| { + subnet + .nodes() + .for_each(|node| node.await_status_is_healthy().unwrap()) + }); + info!(&log, "All nodes are ready, IC setup succeeded."); + + info!( + &log, + "Step 1: Installing NNS canisters on the System subnet ..." + ); + let nns_node = env + .topology_snapshot() + .root_subnet() + .nodes() + .next() + .unwrap(); + NnsInstallationBuilder::new() + .install(&nns_node, &env) + .expect("Could not install NNS canisters."); + + info!( + &log, + "Step 2: Build and install one counter canisters on each subnet. ..." + ); + let subnet = env.topology_snapshot().subnets().next().unwrap(); + let node = subnet.nodes().last().unwrap(); + let agent = block_on(assert_create_agent(node.get_public_url().as_str())); + let message_canister = block_on(MessageCanister::new(&agent, node.effective_canister_id())); + info!(&log, "Installation of counter canisters has succeeded."); + + info!( + log, + "Step 3: Assert that update call to the canister succeeds" + ); + block_on(message_canister.try_store_msg(UPDATE_MSG_1)).expect("Update canister call failed."); + assert_eq!( + block_on(message_canister.try_read_msg()), + Ok(Some(UPDATE_MSG_1.to_string())) + ); + + info!(log, "Step 4: Run idle for a few min"); + block_on(async { tokio::time::sleep(IDLE_DURATION).await }); + block_on(message_canister.try_store_msg(UPDATE_MSG_2)).expect("Update canister call failed."); + assert_eq!( + block_on(message_canister.try_read_msg()), + Ok(Some(UPDATE_MSG_2.to_string())) + ); + + info!(log, "Step 5: Kill {} nodes", FAULTY); + let nodes: Vec<_> = subnet.nodes().collect(); + for node in nodes.iter().take(FAULTY) { + node.vm().kill(); + } + for node in nodes.iter().take(FAULTY) { + node.await_status_is_unavailable() + .expect("Node still healthy"); + } + + info!( + log, + "Step 6: Assert that update call succeeds in presence of {} faulty nodes", FAULTY + ); + block_on(message_canister.try_store_msg(UPDATE_MSG_3)).expect("Update canister call failed."); + assert_eq!( + block_on(message_canister.try_read_msg()), + Ok(Some(UPDATE_MSG_3.to_string())) + ); + + info!( + log, + "Step 7: Kill an additonal node causing consensus to stop due to {} (f+1) faulty nodes", + FAULTY + 1 + ); + nodes[FAULTY].vm().kill(); + nodes[FAULTY] + .await_status_is_unavailable() + .expect("Node still healthy"); + + // Verify that it is not possible to write message + if let Ok(Ok(result)) = block_on(async { + tokio::time::timeout( + std::time::Duration::from_secs(30), + message_canister.try_store_msg(UPDATE_MSG_4), + ) + .await + }) { + panic!("expected the update to fail, got {:?}", result); + }; + + info!(log, "Step 8: Restart one node again",); + nodes[FAULTY].vm().start(); + for n in nodes.iter().skip(FAULTY) { + n.await_status_is_healthy().unwrap(); + } + block_on(message_canister.try_store_msg(UPDATE_MSG_5)).expect("Update canister call failed."); + assert_eq!( + block_on(message_canister.try_read_msg()), + Ok(Some(UPDATE_MSG_5.to_string())) + ); + + info!( + log, + "Step 9: Run idle for a few min on faulty node boundary" + ); + block_on(async { tokio::time::sleep(IDLE_DURATION).await }); + block_on(message_canister.try_store_msg(UPDATE_MSG_6)).expect("Update canister call failed."); + assert_eq!( + block_on(message_canister.try_read_msg()), + Ok(Some(UPDATE_MSG_6.to_string())) + ); +} fn main() -> Result<()> { SystemTestGroup::new() diff --git a/rs/tests/networking/network_reliability_test.rs b/rs/tests/networking/network_reliability_test.rs index 3ae02616763..4c3b2e0b14a 100644 --- a/rs/tests/networking/network_reliability_test.rs +++ b/rs/tests/networking/network_reliability_test.rs @@ -1,12 +1,84 @@ -use std::time::Duration; +/* tag::catalog[] +Title:: Subnet makes progress despite one third of the nodes being stressed. -#[rustfmt::skip] +Runbook:: +0. Instantiate an IC with one System and one Application subnet. +1. Install NNS canisters on the System subnet. +2. Build and install one counter canister on each subnet. +3. Instantiate and start workload for the APP subnet using a subset of 1/3 of the nodes as targets. + Workload send update[canister_id, "write"] requests. + Requests are equally distributed between this subset of 1/3 nodes. +4. Stress (modify tc settings on) another disjoint subset of 1/3 of the nodes (during the workload execution). + Stressing manifests in introducing randomness in: latency, bandwidth, packet drops percentage, stress duration. +5. Collect metrics from the workload and assert: + - Ratio of requests with duration below DURATION_THRESHOLD should exceed MIN_REQUESTS_RATIO_BELOW_THRESHOLD. +6. Perform assertions for both counter canisters (via query `read` call) + - Counter value on the canisters should exceed the threshold = (1 - max_failures_ratio) * total_requests_count. + +end::catalog[] */ use anyhow::Result; +use ic_base_types::NodeId; +use ic_registry_subnet_type::SubnetType; +use ic_system_test_driver::{ + canister_api::{CallMode, GenericRequest}, + driver::{ + constants::DEVICE_NAME, + group::SystemTestGroup, + ic::{AmountOfMemoryKiB, InternetComputer, NrOfVCPUs, Subnet, VmResources}, + test_env::TestEnv, + test_env_api::{ + HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer, IcNodeSnapshot, + NnsInstallationBuilder, SshSession, + }, + }, + systest, + util::{ + self, agent_observes_canister_module, assert_canister_counter_with_retries, block_on, + spawn_round_robin_workload_engine, + }, +}; +use rand::distributions::{Distribution, Uniform}; +use rand_chacha::ChaCha8Rng; +use slog::{debug, info, Logger}; +use std::cmp::max; +use std::io::{self}; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +const COUNTER_CANISTER_WAT: &str = "rs/tests/src/counter.wat"; +const CANISTER_METHOD: &str = "write"; +// Seed for random generator +const RND_SEED: u64 = 42; +// Size of the payload sent to the counter canister in update("write") call. +const PAYLOAD_SIZE_BYTES: usize = 1024; +// Duration of each request is placed into one of two categories - below or above this threshold. +const DURATION_THRESHOLD: Duration = Duration::from_secs(20); +// Parameters related to nodes stressing. +const BANDWIDTH_MIN: u32 = 10; +const BANDWIDTH_MAX: u32 = 100; +const LATENCY_MIN: Duration = Duration::from_millis(10); +const LATENCY_MAX: Duration = Duration::from_millis(990); +const DROPS_PERC_MIN: u32 = 1; +const DROPS_PERC_MAX: u32 = 99; +const MIN_NODE_STRESS_TIME: Duration = Duration::from_secs(10); +const MIN_NODE_UNSTRESSED_TIME: Duration = Duration::ZERO; +const FRACTION_FROM_REMAINING_DURATION: f64 = 0.25; +// Parameters related to reading/asserting counter values of the canisters. +const MAX_CANISTER_READ_RETRIES: u32 = 4; +const CANISTER_READ_RETRY_WAIT: Duration = Duration::from_secs(10); +// Parameters related to workload creation. +const REQUESTS_DISPATCH_EXTRA_TIMEOUT: Duration = Duration::from_secs(1); // This param can be slightly tweaked (1-2 sec), if the workload fails to dispatch requests precisely on time. -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::systest; -use ic_tests::networking::network_reliability::{setup, test, Config}; +// Test can be run with different setup/configuration parameters. +// This config holds these parameters. +#[derive(Copy, Clone, Debug)] +pub struct Config { + pub nodes_system_subnet: usize, + pub nodes_app_subnet: usize, + pub runtime: Duration, + pub rps: usize, +} // Test parameters const CONFIG: Config = Config { @@ -19,6 +91,326 @@ const CONFIG: Config = Config { const TASK_TIMEOUT: Duration = Duration::from_secs(320 * 60); const OVERALL_TIMEOUT: Duration = Duration::from_secs(350 * 60); +pub fn setup(env: TestEnv, config: Config) { + let vm_resources = VmResources { + vcpus: Some(NrOfVCPUs::new(8)), + memory_kibibytes: Some(AmountOfMemoryKiB::new(50331648)), // 48GiB + boot_image_minimal_size_gibibytes: None, + }; + InternetComputer::new() + .add_subnet( + Subnet::new(SubnetType::System) + .with_default_vm_resources(vm_resources) + .add_nodes(config.nodes_system_subnet), + ) + .add_subnet( + Subnet::new(SubnetType::Application) + .with_default_vm_resources(vm_resources) + .add_nodes(config.nodes_app_subnet), + ) + .setup_and_start(&env) + .expect("Failed to setup IC under test."); +} + +pub fn test(env: TestEnv, config: Config) { + let log = env.logger(); + info!( + &log, + "Step 0: Checking readiness of all nodes after the IC setup ..." + ); + env.topology_snapshot().subnets().for_each(|subnet| { + subnet + .nodes() + .for_each(|node| node.await_status_is_healthy().unwrap()) + }); + info!(&log, "All nodes are ready, IC setup succeeded."); + info!( + &log, + "Step 1: Installing NNS canisters on the System subnet ..." + ); + let nns_node = env + .topology_snapshot() + .root_subnet() + .nodes() + .next() + .unwrap(); + NnsInstallationBuilder::new() + .install(&nns_node, &env) + .expect("Could not install NNS canisters."); + info!( + &log, + "Step 2: Build and install one counter canisters on each subnet. ..." + ); + let subnet_app = env + .topology_snapshot() + .subnets() + .find(|s| s.subnet_type() == SubnetType::Application) + .unwrap(); + let canister_app = subnet_app + .nodes() + .next() + .unwrap() + .create_and_install_canister_with_arg(COUNTER_CANISTER_WAT, None); + info!(&log, "Installation of counter canisters has succeeded."); + info!(&log, "Step 3: Instantiate and start one workload per subnet using a subset of 1/3 of the nodes as targets."); + let workload_app_nodes_count = config.nodes_app_subnet / 3; + info!( + &log, + "Launching two workloads for both subnets in separate threads against {} node/s.", + workload_app_nodes_count + ); + let agents_app: Vec<_> = subnet_app + .nodes() + .take(workload_app_nodes_count) + .map(|node| { + debug!( + &log, + "Node with id={} from APP will be used for the workload.", node.node_id + ); + node.with_default_agent(|agent| async move { agent }) + }) + .collect(); + assert!( + agents_app.len() == workload_app_nodes_count, + "Number of nodes and agents do not match." + ); + info!( + &log, + "Asserting all agents observe the installed canister ..." + ); + block_on(async { + for agent in agents_app.iter() { + assert!( + agent_observes_canister_module(agent, &canister_app).await, + "Canister module not available" + ); + } + }); + info!(&log, "All agents observe the installed canister module."); + // Spawn two workloads in separate threads, as we will need to have execution context to stress nodes. + let payload: Vec = vec![0; PAYLOAD_SIZE_BYTES]; + let start_time = Instant::now(); + let stop_time = start_time + config.runtime; + let handle_workload_app = { + let requests = vec![GenericRequest::new( + canister_app, + CANISTER_METHOD.to_string(), + payload.clone(), + CallMode::Update, + )]; + spawn_round_robin_workload_engine( + log.clone(), + requests, + agents_app, + config.rps, + config.runtime, + REQUESTS_DISPATCH_EXTRA_TIMEOUT, + vec![DURATION_THRESHOLD], + ) + }; + info!( + &log, + "Step 4: Stress another disjoint subset of 1/3 of the nodes (during the workload execution)." + ); + let stress_app_nodes_count = config.nodes_app_subnet / 3; + assert!( + stress_app_nodes_count > 0, + "At least one node needs to be stressed on each subnet." + ); + // We stress (modify node's traffic) using random parameters. + let rng: ChaCha8Rng = rand::SeedableRng::seed_from_u64(RND_SEED); + // Stress function for each node is executed in a separate thread. + let stress_app_handles: Vec<_> = subnet_app + .nodes() + .skip(workload_app_nodes_count) + .take(stress_app_nodes_count) + .map(|node| stress_node_periodically(log.clone(), rng.clone(), node, stop_time)) + .collect(); + + for h in stress_app_handles { + let stress_info = h + .join() + .expect("Thread execution failed.") + .unwrap_or_else(|err| { + panic!("Node stressing failed err={}", err); + }); + info!(&log, "{:?}", stress_info); + } + info!( + &log, + "Step 5: Collect metrics from both workloads and perform assertions ..." + ); + let load_metrics_app = handle_workload_app + .join() + .expect("Workload execution against APP subnet failed."); + info!( + &log, + "Workload execution results for APP: {load_metrics_app}" + ); + let requests_count_below_threshold_app = + load_metrics_app.requests_count_below_threshold(DURATION_THRESHOLD); + let min_expected_success_count = config.rps * config.runtime.as_secs() as usize; + assert_eq!(load_metrics_app.failure_calls(), 0); + assert!(requests_count_below_threshold_app + .iter() + .all(|(_, count)| *count as usize == min_expected_success_count)); + let agent_app = subnet_app + .nodes() + .next() + .map(|node| node.with_default_agent(|agent| async move { agent })) + .unwrap(); + info!( + &log, + "Step 6: Assert min counter value on both canisters has been reached ... " + ); + block_on(async { + assert_canister_counter_with_retries( + &log, + &agent_app, + &canister_app, + payload.clone(), + min_expected_success_count, + MAX_CANISTER_READ_RETRIES, + CANISTER_READ_RETRY_WAIT, + ) + .await; + }); +} + +#[derive(Debug)] +struct NodeStressInfo { + _node_id: NodeId, + stressed_times: u32, + stressed_mode_duration: Duration, + normal_mode_duration: Duration, +} + +fn stress_node_periodically( + log: Logger, + mut rng: ChaCha8Rng, + node: IcNodeSnapshot, + stop_time: Instant, +) -> JoinHandle> { + thread::spawn(move || { + let mut stress_info = NodeStressInfo { + _node_id: node.node_id, + stressed_times: 0, + normal_mode_duration: Duration::default(), + stressed_mode_duration: Duration::default(), + }; + + let should_stop = |remaining: Duration| { + if remaining.is_zero() { + info!(&log, "Stressing node with id={} is finished.", node.node_id); + true + } else { + false + } + }; + + // Session is an expensive resource, so we create it once per node. + let session = node + .block_on_ssh_session() + .expect("Failed to ssh into node"); + + loop { + // First keep the node in unstressed mode. + let remaining_duration = stop_time.saturating_duration_since(Instant::now()); + if should_stop(remaining_duration) { + break; + } else { + let _ = node + .block_on_bash_script_from_session(&session, &reset_tc_ssh_command()) + .expect("Failed to execute bash script from session"); + let max_duration = + fraction_of_duration(remaining_duration, FRACTION_FROM_REMAINING_DURATION); + let sleep_time = Uniform::from( + MIN_NODE_UNSTRESSED_TIME..=max(max_duration, MIN_NODE_UNSTRESSED_TIME), + ) + .sample(&mut rng); + info!( + &log, + "Node with id={} is in normal (unstressed) mode for {} sec.", + node.node_id, + sleep_time.as_secs() + ); + thread::sleep(sleep_time); + stress_info.normal_mode_duration = + stress_info.normal_mode_duration.saturating_add(sleep_time); + } + // Now stress the node modifying its traffic parameters. + let remaining_duration = stop_time.saturating_duration_since(Instant::now()); + if should_stop(remaining_duration) { + break; + } else { + let max_duration = + fraction_of_duration(remaining_duration, FRACTION_FROM_REMAINING_DURATION); + let action_time = + Uniform::from(MIN_NODE_STRESS_TIME..=max(max_duration, MIN_NODE_STRESS_TIME)) + .sample(&mut rng); + let tc_rules = node + .block_on_bash_script_from_session( + &session, + &limit_tc_randomly_ssh_command(&mut rng), + ) + .expect("Failed to execute bash script from session"); + info!( + &log, + "Node with id={} is stressed for {} sec. The applied tc rules are:\n{}", + node.node_id, + action_time.as_secs(), + tc_rules.as_str() + ); + thread::sleep(action_time); + stress_info.stressed_mode_duration = stress_info + .stressed_mode_duration + .saturating_add(action_time); + stress_info.stressed_times += 1; + } + } + Ok(stress_info) + }) +} + +fn fraction_of_duration(time: Duration, fraction: f64) -> Duration { + Duration::from_secs((time.as_secs() as f64 * fraction) as u64) +} + +fn reset_tc_ssh_command() -> String { + format!( + r#"set -euo pipefail + sudo tc qdisc del dev {device} root 2> /dev/null || true + "#, + device = DEVICE_NAME + ) +} + +fn limit_tc_randomly_ssh_command(mut rng: &mut ChaCha8Rng) -> String { + let bandwidth_dist = Uniform::from(BANDWIDTH_MIN..=BANDWIDTH_MAX); + let latency_dist = Uniform::from(LATENCY_MIN..=LATENCY_MAX); + let drops_perc_dist = Uniform::from(DROPS_PERC_MIN..=DROPS_PERC_MAX); + let cfg = util::get_config(); + let p2p_listen_port = cfg.transport.unwrap().listening_port; + // The second command deletes existing tc rules (if present). + // The last command reads the active tc rules. + format!( + r#"set -euo pipefail +sudo tc qdisc del dev {device} root 2> /dev/null || true +sudo tc qdisc add dev {device} root handle 1: prio +sudo tc qdisc add dev {device} parent 1:3 handle 10: tbf rate {bandwidth_mbit}mbit latency 400ms burst 100000 +sudo tc qdisc add dev {device} parent 10:1 handle 20: netem delay {latency_ms}ms 5ms drop {drops_percentage}% +sudo tc qdisc add dev {device} parent 20:1 handle 30: sfq +sudo tc filter add dev {device} protocol ipv6 parent 1:0 prio 3 u32 match ip6 dport {p2p_listen_port} 0xFFFF flowid 1:3 +sudo tc qdisc show dev {device} +"#, + device = DEVICE_NAME, + bandwidth_mbit = bandwidth_dist.sample(&mut rng), + latency_ms = latency_dist.sample(&mut rng).as_millis(), + drops_percentage = drops_perc_dist.sample(&mut rng), + p2p_listen_port = p2p_listen_port + ) +} + fn main() -> Result<()> { let setup = |env| setup(env, CONFIG); let test = |env| test(env, CONFIG); diff --git a/rs/tests/networking/p2p_performance.rs b/rs/tests/networking/p2p_performance.rs deleted file mode 100644 index df4f12f94d4..00000000000 --- a/rs/tests/networking/p2p_performance.rs +++ /dev/null @@ -1,51 +0,0 @@ -use anyhow::Result; -use std::time::Duration; - -use ic_system_test_driver::{ - driver::{group::SystemTestGroup, simulate_network::ProductionSubnetTopology}, - systest, -}; -use ic_tests::networking::p2p_performance_workload::{config, test}; - -// Test parameters -const RPS: usize = 10; -const PAYLOAD_SIZE_BYTES: usize = 1024 * 1024; -const WORKLOAD_RUNTIME: Duration = Duration::from_secs(30 * 60); -const NNS_SUBNET_MAX_SIZE: usize = 1; -const APP_SUBNET_MAX_SIZE: usize = 13; -const DOWNLOAD_PROMETHEUS_DATA: bool = true; -// Timeout parameters -const TASK_TIMEOUT_DELTA: Duration = Duration::from_secs(3600); -const OVERALL_TIMEOUT_DELTA: Duration = Duration::from_secs(3600); -// Network topology -const NETWORK_SIMULATION: Option = Some(ProductionSubnetTopology::IO67); - -fn main() -> Result<()> { - let per_task_timeout: Duration = WORKLOAD_RUNTIME + TASK_TIMEOUT_DELTA; - let overall_timeout: Duration = per_task_timeout + OVERALL_TIMEOUT_DELTA; - let config = |env| { - config( - env, - NNS_SUBNET_MAX_SIZE, - APP_SUBNET_MAX_SIZE, - NETWORK_SIMULATION, - None, - ) - }; - let test = |env| { - test( - env, - RPS, - PAYLOAD_SIZE_BYTES, - WORKLOAD_RUNTIME, - DOWNLOAD_PROMETHEUS_DATA, - ) - }; - SystemTestGroup::new() - .with_setup(config) - .add_test(systest!(test)) - .with_timeout_per_test(per_task_timeout) // each task (including the setup function) may take up to `per_task_timeout`. - .with_overall_timeout(overall_timeout) // the entire group may take up to `overall_timeout`. - .execute_from_args()?; - Ok(()) -} diff --git a/rs/tests/src/networking/p2p_performance_workload.rs b/rs/tests/networking/p2p_performance_test.rs similarity index 85% rename from rs/tests/src/networking/p2p_performance_workload.rs rename to rs/tests/networking/p2p_performance_test.rs index 8ffcfcbc7c2..f4869eb8b1c 100644 --- a/rs/tests/src/networking/p2p_performance_workload.rs +++ b/rs/tests/networking/p2p_performance_test.rs @@ -1,7 +1,13 @@ +#[rustfmt::skip] + +use anyhow::{bail, Result}; +use ic_agent::Agent; +use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::{ canister_api::{CallMode, GenericRequest}, driver::{ farm::HostFeature, + group::SystemTestGroup, ic::{AmountOfMemoryKiB, ImageSizeGiB, InternetComputer, NrOfVCPUs, Subnet, VmResources}, prometheus_vm::{HasPrometheus, PrometheusVm}, simulate_network::{ProductionSubnetTopology, SimulateNetwork}, @@ -12,18 +18,28 @@ use ic_system_test_driver::{ }, universal_vm::{UniversalVm, UniversalVms}, }, + systest, util::{agent_observes_canister_module, block_on, spawn_round_robin_workload_engine}, }; - -use anyhow::bail; -use ic_agent::Agent; -use ic_registry_subnet_type::SubnetType; use slog::{debug, info, Logger}; use std::{ net::{IpAddr, SocketAddr}, time::Duration, }; +// Test parameters +const RPS: usize = 10; +const PAYLOAD_SIZE_BYTES: usize = 1024 * 1024; +const WORKLOAD_RUNTIME: Duration = Duration::from_secs(30 * 60); +const NNS_SUBNET_MAX_SIZE: usize = 1; +const APP_SUBNET_MAX_SIZE: usize = 13; +const DOWNLOAD_PROMETHEUS_DATA: bool = true; +// Timeout parameters +const TASK_TIMEOUT_DELTA: Duration = Duration::from_secs(3600); +const OVERALL_TIMEOUT_DELTA: Duration = Duration::from_secs(3600); +// Network topology +const NETWORK_SIMULATION: Option = Some(ProductionSubnetTopology::IO67); + const COUNTER_CANISTER_WAT: &str = "rs/tests/src/counter.wat"; const CANISTER_METHOD: &str = "write"; // Duration of each request is placed into one of the two categories - below or above this threshold. @@ -36,9 +52,7 @@ const JAEGER_VM_NAME: &str = "jaeger-vm"; // 5 minutes const DOWNLOAD_PROMETHEUS_WAIT_TIME: Duration = Duration::from_secs(60 * 60); -// Create an IC with two subnets, with variable number of nodes. -// Install NNS canister on system subnet. -pub fn config( +pub fn setup( env: TestEnv, nodes_nns_subnet: usize, nodes_app_subnet: usize, @@ -260,3 +274,33 @@ fn create_agents_for_subnet(log: &Logger, subnet: &SubnetSnapshot) -> Vec }) .collect::<_>() } + +fn main() -> Result<()> { + let per_task_timeout: Duration = WORKLOAD_RUNTIME + TASK_TIMEOUT_DELTA; + let overall_timeout: Duration = per_task_timeout + OVERALL_TIMEOUT_DELTA; + let setup = |env| { + setup( + env, + NNS_SUBNET_MAX_SIZE, + APP_SUBNET_MAX_SIZE, + NETWORK_SIMULATION, + None, + ) + }; + let test = |env| { + test( + env, + RPS, + PAYLOAD_SIZE_BYTES, + WORKLOAD_RUNTIME, + DOWNLOAD_PROMETHEUS_DATA, + ) + }; + SystemTestGroup::new() + .with_setup(setup) + .add_test(systest!(test)) + .with_timeout_per_test(per_task_timeout) // each task (including the setup function) may take up to `per_task_timeout`. + .with_overall_timeout(overall_timeout) // the entire group may take up to `overall_timeout`. + .execute_from_args()?; + Ok(()) +} diff --git a/rs/tests/networking/query_workload_long_test.rs b/rs/tests/networking/query_workload_long_test.rs index 46eda307b67..a6dca7a4278 100644 --- a/rs/tests/networking/query_workload_long_test.rs +++ b/rs/tests/networking/query_workload_long_test.rs @@ -1,14 +1,50 @@ -#[rustfmt::skip] +/* tag::catalog[] + +Title:: Single replica handles query workloads. + +Goal:: Ensure IC responds to queries of a given size in a timely manner. + +Runbook:: +0. Instantiate an IC with one System and one Application subnet. + - Optionally install one boundary node. +1. Install NNS canisters on the System subnet. +2. Build and install one counter canister on the Application subnet. +3. Instantiate and start a workload against the Application subnet. + Workload sends query[canister_id, "read"] requests. + All requests are sent to the same node/replica. +4. Collect metrics from the workload and assert: + - Ratio of requests with duration below DURATION_THRESHOLD should exceed MIN_REQUESTS_RATIO_BELOW_THRESHOLD. + - Ratio of successful requests should exceed min_success_ratio threshold. + +end::catalog[] */ use anyhow::Result; +use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; +use ic_networking_subnet_update_workload::setup; +use ic_registry_subnet_type::SubnetType; +use ic_system_test_driver::{ + canister_api::{CallMode, GenericRequest}, + driver::{ + group::SystemTestGroup, + ic::ImageSizeGiB, + test_env::TestEnv, + test_env_api::{HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer}, + }, + systest, + util::spawn_round_robin_workload_engine, +}; +use slog::{debug, info, Logger}; +use std::process::Command; use std::time::Duration; -use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::driver::ic::ImageSizeGiB; -use ic_system_test_driver::systest; -use ic_tests::networking::replica_query_workload::test; -use ic_tests::networking::subnet_update_workload::config; +const COUNTER_CANISTER_WAT: &str = "rs/tests/src/counter.wat"; +const CANISTER_METHOD: &str = "read"; +// Size of the payload sent to the counter canister in query("write") call. +const PAYLOAD_SIZE_BYTES: usize = 1024; +// Duration of each request is placed into one of two categories - below or above this threshold. +const DURATION_THRESHOLD: Duration = Duration::from_secs(2); +// Parameters related to workload creation. +const REQUESTS_DISPATCH_EXTRA_TIMEOUT: Duration = Duration::from_secs(2); // This param can be slightly tweaked (1-2 sec), if the workload fails to dispatch requests precisely on time. // Test parameters // This value should more or less equal to @@ -21,11 +57,95 @@ const WORKLOAD_RUNTIME: Duration = Duration::from_secs(5 * 60); const TASK_TIMEOUT_DELTA: Duration = Duration::from_secs(10 * 60); const OVERALL_TIMEOUT_DELTA: Duration = Duration::from_secs(5 * 60); +pub fn log_max_open_files(log: &Logger) { + let output = Command::new("sh") + .arg("-c") + .arg("ulimit -n") + .output() + .unwrap(); + let output = String::from_utf8_lossy(&output.stdout).replace('\n', ""); + info!(&log, "ulimit -n: {}", output); +} + +// Run a test with configurable number of query requests per second, +// duration of the test, and the required success ratio. +pub fn test(env: TestEnv, rps: usize, runtime: Duration) { + let log = env.logger(); + log_max_open_files(&log); + info!( + &log, + "Checking readiness of all nodes after the IC setup ..." + ); + let topology_snapshot = env.topology_snapshot(); + topology_snapshot.subnets().for_each(|subnet| { + subnet + .nodes() + .for_each(|node| node.await_status_is_healthy().unwrap()) + }); + info!(&log, "All nodes are ready, IC setup succeeded."); + info!( + &log, + "Step 2: Build and install one counter canister on the Application subnet..." + ); + let app_subnet = topology_snapshot + .subnets() + .find(|s| s.subnet_type() == SubnetType::Application) + .unwrap(); + // Take the first node in the Application subnet. + let app_node = app_subnet.nodes().next().unwrap(); + debug!( + &log, + "Node with id={} from the Application subnet will be used as a target for the workload.", + app_node.node_id + ); + let app_canister = app_node.create_and_install_canister_with_arg(COUNTER_CANISTER_WAT, None); + info!(&log, "Installation of counter canister has succeeded."); + info!(&log, "Step 3: Instantiate and start a workload using one node of the Application subnet as target."); + // Workload sends messages to canister via node agents. + // As we talk to a single node, we create one agent, accordingly. + let app_agent = app_node.with_default_agent(|agent| async move { agent }); + // Spawn a workload against counter canister. + let handle_workload = { + let requests = vec![GenericRequest::new( + app_canister, + CANISTER_METHOD.to_string(), + vec![0; PAYLOAD_SIZE_BYTES], + CallMode::Query, + )]; + spawn_round_robin_workload_engine( + log.clone(), + requests, + vec![app_agent], + rps, + runtime, + REQUESTS_DISPATCH_EXTRA_TIMEOUT, + vec![DURATION_THRESHOLD], + ) + }; + let load_metrics = handle_workload.join().expect("Workload execution failed."); + info!( + &log, + "Step 4: Collect metrics from the workload and perform assertions ..." + ); + let requests_count_below_threshold = + load_metrics.requests_count_below_threshold(DURATION_THRESHOLD); + info!(log, "Workload execution results: {load_metrics}"); + assert_eq!( + load_metrics.failure_calls(), + 0, + "Too many requests have failed." + ); + let min_expected_counter = rps as u64 * runtime.as_secs(); + assert!(requests_count_below_threshold + .iter() + .all(|(_, count)| *count == min_expected_counter)); +} + fn main() -> Result<()> { let per_task_timeout: Duration = WORKLOAD_RUNTIME + TASK_TIMEOUT_DELTA; // This should be a bit larger than the workload execution time. let overall_timeout: Duration = per_task_timeout + OVERALL_TIMEOUT_DELTA; // This should be a bit larger than the per_task_timeout. - let config = |env| { - config( + let setup = |env| { + setup( env, SMALL_APP_SUBNET_MAX_SIZE, USE_BOUNDARY_NODE, @@ -36,7 +156,7 @@ fn main() -> Result<()> { }; let test = |env| test(env, RPS, WORKLOAD_RUNTIME); SystemTestGroup::new() - .with_setup(config) + .with_setup(setup) .add_test(systest!(test)) .with_timeout_per_test(per_task_timeout) // each task (including the setup function) may take up to `per_task_timeout`. .with_overall_timeout(overall_timeout) // the entire group may take up to `overall_timeout`. diff --git a/rs/tests/networking/subnet_update_workload/BUILD.bazel b/rs/tests/networking/subnet_update_workload/BUILD.bazel new file mode 100644 index 00000000000..85eaa7c1a91 --- /dev/null +++ b/rs/tests/networking/subnet_update_workload/BUILD.bazel @@ -0,0 +1,30 @@ +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//rs:system-tests-pkg"]) + +rust_library( + name = "subnet_update_workload", + testonly = True, + srcs = glob(["src/**/*.rs"]), + crate_name = "ic_networking_subnet_update_workload", + crate_root = "src/lib.rs", + deps = [ + # Keep sorted. + "//rs/interfaces/registry", + "//rs/protobuf", + "//rs/registry/canister", + "//rs/registry/keys", + "//rs/registry/nns_data_provider", + "//rs/registry/routing_table", + "//rs/registry/subnet_type", + "//rs/tests/driver:ic-system-test-driver", + "@crate_index//:anyhow", + "@crate_index//:ic-agent", + "@crate_index//:slog", + "@crate_index//:slog-async", + "@crate_index//:slog-term", + "@crate_index//:tokio", + "@crate_index//:tokio-util", + "@crate_index//:url", + ], +) diff --git a/rs/tests/networking/subnet_update_workload/Cargo.toml b/rs/tests/networking/subnet_update_workload/Cargo.toml new file mode 100644 index 00000000000..df90a473ae4 --- /dev/null +++ b/rs/tests/networking/subnet_update_workload/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ic-networking-subnet-update-workload" +version.workspace = true +authors.workspace = true +edition.workspace = true +description.workspace = true +documentation.workspace = true + +[dependencies] +anyhow = { workspace = true } +ic-agent = { workspace = true } +ic-interfaces-registry = { path = "../../../interfaces/registry" } +ic-protobuf = { path = "../../../protobuf" } +ic-registry-canister-api = { path = "../../../registry/canister/api" } +ic-registry-keys = { path = "../../../registry/keys" } +ic-registry-nns-data-provider = { path = "../../../registry/nns_data_provider" } +ic-registry-routing-table = { path = "../../../registry/routing_table" } +ic-registry-subnet-type = { path = "../../../registry/subnet_type" } +ic-system-test-driver = { path = "../../driver" } +registry-canister = {path = "../../../registry/canister"} +slog = { workspace = true } +slog-async = { workspace = true } +slog-term = { workspace = true } +tokio = { workspace = true } +tokio-util = { workspace = true } +url = { workspace = true } \ No newline at end of file diff --git a/rs/tests/src/networking/subnet_update_workload.rs b/rs/tests/networking/subnet_update_workload/src/lib.rs similarity index 99% rename from rs/tests/src/networking/subnet_update_workload.rs rename to rs/tests/networking/subnet_update_workload/src/lib.rs index e90738c43eb..63cfe8661f6 100644 --- a/rs/tests/src/networking/subnet_update_workload.rs +++ b/rs/tests/networking/subnet_update_workload/src/lib.rs @@ -66,7 +66,7 @@ const REQUESTS_DISPATCH_EXTRA_TIMEOUT: Duration = Duration::from_secs(2); // Thi // Create an IC with two subnets, with variable number of nodes and boundary nodes // Install NNS canister on system subnet -pub fn config( +pub fn setup( env: TestEnv, nodes_app_subnet: usize, use_boundary_node: bool, diff --git a/rs/tests/networking/update_workload_large_payload.rs b/rs/tests/networking/update_workload_large_payload.rs index 3fdeadda1c1..118fcff24f5 100644 --- a/rs/tests/networking/update_workload_large_payload.rs +++ b/rs/tests/networking/update_workload_large_payload.rs @@ -4,9 +4,9 @@ use anyhow::Result; use std::time::Duration; use ic_limits::SMALL_APP_SUBNET_MAX_SIZE; +use ic_networking_subnet_update_workload::{setup, test}; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::systest; -use ic_tests::networking::subnet_update_workload::{config, test}; // Test parameters const RPS: usize = 5; @@ -20,7 +20,7 @@ const OVERALL_TIMEOUT_DELTA: Duration = Duration::from_secs(5 * 60); fn main() -> Result<()> { let per_task_timeout: Duration = WORKLOAD_RUNTIME + TASK_TIMEOUT_DELTA; // This should be a bit larger than the workload execution time. let overall_timeout: Duration = per_task_timeout + OVERALL_TIMEOUT_DELTA; // This should be a bit larger than the per_task_timeout. - let config = |env| config(env, SMALL_APP_SUBNET_MAX_SIZE, USE_BOUNDARY_NODE, None); + let setup = |env| setup(env, SMALL_APP_SUBNET_MAX_SIZE, USE_BOUNDARY_NODE, None); let test = |env| { test( env, @@ -31,7 +31,7 @@ fn main() -> Result<()> { ) }; SystemTestGroup::new() - .with_setup(config) + .with_setup(setup) .add_test(systest!(test)) .with_timeout_per_test(per_task_timeout) // each task (including the setup function) may take up to `per_task_timeout`. .with_overall_timeout(overall_timeout) // the entire group may take up to `overall_timeout`. diff --git a/rs/tests/src/lib.rs b/rs/tests/src/lib.rs index 3ce34589a29..fdd1d8c88a5 100644 --- a/rs/tests/src/lib.rs +++ b/rs/tests/src/lib.rs @@ -1,6 +1,5 @@ pub mod api_test; pub mod ledger_tests; -pub mod networking; pub mod nns_tests; pub mod rosetta_test; pub mod rosetta_tests; diff --git a/rs/tests/src/networking/firewall_max_connections.rs b/rs/tests/src/networking/firewall_max_connections.rs deleted file mode 100644 index 7556b973fce..00000000000 --- a/rs/tests/src/networking/firewall_max_connections.rs +++ /dev/null @@ -1,173 +0,0 @@ -/* tag::catalog[] -Title:: Firewall limit connection count. - -Goal:: Verify that nodes set a hard limit on number of simultaneous connections from a single IP addresses as defined in the firewall. - -Runbook:: -. Set up a test net, application typ, with 2 nodes. -. Set up a universal vm with default config. -. Set `max_simultaneous_connections_per_ip_address` to the configured value `max_simultaneous_connections_per_ip_address` in template file -`ic.json5.template`. -. Create `max_simultaneous_connections_per_ip_address` tcp connections from the driver simultaneously to a node and keep the connections alive. -. Verify that the universal vm can create a tcp connection the node. -. Verify the driver is unable to create new tcp connections to the node. -. Terminate one of the active connections the driver has to the node. -. Verify the node now accepts one connection at a time on ports [8080, 9090, 9091, 9100] from the driver. -. All connectivity tests succeed as expected - -end::catalog[] */ - -use ic_registry_subnet_type::SubnetType; -use ic_system_test_driver::{ - driver::{ - ic::{InternetComputer, Subnet}, - test_env::TestEnv, - test_env_api::{HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer, SshSession}, - universal_vm::{UniversalVm, UniversalVms}, - }, - util::block_on, -}; -use slog::{debug, info}; -use std::net::IpAddr; -use std::time::Duration; -use tokio::net::TcpStream; - -/// This value reflects the value `max_simultaneous_connections_per_ip_address` in the firewall config file. -const MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS: usize = 1000; - -const UNIVERSAL_VM_NAME: &str = "httpbin"; - -const TCP_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2); - -pub fn config(env: TestEnv) { - let log = env.logger(); - - info!(log, "Starting new universal VM"); - UniversalVm::new(String::from(UNIVERSAL_VM_NAME)) - .start(&env) - .expect("failed to setup universal VM"); - - info!(log, "Universal VM successfully deployed."); - InternetComputer::new() - .add_subnet(Subnet::fast(SubnetType::Application, 2)) - .setup_and_start(&env) - .expect("failed to setup IC under test"); - - let topology = env.topology_snapshot(); - topology.subnets().for_each(|subnet| { - subnet - .nodes() - .for_each(|node| node.await_status_is_healthy().unwrap()) - }); -} - -pub fn connection_count_test(env: TestEnv) { - let log = env.logger(); - let topology = env.topology_snapshot(); - - let node_with_firewall = topology - .subnets() - .find(|s| s.subnet_type() == SubnetType::Application) - .unwrap() - .nodes() - .next() - .unwrap(); - - let deployed_universal_vm = env - .get_deployed_universal_vm(UNIVERSAL_VM_NAME) - .expect("unable to get deployed VM."); - - let universal_vm = deployed_universal_vm.get_vm().unwrap(); - - let node_ip_addr = node_with_firewall.get_ip_addr(); - - debug!( - log, - "`max_simultaneous_connections_per_ip_address` = {}, VM IP = {}, node Ip = {}", - MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS, - universal_vm.ipv6, - node_ip_addr - ); - - info!( - log, - "Attempting to create `max_simultaneous_connections_per_ip_address` tcp connections from driver to the node." - ); - - let mut streams = Vec::with_capacity(MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS); - - for connection_number in 0..MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS { - let stream = block_on(create_tcp_connection(node_ip_addr, 9090)); - match stream { - Ok(stream) => streams.push(stream), - Err(_) => { - panic!("Could not create connection {}#. Connection is below the limit of active connections defined in the firewall, and should be accepted", connection_number); - } - } - } - - info!( - log, - "Created `max_simultaneous_connections_per_ip_address` tcp connections successfully. Now attempting to perform tcp handshakes from the virtual vm to the node." - ); - - // Connect to VM with SSH and establish TCP connection to the node. - - let script = format!("nc -z {} {}", node_ip_addr, 9090); - - let result: String = deployed_universal_vm - .block_on_bash_script(&script) - .expect("Couldn't run bash script over ssh."); - - info!( - log, - "Universal VM successfully connected the the node. STDOUT: {}", result - ); - - // Make connections from driver to node that should be rejected. - - info!( - log, - "Making a connection from driver to the node that should be rejected by the firewall!" - ); - - let ports = vec![8080, 9090, 9091, 9100]; - - for port in &ports { - debug!(log, "Attempting connection on port: {}", *port); - let connection = block_on(create_tcp_connection(node_ip_addr, *port)); - assert!( - connection.is_err(), - "Was able to make more requests than the configured firewall limit" - ); - } - info!( - log, - "{} {}", - "All connection attempts over firewall limit were rejected by the node.", - "Terminating an existing connection, to verify new one can be established." - ); - - drop(streams.pop()); - - for port in &ports { - debug!(log, "Attempting connection on port: {}", *port); - let connection = block_on(create_tcp_connection(node_ip_addr, *port)); - assert!( - connection.is_ok(), - "Was not able to make new connection after dropping previous connections", - ); - } -} - -/// Helper function to make a tcp connection where the server -/// can drop incoming connections. -async fn create_tcp_connection(ip_addr: IpAddr, port: u16) -> Result { - let tcp = - tokio::time::timeout(TCP_HANDSHAKE_TIMEOUT, TcpStream::connect((ip_addr, port))).await; - - match tcp { - Ok(Ok(stream)) => Ok(stream), - _ => Err(()), - } -} diff --git a/rs/tests/src/networking/firewall_priority.rs b/rs/tests/src/networking/firewall_priority.rs deleted file mode 100644 index e134c7a06f5..00000000000 --- a/rs/tests/src/networking/firewall_priority.rs +++ /dev/null @@ -1,476 +0,0 @@ -/* tag::catalog[] -Title:: Firewall Priority - -Goal:: Checks the precedence levels of the firewall configurations - -Runbook:: -. set up the testnet and startup firewall configuration of replica_nodes -. get the existing rule that allows access at a port -. add a firewall rule that's a copy of above fetched rule -. verify that ports are still reachable -. add another rule that is a copy of the above, but denies access to port 9090, with higher priority -. verify the port is unreachable with the new rule -. add another rule to position 0, but now allowing access to port 9090 -. verify the port is now reachable again -. remove that last added rule -. verify that the port is unreachable -. update the other existing rule to block port 9091 instead of 9090 -. verify that port 9091 is unreachable, and 9090 is reachable -. update the same rule to block port 8080 -. verify that port 8080 is unreachable (from the test machine) -. verify that port 8080 is still reachable from replica nodes - -Success:: -. all connectivity tests succeed as expected - -end::catalog[] */ - -use candid::CandidType; -use ic_nns_governance_api::pb::v1::NnsFunction; -use ic_protobuf::registry::firewall::v1::{FirewallAction, FirewallRule, FirewallRuleDirection}; -use ic_registry_keys::FirewallRulesScope; -use ic_registry_subnet_type::SubnetType; -use ic_system_test_driver::{ - driver::{ - ic::{InternetComputer, Subnet}, - test_env::TestEnv, - test_env_api::{ - HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer, IcNodeSnapshot, - NnsInstallationBuilder, SshSession, - }, - }, - nns::{ - await_proposal_execution, submit_external_proposal_with_test_id, - vote_execute_proposal_assert_executed, - }, - util::{self, block_on}, -}; -use registry_canister::mutations::firewall::{ - compute_firewall_ruleset_hash, AddFirewallRulesPayload, RemoveFirewallRulesPayload, - UpdateFirewallRulesPayload, -}; -use slog::{info, Logger}; -use std::time::Duration; -use url::Url; - -const INITIAL_WAIT: Duration = Duration::from_secs(10); -const WAIT_TIMEOUT: Duration = Duration::from_secs(60); -const BACKOFF_DELAY: Duration = Duration::from_secs(5); -const MAX_WAIT: Duration = Duration::from_secs(120); - -enum Proposal { - Add(T, NnsFunction), - Remove(T, NnsFunction), - Update(T, NnsFunction), -} - -pub fn config(env: TestEnv) { - InternetComputer::new() - .add_subnet(Subnet::fast(SubnetType::System, 1)) - .add_subnet(Subnet::fast(SubnetType::Application, 2)) - .setup_and_start(&env) - .expect("failed to setup IC under test"); - env.topology_snapshot().subnets().for_each(|subnet| { - subnet - .nodes() - .for_each(|node| node.await_status_is_healthy().unwrap()) - }); -} - -pub fn override_firewall_rules_with_priority(env: TestEnv) { - let log = env.logger(); - let topology = env.topology_snapshot(); - let nns_node = env - .topology_snapshot() - .root_subnet() - .nodes() - .next() - .unwrap(); - let toggle_endpoint = topology - .subnets() - .find(|s| s.subnet_type() == SubnetType::Application) - .unwrap() - .nodes() - .next() - .unwrap(); - info!(log, "Installing NNS canisters on the root subnet..."); - NnsInstallationBuilder::new() - .install(&nns_node, &env) - .expect("Could not install NNS canisters"); - info!(&log, "NNS canisters installed successfully."); - - let mut toggle_metrics_url = toggle_endpoint.get_public_url(); - toggle_metrics_url.set_port(Some(9090)).unwrap(); - let mut toggle_9091_url = toggle_endpoint.get_public_url(); - toggle_9091_url.set_port(Some(9091)).unwrap(); - let mut toggle_xnet_url = toggle_endpoint.get_public_url(); - toggle_xnet_url.set_port(Some(2497)).unwrap(); - - info!(log, "Firewall priority test is starting"); - - // assert before a new rule is added, port 9090 is available - assert!(get_request_succeeds(&toggle_metrics_url)); - // assert port 8080 is available - assert!(get_request_succeeds(&toggle_endpoint.get_public_url())); - - info!( - log, - "Firewall priority test is ready. Setting default rules in the registry..." - ); - - // Set the default rules in the registry for the first time - block_on(set_default_registry_rules(&log, &nns_node)); - - info!( - log, - "Default rules set. Testing connectivity with backoff..." - ); - - assert!(await_rule_execution_with_backoff( - &log, - &|| { - // assert that ports 9090 and 8080 are still available - get_request_succeeds(&toggle_metrics_url) - && get_request_succeeds(&toggle_endpoint.get_public_url()) - }, - INITIAL_WAIT, - BACKOFF_DELAY, - MAX_WAIT - )); - - info!( - log, - "Succeeded. Adding a rule to deny port 9090 on node {}...", toggle_endpoint.node_id - ); - - // add a firewall rule that disables 9090 on the first node - let firewall_config = util::get_config().firewall.unwrap(); - - let deny_port = FirewallAction::Deny; - let mut node_rules = vec![FirewallRule { - ipv4_prefixes: vec![], - ipv6_prefixes: firewall_config.default_rules[0].ipv6_prefixes.clone(), - ports: vec![9090], - action: deny_port.into(), - comment: "Test rule".to_string(), - user: None, - direction: Some(FirewallRuleDirection::Inbound as i32), - }]; - let proposal = prepare_add_rules_proposal( - FirewallRulesScope::Node(toggle_endpoint.node_id), - node_rules.clone(), - vec![0], - vec![], - ); - block_on(execute_proposal( - &log, - &nns_node, - Proposal::Add(proposal, NnsFunction::AddFirewallRules), - )); - - info!(log, "New rule is set. Testing connectivity with backoff..."); - assert!(await_rule_execution_with_backoff( - &log, - &|| { - // assert port 9090 is now turned off - !get_request_succeeds(&toggle_metrics_url) - && get_request_succeeds(&toggle_endpoint.get_public_url()) - }, - INITIAL_WAIT, - BACKOFF_DELAY, - MAX_WAIT - )); - - info!( - log, - "Succeeded. Adding a higher priority rule to allow port 9090 on node {}...", - toggle_endpoint.node_id - ); - // add a firewall rule that re-enables port 9090 - let allow_port = FirewallAction::Allow; - let mut new_rule = node_rules[0].clone(); - new_rule.action = allow_port.into(); - let proposal = prepare_add_rules_proposal( - FirewallRulesScope::Node(toggle_endpoint.node_id), - vec![new_rule.clone()], - vec![0], - node_rules.clone(), - ); - node_rules = vec![new_rule, node_rules[0].clone()]; - block_on(execute_proposal( - &log, - &nns_node, - Proposal::Add(proposal, NnsFunction::AddFirewallRules), - )); - - info!(log, "New rule is set. Testing connectivity with backoff..."); - assert!(await_rule_execution_with_backoff( - &log, - &|| { - // assert port 9090 is now restored - get_request_succeeds(&toggle_metrics_url) - }, - INITIAL_WAIT, - BACKOFF_DELAY, - MAX_WAIT - )); - - info!( - log, - "Succeeded. Removing the higher priority rule for node {}...", toggle_endpoint.node_id - ); - - // Remove the last rule we added - let proposal = prepare_remove_rules_proposal( - FirewallRulesScope::Node(toggle_endpoint.node_id), - vec![0], - node_rules.clone(), - ); - node_rules = vec![node_rules[1].clone()]; - block_on(execute_proposal( - &log, - &nns_node, - Proposal::Remove(proposal, NnsFunction::RemoveFirewallRules), - )); - - info!(log, "Rule is removed. Testing connectivity with backoff..."); - assert!(await_rule_execution_with_backoff( - &log, - &|| { - // assert port 9090 is now turned off - !get_request_succeeds(&toggle_metrics_url) - }, - INITIAL_WAIT, - BACKOFF_DELAY, - MAX_WAIT - )); - - info!( - log, - "Succeeded. Updating the existing rule for node {} to block port 9091...", - toggle_endpoint.node_id - ); - - // Update the other existing node-specific rule to block port 9091 - let mut updated_rule = node_rules[0].clone(); - updated_rule.ports = vec![9091]; - let proposal = prepare_update_rules_proposal( - FirewallRulesScope::Node(toggle_endpoint.node_id), - vec![updated_rule.clone()], - vec![0], - node_rules, - ); - node_rules = vec![updated_rule]; - block_on(execute_proposal( - &log, - &nns_node, - Proposal::Update(proposal, NnsFunction::UpdateFirewallRules), - )); - - info!(log, "Rule is updated. Testing connectivity with backoff..."); - assert!(await_rule_execution_with_backoff( - &log, - &|| { - // assert port 9091 is now turned off and port 9090 is now turned on - !get_request_succeeds(&toggle_9091_url) && get_request_succeeds(&toggle_metrics_url) - }, - INITIAL_WAIT, - BACKOFF_DELAY, - MAX_WAIT - )); - - info!( - log, - "Succeeded. Updating the existing rule for node {} to block http port...", - toggle_endpoint.node_id - ); - - // Update the existing node-specific rule to block port {http} - let mut updated_rule = node_rules[0].clone(); - updated_rule.ports = vec![toggle_endpoint.get_public_url().port().unwrap().into()]; - let proposal = prepare_update_rules_proposal( - FirewallRulesScope::Node(toggle_endpoint.node_id), - vec![updated_rule], - vec![0], - node_rules, - ); - block_on(execute_proposal( - &log, - &nns_node, - Proposal::Update(proposal, NnsFunction::UpdateFirewallRules), - )); - - info!(log, "Rule is updated. Testing connectivity with backoff..."); - - assert!(await_rule_execution_with_backoff( - &log, - &|| { - // assert port 8080 is now turned off - !get_request_succeeds(&toggle_endpoint.get_public_url()) - }, - INITIAL_WAIT, - BACKOFF_DELAY, - MAX_WAIT - )); - - // Verify that port {xnet} is reachable on this node from other nodes - let node = env - .topology_snapshot() - .subnets() - .find(|s| s.subnet_type() == SubnetType::System) - .unwrap() - .nodes() - .next() - .unwrap(); - - let session = node.block_on_ssh_session().unwrap(); - info!( - log, - "Calling curl {} from node {}", - toggle_endpoint.get_public_url(), - node.node_id - ); - - let res = node.block_on_bash_script_from_session( - &session, - &format!("timeout 10s curl {}", toggle_endpoint.get_public_url()), - ); - assert!(res.is_ok()); - - info!(log, "Firewall priority tests has succeeded.") -} - -async fn set_default_registry_rules(log: &Logger, nns_node: &IcNodeSnapshot) { - let firewall_config = util::get_config().firewall.unwrap(); - let default_rules = firewall_config.default_rules.clone(); - let proposal = prepare_add_rules_proposal( - FirewallRulesScope::ReplicaNodes, - default_rules.clone(), - (0..default_rules.len()).map(|u| u as i32).collect(), - vec![], - ); - execute_proposal( - log, - nns_node, - Proposal::Add(proposal, NnsFunction::AddFirewallRules), - ) - .await; -} - -async fn execute_proposal( - log: &Logger, - nns_node: &IcNodeSnapshot, - proposal: Proposal, -) { - let (proposal_payload, function) = match proposal { - Proposal::Add(payload, func) => (payload, func), - Proposal::Remove(payload, func) => (payload, func), - Proposal::Update(payload, func) => (payload, func), - }; - let nns = util::runtime_from_url(nns_node.get_public_url(), nns_node.effective_canister_id()); - let governance = ic_system_test_driver::nns::get_governance_canister(&nns); - let proposal_id = - submit_external_proposal_with_test_id(&governance, function, proposal_payload.clone()) - .await; - vote_execute_proposal_assert_executed(&governance, proposal_id).await; - - // wait until proposal is executed - await_proposal_execution(log, &governance, proposal_id, BACKOFF_DELAY, WAIT_TIMEOUT).await; -} - -fn get_request_succeeds(url: &Url) -> bool { - let http_client = reqwest::blocking::ClientBuilder::new() - .timeout(BACKOFF_DELAY) - .build() - .expect("Could not build reqwest client."); - - http_client.get(url.clone()).send().is_ok() -} - -fn prepare_add_rules_proposal( - scope: FirewallRulesScope, - new_rules: Vec, - positions_sorted: Vec, - previous_rules: Vec, -) -> AddFirewallRulesPayload { - let mut all_rules = previous_rules; - for (rule, pos) in new_rules.iter().zip(positions_sorted.clone()) { - all_rules.insert(pos as usize, rule.clone()); - } - AddFirewallRulesPayload { - scope, - rules: new_rules, - positions: positions_sorted, - expected_hash: compute_firewall_ruleset_hash(&all_rules), - } -} - -fn prepare_remove_rules_proposal( - scope: FirewallRulesScope, - positions: Vec, - previous_rules: Vec, -) -> RemoveFirewallRulesPayload { - let mut all_rules = previous_rules; - let mut positions_sorted = positions.clone(); - positions_sorted.sort_unstable(); - positions_sorted.reverse(); - for pos in positions_sorted { - all_rules.remove(pos as usize); - } - RemoveFirewallRulesPayload { - scope, - positions, - expected_hash: compute_firewall_ruleset_hash(&all_rules), - } -} - -fn prepare_update_rules_proposal( - scope: FirewallRulesScope, - new_rules: Vec, - positions_sorted: Vec, - previous_rules: Vec, -) -> UpdateFirewallRulesPayload { - let mut all_rules = previous_rules; - for (rule, pos) in new_rules.iter().zip(positions_sorted.clone()) { - all_rules[pos as usize] = rule.clone(); - } - UpdateFirewallRulesPayload { - scope, - rules: new_rules, - positions: positions_sorted, - expected_hash: compute_firewall_ruleset_hash(&all_rules), - } -} - -fn await_rule_execution_with_backoff( - log: &slog::Logger, - test: &dyn Fn() -> bool, - initial_wait: Duration, - linear_backoff: Duration, - max_wait: Duration, -) -> bool { - let mut total_duration = initial_wait; - std::thread::sleep(initial_wait); - if test() { - info!( - log, - "(Waited {} seconds, succeeded)", - total_duration.as_secs() - ); - return true; - } - while total_duration < max_wait { - std::thread::sleep(linear_backoff); - total_duration += linear_backoff; - if test() { - info!( - log, - "(Waited {} seconds, succeeded)", - total_duration.as_secs() - ); - return true; - } - } - info!(log, "(Waited {} seconds, failed)", total_duration.as_secs()); - false -} diff --git a/rs/tests/src/networking/mod.rs b/rs/tests/src/networking/mod.rs deleted file mode 100644 index 923e54e7ca2..00000000000 --- a/rs/tests/src/networking/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod firewall_max_connections; -pub mod firewall_priority; -pub mod network_large; -pub mod network_reliability; -pub mod p2p_performance_workload; -pub mod replica_query_workload; -pub mod subnet_update_workload; diff --git a/rs/tests/src/networking/network_large.rs b/rs/tests/src/networking/network_large.rs deleted file mode 100644 index db06c188c7a..00000000000 --- a/rs/tests/src/networking/network_large.rs +++ /dev/null @@ -1,188 +0,0 @@ -/* tag::catalog[] -Title:: Subnet makes progress despite one third of the nodes being stressed. - -Runbook:: -0. Instantiate an IC with one System subnet larger than the current production NNS. -1. Install NNS canisters on the System subnet. -2. Build and install canister that stores msgs. -3. Let subnet run idle for a few minutes and confirm that it is up and running by storing message. -4. Stop f nodes and confirm subnet still is available. -5. Stop f+1 nodes and confirm that subnet is not making progress. -6. Restart one node such that we have f faulty nodes again and confirm subnet is available again. -7. Let subnet run idle with f faulty nodes and confirm that everything works. - -end::catalog[] */ - -use std::time::Duration; - -use ic_registry_subnet_type::SubnetType; -use ic_system_test_driver::driver::ic::{ - AmountOfMemoryKiB, InternetComputer, NrOfVCPUs, Subnet, VmResources, -}; -use ic_system_test_driver::driver::prometheus_vm::{HasPrometheus, PrometheusVm}; -use ic_system_test_driver::driver::test_env::TestEnv; -use ic_system_test_driver::driver::test_env_api::{ - HasPublicApiUrl, HasTopologySnapshot, HasVm, IcNodeContainer, NnsInstallationBuilder, -}; -use ic_system_test_driver::util::{assert_create_agent, block_on, MessageCanister}; -use ic_types::Height; -use slog::info; - -const UPDATE_MSG_1: &str = "This beautiful prose should be persisted for future generations"; -const UPDATE_MSG_2: &str = "I just woke up"; -const UPDATE_MSG_3: &str = "And this beautiful prose should be persisted for future generations"; -const UPDATE_MSG_4: &str = "However this prose will NOT be persisted for future generations"; -const UPDATE_MSG_5: &str = "This will be persisted again!"; -const UPDATE_MSG_6: &str = "Fell asleep again!"; - -const FAULTY: usize = 16; -const NODES: usize = 3 * FAULTY + 1; // 49 - -const IDLE_DURATION: Duration = Duration::from_secs(10 * 60); - -pub fn setup(env: TestEnv) { - let vm_resources = VmResources { - vcpus: Some(NrOfVCPUs::new(8)), - memory_kibibytes: Some(AmountOfMemoryKiB::new(4195000)), // 4GiB - boot_image_minimal_size_gibibytes: None, - }; - PrometheusVm::default() - .start(&env) - .expect("failed to start prometheus VM"); - InternetComputer::new() - .add_subnet( - Subnet::new(SubnetType::System) - .with_default_vm_resources(vm_resources) - // Use low DKG interval to confirm system works across interval boundaries. - .with_dkg_interval_length(Height::from(99)) - .add_nodes(NODES), - ) - .add_subnet( - Subnet::new(SubnetType::Application) - .with_default_vm_resources(vm_resources) - .with_dkg_interval_length(Height::from(49)) - .add_nodes(1), - ) - .setup_and_start(&env) - .expect("Failed to setup IC under test."); - env.sync_with_prometheus(); -} - -pub fn test(env: TestEnv) { - let log = env.logger(); - info!( - &log, - "Step 0: Checking readiness of all nodes after the IC setup ..." - ); - env.topology_snapshot().subnets().for_each(|subnet| { - subnet - .nodes() - .for_each(|node| node.await_status_is_healthy().unwrap()) - }); - info!(&log, "All nodes are ready, IC setup succeeded."); - - info!( - &log, - "Step 1: Installing NNS canisters on the System subnet ..." - ); - let nns_node = env - .topology_snapshot() - .root_subnet() - .nodes() - .next() - .unwrap(); - NnsInstallationBuilder::new() - .install(&nns_node, &env) - .expect("Could not install NNS canisters."); - - info!( - &log, - "Step 2: Build and install one counter canisters on each subnet. ..." - ); - let subnet = env.topology_snapshot().subnets().next().unwrap(); - let node = subnet.nodes().last().unwrap(); - let agent = block_on(assert_create_agent(node.get_public_url().as_str())); - let message_canister = block_on(MessageCanister::new(&agent, node.effective_canister_id())); - info!(&log, "Installation of counter canisters has succeeded."); - - info!( - log, - "Step 3: Assert that update call to the canister succeeds" - ); - block_on(message_canister.try_store_msg(UPDATE_MSG_1)).expect("Update canister call failed."); - assert_eq!( - block_on(message_canister.try_read_msg()), - Ok(Some(UPDATE_MSG_1.to_string())) - ); - - info!(log, "Step 4: Run idle for a few min"); - block_on(async { tokio::time::sleep(IDLE_DURATION).await }); - block_on(message_canister.try_store_msg(UPDATE_MSG_2)).expect("Update canister call failed."); - assert_eq!( - block_on(message_canister.try_read_msg()), - Ok(Some(UPDATE_MSG_2.to_string())) - ); - - info!(log, "Step 5: Kill {} nodes", FAULTY); - let nodes: Vec<_> = subnet.nodes().collect(); - for node in nodes.iter().take(FAULTY) { - node.vm().kill(); - } - for node in nodes.iter().take(FAULTY) { - node.await_status_is_unavailable() - .expect("Node still healthy"); - } - - info!( - log, - "Step 6: Assert that update call succeeds in presence of {} faulty nodes", FAULTY - ); - block_on(message_canister.try_store_msg(UPDATE_MSG_3)).expect("Update canister call failed."); - assert_eq!( - block_on(message_canister.try_read_msg()), - Ok(Some(UPDATE_MSG_3.to_string())) - ); - - info!( - log, - "Step 7: Kill an additonal node causing consensus to stop due to {} (f+1) faulty nodes", - FAULTY + 1 - ); - nodes[FAULTY].vm().kill(); - nodes[FAULTY] - .await_status_is_unavailable() - .expect("Node still healthy"); - - // Verify that it is not possible to write message - if let Ok(Ok(result)) = block_on(async { - tokio::time::timeout( - std::time::Duration::from_secs(30), - message_canister.try_store_msg(UPDATE_MSG_4), - ) - .await - }) { - panic!("expected the update to fail, got {:?}", result); - }; - - info!(log, "Step 8: Restart one node again",); - nodes[FAULTY].vm().start(); - for n in nodes.iter().skip(FAULTY) { - n.await_status_is_healthy().unwrap(); - } - block_on(message_canister.try_store_msg(UPDATE_MSG_5)).expect("Update canister call failed."); - assert_eq!( - block_on(message_canister.try_read_msg()), - Ok(Some(UPDATE_MSG_5.to_string())) - ); - - info!( - log, - "Step 9: Run idle for a few min on faulty node boundary" - ); - block_on(async { tokio::time::sleep(IDLE_DURATION).await }); - block_on(message_canister.try_store_msg(UPDATE_MSG_6)).expect("Update canister call failed."); - assert_eq!( - block_on(message_canister.try_read_msg()), - Ok(Some(UPDATE_MSG_6.to_string())) - ); -} diff --git a/rs/tests/src/networking/network_reliability.rs b/rs/tests/src/networking/network_reliability.rs deleted file mode 100644 index 0a199f3d0a3..00000000000 --- a/rs/tests/src/networking/network_reliability.rs +++ /dev/null @@ -1,396 +0,0 @@ -/* tag::catalog[] -Title:: Subnet makes progress despite one third of the nodes being stressed. - -Runbook:: -0. Instantiate an IC with one System and one Application subnet. -1. Install NNS canisters on the System subnet. -2. Build and install one counter canister on each subnet. -3. Instantiate and start workload for the APP subnet using a subset of 1/3 of the nodes as targets. - Workload send update[canister_id, "write"] requests. - Requests are equally distributed between this subset of 1/3 nodes. -4. Stress (modify tc settings on) another disjoint subset of 1/3 of the nodes (during the workload execution). - Stressing manifests in introducing randomness in: latency, bandwidth, packet drops percentage, stress duration. -5. Collect metrics from the workload and assert: - - Ratio of requests with duration below DURATION_THRESHOLD should exceed MIN_REQUESTS_RATIO_BELOW_THRESHOLD. -6. Perform assertions for both counter canisters (via query `read` call) - - Counter value on the canisters should exceed the threshold = (1 - max_failures_ratio) * total_requests_count. - -end::catalog[] */ - -use ic_base_types::NodeId; -use ic_registry_subnet_type::SubnetType; -use ic_system_test_driver::canister_api::{CallMode, GenericRequest}; -use ic_system_test_driver::driver::constants::DEVICE_NAME; -use ic_system_test_driver::driver::ic::{ - AmountOfMemoryKiB, InternetComputer, NrOfVCPUs, Subnet, VmResources, -}; -use ic_system_test_driver::driver::test_env::TestEnv; -use ic_system_test_driver::driver::test_env_api::{ - HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer, IcNodeSnapshot, NnsInstallationBuilder, - SshSession, -}; -use ic_system_test_driver::util::{ - self, agent_observes_canister_module, assert_canister_counter_with_retries, block_on, - spawn_round_robin_workload_engine, -}; -use rand::distributions::{Distribution, Uniform}; -use rand_chacha::ChaCha8Rng; -use slog::{debug, info, Logger}; -use std::cmp::max; -use std::io::{self}; -use std::thread::{self, JoinHandle}; -use std::time::{Duration, Instant}; - -const COUNTER_CANISTER_WAT: &str = "rs/tests/src/counter.wat"; -const CANISTER_METHOD: &str = "write"; -// Seed for random generator -const RND_SEED: u64 = 42; -// Size of the payload sent to the counter canister in update("write") call. -const PAYLOAD_SIZE_BYTES: usize = 1024; -// Duration of each request is placed into one of two categories - below or above this threshold. -const DURATION_THRESHOLD: Duration = Duration::from_secs(20); -// Parameters related to nodes stressing. -const BANDWIDTH_MIN: u32 = 10; -const BANDWIDTH_MAX: u32 = 100; -const LATENCY_MIN: Duration = Duration::from_millis(10); -const LATENCY_MAX: Duration = Duration::from_millis(990); -const DROPS_PERC_MIN: u32 = 1; -const DROPS_PERC_MAX: u32 = 99; -const MIN_NODE_STRESS_TIME: Duration = Duration::from_secs(10); -const MIN_NODE_UNSTRESSED_TIME: Duration = Duration::ZERO; -const FRACTION_FROM_REMAINING_DURATION: f64 = 0.25; -// Parameters related to reading/asserting counter values of the canisters. -const MAX_CANISTER_READ_RETRIES: u32 = 4; -const CANISTER_READ_RETRY_WAIT: Duration = Duration::from_secs(10); -// Parameters related to workload creation. -const REQUESTS_DISPATCH_EXTRA_TIMEOUT: Duration = Duration::from_secs(1); // This param can be slightly tweaked (1-2 sec), if the workload fails to dispatch requests precisely on time. - -// Test can be run with different setup/configuration parameters. -// This config holds these parameters. -#[derive(Copy, Clone, Debug)] -pub struct Config { - pub nodes_system_subnet: usize, - pub nodes_app_subnet: usize, - pub runtime: Duration, - pub rps: usize, -} - -pub fn setup(env: TestEnv, config: Config) { - let vm_resources = VmResources { - vcpus: Some(NrOfVCPUs::new(8)), - memory_kibibytes: Some(AmountOfMemoryKiB::new(50331648)), // 48GiB - boot_image_minimal_size_gibibytes: None, - }; - InternetComputer::new() - .add_subnet( - Subnet::new(SubnetType::System) - .with_default_vm_resources(vm_resources) - .add_nodes(config.nodes_system_subnet), - ) - .add_subnet( - Subnet::new(SubnetType::Application) - .with_default_vm_resources(vm_resources) - .add_nodes(config.nodes_app_subnet), - ) - .setup_and_start(&env) - .expect("Failed to setup IC under test."); -} - -pub fn test(env: TestEnv, config: Config) { - let log = env.logger(); - info!( - &log, - "Step 0: Checking readiness of all nodes after the IC setup ..." - ); - env.topology_snapshot().subnets().for_each(|subnet| { - subnet - .nodes() - .for_each(|node| node.await_status_is_healthy().unwrap()) - }); - info!(&log, "All nodes are ready, IC setup succeeded."); - info!( - &log, - "Step 1: Installing NNS canisters on the System subnet ..." - ); - let nns_node = env - .topology_snapshot() - .root_subnet() - .nodes() - .next() - .unwrap(); - NnsInstallationBuilder::new() - .install(&nns_node, &env) - .expect("Could not install NNS canisters."); - info!( - &log, - "Step 2: Build and install one counter canisters on each subnet. ..." - ); - let subnet_app = env - .topology_snapshot() - .subnets() - .find(|s| s.subnet_type() == SubnetType::Application) - .unwrap(); - let canister_app = subnet_app - .nodes() - .next() - .unwrap() - .create_and_install_canister_with_arg(COUNTER_CANISTER_WAT, None); - info!(&log, "Installation of counter canisters has succeeded."); - info!(&log, "Step 3: Instantiate and start one workload per subnet using a subset of 1/3 of the nodes as targets."); - let workload_app_nodes_count = config.nodes_app_subnet / 3; - info!( - &log, - "Launching two workloads for both subnets in separate threads against {} node/s.", - workload_app_nodes_count - ); - let agents_app: Vec<_> = subnet_app - .nodes() - .take(workload_app_nodes_count) - .map(|node| { - debug!( - &log, - "Node with id={} from APP will be used for the workload.", node.node_id - ); - node.with_default_agent(|agent| async move { agent }) - }) - .collect(); - assert!( - agents_app.len() == workload_app_nodes_count, - "Number of nodes and agents do not match." - ); - info!( - &log, - "Asserting all agents observe the installed canister ..." - ); - block_on(async { - for agent in agents_app.iter() { - assert!( - agent_observes_canister_module(agent, &canister_app).await, - "Canister module not available" - ); - } - }); - info!(&log, "All agents observe the installed canister module."); - // Spawn two workloads in separate threads, as we will need to have execution context to stress nodes. - let payload: Vec = vec![0; PAYLOAD_SIZE_BYTES]; - let start_time = Instant::now(); - let stop_time = start_time + config.runtime; - let handle_workload_app = { - let requests = vec![GenericRequest::new( - canister_app, - CANISTER_METHOD.to_string(), - payload.clone(), - CallMode::Update, - )]; - spawn_round_robin_workload_engine( - log.clone(), - requests, - agents_app, - config.rps, - config.runtime, - REQUESTS_DISPATCH_EXTRA_TIMEOUT, - vec![DURATION_THRESHOLD], - ) - }; - info!( - &log, - "Step 4: Stress another disjoint subset of 1/3 of the nodes (during the workload execution)." - ); - let stress_app_nodes_count = config.nodes_app_subnet / 3; - assert!( - stress_app_nodes_count > 0, - "At least one node needs to be stressed on each subnet." - ); - // We stress (modify node's traffic) using random parameters. - let rng: ChaCha8Rng = rand::SeedableRng::seed_from_u64(RND_SEED); - // Stress function for each node is executed in a separate thread. - let stress_app_handles: Vec<_> = subnet_app - .nodes() - .skip(workload_app_nodes_count) - .take(stress_app_nodes_count) - .map(|node| stress_node_periodically(log.clone(), rng.clone(), node, stop_time)) - .collect(); - - for h in stress_app_handles { - let stress_info = h - .join() - .expect("Thread execution failed.") - .unwrap_or_else(|err| { - panic!("Node stressing failed err={}", err); - }); - info!(&log, "{:?}", stress_info); - } - info!( - &log, - "Step 5: Collect metrics from both workloads and perform assertions ..." - ); - let load_metrics_app = handle_workload_app - .join() - .expect("Workload execution against APP subnet failed."); - info!( - &log, - "Workload execution results for APP: {load_metrics_app}" - ); - let requests_count_below_threshold_app = - load_metrics_app.requests_count_below_threshold(DURATION_THRESHOLD); - let min_expected_success_count = config.rps * config.runtime.as_secs() as usize; - assert_eq!(load_metrics_app.failure_calls(), 0); - assert!(requests_count_below_threshold_app - .iter() - .all(|(_, count)| *count as usize == min_expected_success_count)); - let agent_app = subnet_app - .nodes() - .next() - .map(|node| node.with_default_agent(|agent| async move { agent })) - .unwrap(); - info!( - &log, - "Step 6: Assert min counter value on both canisters has been reached ... " - ); - block_on(async { - assert_canister_counter_with_retries( - &log, - &agent_app, - &canister_app, - payload.clone(), - min_expected_success_count, - MAX_CANISTER_READ_RETRIES, - CANISTER_READ_RETRY_WAIT, - ) - .await; - }); -} - -#[derive(Debug)] -struct NodeStressInfo { - _node_id: NodeId, - stressed_times: u32, - stressed_mode_duration: Duration, - normal_mode_duration: Duration, -} - -fn stress_node_periodically( - log: Logger, - mut rng: ChaCha8Rng, - node: IcNodeSnapshot, - stop_time: Instant, -) -> JoinHandle> { - thread::spawn(move || { - let mut stress_info = NodeStressInfo { - _node_id: node.node_id, - stressed_times: 0, - normal_mode_duration: Duration::default(), - stressed_mode_duration: Duration::default(), - }; - - let should_stop = |remaining: Duration| { - if remaining.is_zero() { - info!(&log, "Stressing node with id={} is finished.", node.node_id); - true - } else { - false - } - }; - - // Session is an expensive resource, so we create it once per node. - let session = node - .block_on_ssh_session() - .expect("Failed to ssh into node"); - - loop { - // First keep the node in unstressed mode. - let remaining_duration = stop_time.saturating_duration_since(Instant::now()); - if should_stop(remaining_duration) { - break; - } else { - let _ = node - .block_on_bash_script_from_session(&session, &reset_tc_ssh_command()) - .expect("Failed to execute bash script from session"); - let max_duration = - fraction_of_duration(remaining_duration, FRACTION_FROM_REMAINING_DURATION); - let sleep_time = Uniform::from( - MIN_NODE_UNSTRESSED_TIME..=max(max_duration, MIN_NODE_UNSTRESSED_TIME), - ) - .sample(&mut rng); - info!( - &log, - "Node with id={} is in normal (unstressed) mode for {} sec.", - node.node_id, - sleep_time.as_secs() - ); - thread::sleep(sleep_time); - stress_info.normal_mode_duration = - stress_info.normal_mode_duration.saturating_add(sleep_time); - } - // Now stress the node modifying its traffic parameters. - let remaining_duration = stop_time.saturating_duration_since(Instant::now()); - if should_stop(remaining_duration) { - break; - } else { - let max_duration = - fraction_of_duration(remaining_duration, FRACTION_FROM_REMAINING_DURATION); - let action_time = - Uniform::from(MIN_NODE_STRESS_TIME..=max(max_duration, MIN_NODE_STRESS_TIME)) - .sample(&mut rng); - let tc_rules = node - .block_on_bash_script_from_session( - &session, - &limit_tc_randomly_ssh_command(&mut rng), - ) - .expect("Failed to execute bash script from session"); - info!( - &log, - "Node with id={} is stressed for {} sec. The applied tc rules are:\n{}", - node.node_id, - action_time.as_secs(), - tc_rules.as_str() - ); - thread::sleep(action_time); - stress_info.stressed_mode_duration = stress_info - .stressed_mode_duration - .saturating_add(action_time); - stress_info.stressed_times += 1; - } - } - Ok(stress_info) - }) -} - -fn fraction_of_duration(time: Duration, fraction: f64) -> Duration { - Duration::from_secs((time.as_secs() as f64 * fraction) as u64) -} - -fn reset_tc_ssh_command() -> String { - format!( - r#"set -euo pipefail - sudo tc qdisc del dev {device} root 2> /dev/null || true - "#, - device = DEVICE_NAME - ) -} - -fn limit_tc_randomly_ssh_command(mut rng: &mut ChaCha8Rng) -> String { - let bandwidth_dist = Uniform::from(BANDWIDTH_MIN..=BANDWIDTH_MAX); - let latency_dist = Uniform::from(LATENCY_MIN..=LATENCY_MAX); - let drops_perc_dist = Uniform::from(DROPS_PERC_MIN..=DROPS_PERC_MAX); - let cfg = util::get_config(); - let p2p_listen_port = cfg.transport.unwrap().listening_port; - // The second command deletes existing tc rules (if present). - // The last command reads the active tc rules. - format!( - r#"set -euo pipefail -sudo tc qdisc del dev {device} root 2> /dev/null || true -sudo tc qdisc add dev {device} root handle 1: prio -sudo tc qdisc add dev {device} parent 1:3 handle 10: tbf rate {bandwidth_mbit}mbit latency 400ms burst 100000 -sudo tc qdisc add dev {device} parent 10:1 handle 20: netem delay {latency_ms}ms 5ms drop {drops_percentage}% -sudo tc qdisc add dev {device} parent 20:1 handle 30: sfq -sudo tc filter add dev {device} protocol ipv6 parent 1:0 prio 3 u32 match ip6 dport {p2p_listen_port} 0xFFFF flowid 1:3 -sudo tc qdisc show dev {device} -"#, - device = DEVICE_NAME, - bandwidth_mbit = bandwidth_dist.sample(&mut rng), - latency_ms = latency_dist.sample(&mut rng).as_millis(), - drops_percentage = drops_perc_dist.sample(&mut rng), - p2p_listen_port = p2p_listen_port - ) -} diff --git a/rs/tests/src/networking/replica_query_workload.rs b/rs/tests/src/networking/replica_query_workload.rs deleted file mode 100644 index 459782ff005..00000000000 --- a/rs/tests/src/networking/replica_query_workload.rs +++ /dev/null @@ -1,125 +0,0 @@ -/* tag::catalog[] - -Title:: Single replica handles query workloads. - -Goal:: Ensure IC responds to queries of a given size in a timely manner. - -Runbook:: -0. Instantiate an IC with one System and one Application subnet. - - Optionally install one boundary node. -1. Install NNS canisters on the System subnet. -2. Build and install one counter canister on the Application subnet. -3. Instantiate and start a workload against the Application subnet. - Workload sends query[canister_id, "read"] requests. - All requests are sent to the same node/replica. -4. Collect metrics from the workload and assert: - - Ratio of requests with duration below DURATION_THRESHOLD should exceed MIN_REQUESTS_RATIO_BELOW_THRESHOLD. - - Ratio of successful requests should exceed min_success_ratio threshold. - -end::catalog[] */ - -use ic_registry_subnet_type::SubnetType; -use ic_system_test_driver::canister_api::{CallMode, GenericRequest}; -use ic_system_test_driver::driver::test_env::TestEnv; -use ic_system_test_driver::driver::test_env_api::{ - HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer, -}; -use ic_system_test_driver::util::spawn_round_robin_workload_engine; - -use slog::{debug, info, Logger}; - -use std::process::Command; -use std::time::Duration; - -const COUNTER_CANISTER_WAT: &str = "rs/tests/src/counter.wat"; -const CANISTER_METHOD: &str = "read"; -// Size of the payload sent to the counter canister in query("write") call. -const PAYLOAD_SIZE_BYTES: usize = 1024; -// Duration of each request is placed into one of two categories - below or above this threshold. -const DURATION_THRESHOLD: Duration = Duration::from_secs(2); -// Parameters related to workload creation. -const REQUESTS_DISPATCH_EXTRA_TIMEOUT: Duration = Duration::from_secs(2); // This param can be slightly tweaked (1-2 sec), if the workload fails to dispatch requests precisely on time. - -pub fn log_max_open_files(log: &Logger) { - let output = Command::new("sh") - .arg("-c") - .arg("ulimit -n") - .output() - .unwrap(); - let output = String::from_utf8_lossy(&output.stdout).replace('\n', ""); - info!(&log, "ulimit -n: {}", output); -} - -// Run a test with configurable number of query requests per second, -// duration of the test, and the required success ratio. -pub fn test(env: TestEnv, rps: usize, runtime: Duration) { - let log = env.logger(); - log_max_open_files(&log); - info!( - &log, - "Checking readiness of all nodes after the IC setup ..." - ); - let topology_snapshot = env.topology_snapshot(); - topology_snapshot.subnets().for_each(|subnet| { - subnet - .nodes() - .for_each(|node| node.await_status_is_healthy().unwrap()) - }); - info!(&log, "All nodes are ready, IC setup succeeded."); - info!( - &log, - "Step 2: Build and install one counter canister on the Application subnet..." - ); - let app_subnet = topology_snapshot - .subnets() - .find(|s| s.subnet_type() == SubnetType::Application) - .unwrap(); - // Take the first node in the Application subnet. - let app_node = app_subnet.nodes().next().unwrap(); - debug!( - &log, - "Node with id={} from the Application subnet will be used as a target for the workload.", - app_node.node_id - ); - let app_canister = app_node.create_and_install_canister_with_arg(COUNTER_CANISTER_WAT, None); - info!(&log, "Installation of counter canister has succeeded."); - info!(&log, "Step 3: Instantiate and start a workload using one node of the Application subnet as target."); - // Workload sends messages to canister via node agents. - // As we talk to a single node, we create one agent, accordingly. - let app_agent = app_node.with_default_agent(|agent| async move { agent }); - // Spawn a workload against counter canister. - let handle_workload = { - let requests = vec![GenericRequest::new( - app_canister, - CANISTER_METHOD.to_string(), - vec![0; PAYLOAD_SIZE_BYTES], - CallMode::Query, - )]; - spawn_round_robin_workload_engine( - log.clone(), - requests, - vec![app_agent], - rps, - runtime, - REQUESTS_DISPATCH_EXTRA_TIMEOUT, - vec![DURATION_THRESHOLD], - ) - }; - let load_metrics = handle_workload.join().expect("Workload execution failed."); - info!( - &log, - "Step 4: Collect metrics from the workload and perform assertions ..." - ); - let requests_count_below_threshold = - load_metrics.requests_count_below_threshold(DURATION_THRESHOLD); - info!(log, "Workload execution results: {load_metrics}"); - assert_eq!( - load_metrics.failure_calls(), - 0, - "Too many requests have failed." - ); - let min_expected_counter = rps as u64 * runtime.as_secs(); - assert!(requests_count_below_threshold - .iter() - .all(|(_, count)| *count == min_expected_counter)); -} From f3aacc703cda7863729483f119a1af7e47044418 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Thu, 24 Oct 2024 15:31:55 +0200 Subject: [PATCH 08/22] test(IDX): parallelise the spec_compliance setup functions (#2236) This shaves of about 30 seconds from the `//rs/tests/testing_verification:spec_compliance_...` system-tests by deploying the Boundary Node, httpbin UVM and IC concurrently instead of in sequence. spec_compliance test run: https://github.com/dfinity/ic/actions/runs/11498495840. --- .../spec_compliance/spec_compliance.rs | 62 +++++++++++++------ ...spec_compliance_application_subnet_test.rs | 8 +-- ...liance_group_01_application_subnet_test.rs | 8 +-- ..._compliance_group_01_system_subnet_test.rs | 8 +-- ..._compliance_group_02_system_subnet_test.rs | 8 +-- .../spec_compliance_system_subnet_test.rs | 8 +-- 6 files changed, 64 insertions(+), 38 deletions(-) diff --git a/rs/tests/testing_verification/spec_compliance/spec_compliance.rs b/rs/tests/testing_verification/spec_compliance/spec_compliance.rs index 4f1f653cf40..7ae62f2a8a4 100644 --- a/rs/tests/testing_verification/spec_compliance/spec_compliance.rs +++ b/rs/tests/testing_verification/spec_compliance/spec_compliance.rs @@ -2,7 +2,9 @@ use canister_http::get_universal_vm_address; use ic_registry_routing_table::canister_id_into_u64; use ic_registry_subnet_features::SubnetFeatures; use ic_registry_subnet_type::SubnetType; -use ic_system_test_driver::driver::boundary_node::{BoundaryNode, BoundaryNodeVm}; +use ic_system_test_driver::driver::boundary_node::{ + BoundaryNode, BoundaryNodeVm, BoundaryNodeWithVm, +}; use ic_system_test_driver::driver::ic::{InternetComputer, NrOfVCPUs, Subnet, VmResources}; use ic_system_test_driver::driver::test_env::TestEnv; use ic_system_test_driver::driver::test_env_api::{ @@ -14,6 +16,7 @@ use ic_types::SubnetId; use slog::{info, Logger}; use std::path::PathBuf; use std::process::{Command, Stdio}; +use std::thread::{spawn, JoinHandle}; pub const UNIVERSAL_VM_NAME: &str = "httpbin"; @@ -32,11 +35,46 @@ const EXCLUDED: &[&str] = &[ "$0 ~ /Call from query method traps (in query call)/", ]; -pub fn config_impl(env: TestEnv, deploy_bn_and_nns_canisters: bool, http_requests: bool) { +pub fn setup_impl(env: TestEnv, deploy_bn_and_nns_canisters: bool, http_requests: bool) { use ic_system_test_driver::driver::test_env_api::secs; use ic_system_test_driver::util::block_on; use std::env; + // If requested, deploy a Boundary Node concurrently with deploying the rest of the testnet: + let mut deploy_bn_thread: Option> = None; + let cloned_env = env.clone(); + if deploy_bn_and_nns_canisters { + deploy_bn_thread = Some(spawn(move || { + BoundaryNode::new(String::from(BOUNDARY_NODE_NAME)) + .allocate_vm(&cloned_env) + .expect("Allocation of BoundaryNode failed.") + })); + } + + // If requested, deploy the httpbin UVM concurrently with deploying the rest of the testnet: + let mut deploy_httpbin_uvm_thread: Option> = None; + let cloned_env = env.clone(); + if http_requests { + deploy_httpbin_uvm_thread = Some(spawn(move || { + env::set_var( + "SSL_CERT_FILE", + get_dependency_path( + "ic-os/components/networking/dev-certs/canister_http_test_ca.cert", + ), + ); + env::remove_var("NIX_SSL_CERT_FILE"); + + // Set up Universal VM for httpbin testing service + UniversalVm::new(String::from(UNIVERSAL_VM_NAME)) + .with_config_img(get_dependency_path( + "rs/tests/networking/canister_http/http_uvm_config_image.zst", + )) + .start(&cloned_env) + .expect("failed to set up universal VM"); + canister_http::start_httpbin_on_uvm(&cloned_env); + })) + } + let vm_resources = VmResources { vcpus: Some(NrOfVCPUs::new(16)), memory_kibibytes: None, @@ -74,9 +112,9 @@ pub fn config_impl(env: TestEnv, deploy_bn_and_nns_canisters: bool, http_request .install(&nns_node, &env) .expect("NNS canisters not installed"); info!(env.logger(), "NNS canisters are installed."); - BoundaryNode::new(String::from(BOUNDARY_NODE_NAME)) - .allocate_vm(&env) - .expect("Allocation of BoundaryNode failed.") + + let allocated_bn = deploy_bn_thread.unwrap().join().unwrap(); + allocated_bn .for_ic(&env, "") .use_real_certs_and_dns() .start(&env) @@ -89,20 +127,8 @@ pub fn config_impl(env: TestEnv, deploy_bn_and_nns_canisters: bool, http_request }); if http_requests { - env::set_var( - "SSL_CERT_FILE", - get_dependency_path("ic-os/components/networking/dev-certs/canister_http_test_ca.cert"), - ); - env::remove_var("NIX_SSL_CERT_FILE"); + deploy_httpbin_uvm_thread.unwrap().join().unwrap(); - // Set up Universal VM for httpbin testing service - UniversalVm::new(String::from(UNIVERSAL_VM_NAME)) - .with_config_img(get_dependency_path( - "rs/tests/networking/canister_http/http_uvm_config_image.zst", - )) - .start(&env) - .expect("failed to set up universal VM"); - canister_http::start_httpbin_on_uvm(&env); let log = env.logger(); ic_system_test_driver::retry_with_msg!( "check if httpbin is responding to requests", diff --git a/rs/tests/testing_verification/spec_compliance_application_subnet_test.rs b/rs/tests/testing_verification/spec_compliance_application_subnet_test.rs index be823517eb2..98faeb014a2 100644 --- a/rs/tests/testing_verification/spec_compliance_application_subnet_test.rs +++ b/rs/tests/testing_verification/spec_compliance_application_subnet_test.rs @@ -17,10 +17,10 @@ use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; use ic_system_test_driver::systest; -use spec_compliance::{config_impl, test_subnet}; +use spec_compliance::{setup_impl, test_subnet}; -pub fn config(env: TestEnv) { - config_impl(env, true, true); +pub fn setup(env: TestEnv) { + setup_impl(env, true, true); } pub fn test(env: TestEnv) { @@ -44,7 +44,7 @@ pub fn test(env: TestEnv) { fn main() -> Result<()> { SystemTestGroup::new() - .with_setup(config) + .with_setup(setup) .add_test(systest!(test)) .execute_from_args()?; diff --git a/rs/tests/testing_verification/spec_compliance_group_01_application_subnet_test.rs b/rs/tests/testing_verification/spec_compliance_group_01_application_subnet_test.rs index d4e4963abf2..3e3a83913af 100644 --- a/rs/tests/testing_verification/spec_compliance_group_01_application_subnet_test.rs +++ b/rs/tests/testing_verification/spec_compliance_group_01_application_subnet_test.rs @@ -17,10 +17,10 @@ use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; use ic_system_test_driver::systest; -use spec_compliance::{config_impl, test_subnet}; +use spec_compliance::{setup_impl, test_subnet}; -pub fn config(env: TestEnv) { - config_impl(env, true, true); +pub fn setup(env: TestEnv) { + setup_impl(env, true, true); } pub fn test(env: TestEnv) { @@ -43,7 +43,7 @@ pub fn test(env: TestEnv) { fn main() -> Result<()> { SystemTestGroup::new() - .with_setup(config) + .with_setup(setup) .add_test(systest!(test)) .execute_from_args()?; diff --git a/rs/tests/testing_verification/spec_compliance_group_01_system_subnet_test.rs b/rs/tests/testing_verification/spec_compliance_group_01_system_subnet_test.rs index fe9fe962f15..97d983ac233 100644 --- a/rs/tests/testing_verification/spec_compliance_group_01_system_subnet_test.rs +++ b/rs/tests/testing_verification/spec_compliance_group_01_system_subnet_test.rs @@ -17,10 +17,10 @@ use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; use ic_system_test_driver::systest; -use spec_compliance::{config_impl, test_subnet}; +use spec_compliance::{setup_impl, test_subnet}; -pub fn config(env: TestEnv) { - config_impl(env, true, true); +pub fn setup(env: TestEnv) { + setup_impl(env, true, true); } pub fn test(env: TestEnv) { @@ -43,7 +43,7 @@ pub fn test(env: TestEnv) { fn main() -> Result<()> { SystemTestGroup::new() - .with_setup(config) + .with_setup(setup) .add_test(systest!(test)) .execute_from_args()?; diff --git a/rs/tests/testing_verification/spec_compliance_group_02_system_subnet_test.rs b/rs/tests/testing_verification/spec_compliance_group_02_system_subnet_test.rs index ccb105bfcb5..4372ae5a7c2 100644 --- a/rs/tests/testing_verification/spec_compliance_group_02_system_subnet_test.rs +++ b/rs/tests/testing_verification/spec_compliance_group_02_system_subnet_test.rs @@ -17,10 +17,10 @@ use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; use ic_system_test_driver::systest; -use spec_compliance::{config_impl, test_subnet}; +use spec_compliance::{setup_impl, test_subnet}; -pub fn config(env: TestEnv) { - config_impl(env, false, false); +pub fn setup(env: TestEnv) { + setup_impl(env, false, false); } pub fn test(env: TestEnv) { @@ -37,7 +37,7 @@ pub fn test(env: TestEnv) { fn main() -> Result<()> { SystemTestGroup::new() - .with_setup(config) + .with_setup(setup) .add_test(systest!(test)) .execute_from_args()?; diff --git a/rs/tests/testing_verification/spec_compliance_system_subnet_test.rs b/rs/tests/testing_verification/spec_compliance_system_subnet_test.rs index 11954eaf9f8..a047bc4d1c7 100644 --- a/rs/tests/testing_verification/spec_compliance_system_subnet_test.rs +++ b/rs/tests/testing_verification/spec_compliance_system_subnet_test.rs @@ -17,10 +17,10 @@ use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; use ic_system_test_driver::systest; -use spec_compliance::{config_impl, test_subnet}; +use spec_compliance::{setup_impl, test_subnet}; -pub fn config(env: TestEnv) { - config_impl(env, true, true); +pub fn setup(env: TestEnv) { + setup_impl(env, true, true); } pub fn test(env: TestEnv) { @@ -44,7 +44,7 @@ pub fn test(env: TestEnv) { fn main() -> Result<()> { SystemTestGroup::new() - .with_setup(config) + .with_setup(setup) .add_test(systest!(test)) .execute_from_args()?; From 61a229ece56f98f13394b566aafa06b4e815dcc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Thu, 24 Oct 2024 15:32:45 +0200 Subject: [PATCH 09/22] test(ICP_ledger): FI-1506: Thread limit in flaky ICP ledger and index tests (#2208) Set a limit on the number of threads used to run a couple of flaky ICP ledger and index tests. --- rs/ledger_suite/icp/index/BUILD.bazel | 1 + rs/ledger_suite/icp/ledger/BUILD.bazel | 1 + 2 files changed, 2 insertions(+) diff --git a/rs/ledger_suite/icp/index/BUILD.bazel b/rs/ledger_suite/icp/index/BUILD.bazel index 1cbef0739ec..19df2bcc4c2 100644 --- a/rs/ledger_suite/icp/index/BUILD.bazel +++ b/rs/ledger_suite/icp/index/BUILD.bazel @@ -98,6 +98,7 @@ rust_ic_test( ], edition = "2018", env = { + "RUST_TEST_THREADS": "4", "CARGO_MANIFEST_DIR": "rs/ledger_suite/icp/index", "IC_ICP_INDEX_WASM_PATH": "$(rootpath :ic-icp-index-canister.wasm)", "LEDGER_CANISTER_NOTIFY_METHOD_WASM_PATH": "$(rootpath //rs/ledger_suite/icp/ledger:ledger-canister-wasm-notify-method)", diff --git a/rs/ledger_suite/icp/ledger/BUILD.bazel b/rs/ledger_suite/icp/ledger/BUILD.bazel index c528ed45940..39b00e524e5 100644 --- a/rs/ledger_suite/icp/ledger/BUILD.bazel +++ b/rs/ledger_suite/icp/ledger/BUILD.bazel @@ -100,6 +100,7 @@ rust_ic_test( "@mainnet_icp_ledger_canister//file", ], env = { + "RUST_TEST_THREADS": "4", "CARGO_MANIFEST_DIR": "rs/ledger_suite/icp/ledger", "ICP_LEDGER_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_icp_ledger_canister//file)", "LEDGER_CANISTER_WASM_PATH": "$(rootpath :ledger-canister-wasm)", From e18c77780dfaeb83b716c5ac74e2c4825196d9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Bj=C3=B6rkqvist?= Date: Thu, 24 Oct 2024 15:33:16 +0200 Subject: [PATCH 10/22] test(ICRC_ledger): FI-1456: Upgrade archive canisters in ICRC golden state tests (#2119) In addition to upgrading the ledger and index canisters, also upgrade the archive canisters in the ICRC golden state tests. --- WORKSPACE.bazel | 4 ++ mainnet-canisters.json | 10 +++- rs/ledger_suite/icrc1/BUILD.bazel | 8 +++ .../tests/golden_state_upgrade_downgrade.rs | 50 +++++++++++++++++-- rs/ledger_suite/tests/sm-tests/src/lib.rs | 2 +- 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 57c24210bc5..19b56b4de2f 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -23,8 +23,10 @@ canisters( "genesis-token": "genesis-token-canister.wasm.gz", "cycles-minting": "cycles-minting-canister.wasm.gz", "sns-wasm": "sns-wasm-canister.wasm.gz", + "ck_btc_archive": "ic-icrc1-archive.wasm.gz", "ck_btc_ledger": "ic-icrc1-ledger.wasm.gz", "ck_btc_index": "ic-icrc1-index-ng.wasm.gz", + "ck_eth_archive": "ic-icrc1-archive-u256.wasm.gz", "ck_eth_ledger": "ic-icrc1-ledger-u256.wasm.gz", "ck_eth_index": "ic-icrc1-index-ng-u256.wasm.gz", "sns_root": "sns-root-canister.wasm.gz", @@ -46,8 +48,10 @@ canisters( "genesis-token": "mainnet_nns_genesis-token-canister", "cycles-minting": "mainnet_nns_cycles-minting-canister", "sns-wasm": "mainnet_nns_sns-wasm-canister", + "ck_btc_archive": "mainnet_ckbtc_ic-icrc1-archive", "ck_btc_ledger": "mainnet_ckbtc_ic-icrc1-ledger", "ck_btc_index": "mainnet_ckbtc-index-ng", + "ck_eth_archive": "mainnet_cketh_ic-icrc1-archive-u256", "ck_eth_ledger": "mainnet_cketh_ic-icrc1-ledger-u256", "ck_eth_index": "mainnet_cketh-index-ng", "sns_root": "mainnet_sns-root-canister", diff --git a/mainnet-canisters.json b/mainnet-canisters.json index f8634d509ed..36534653f07 100644 --- a/mainnet-canisters.json +++ b/mainnet-canisters.json @@ -3,6 +3,10 @@ "rev": "b43280208c32633a29657a1051660324e88a373d", "sha256": "db0f094005a0e84e243f8f300236be879dcefa412c2fd36d675390caa689d88d" }, + "ck_btc_archive": { + "rev": "d4ee25b0865e89d3eaac13a60f0016d5e3296b31", + "sha256": "9476aa71bcee621aba93a3d7c115c543f42c543de840da3224c5f70a32dbfe4d" + }, "ck_btc_index": { "rev": "d4ee25b0865e89d3eaac13a60f0016d5e3296b31", "sha256": "612410c71e893bb64772ab8131d77264740398f3932d873cb4f640fc257f9e61" @@ -11,6 +15,10 @@ "rev": "d4ee25b0865e89d3eaac13a60f0016d5e3296b31", "sha256": "a170bfdce5d66e751a3cc03747cb0f06b450af500e75e15976ec08a3f5691f4c" }, + "ck_eth_archive": { + "rev": "d4ee25b0865e89d3eaac13a60f0016d5e3296b31", + "sha256": "e9c7cad647ede2ea2942572f337bd27d0839dd06c5e2c7f03591226acb10a9fb" + }, "ck_eth_index": { "rev": "d4ee25b0865e89d3eaac13a60f0016d5e3296b31", "sha256": "de250f08dc7e699144b73514f55fbbb3a3f8cd97abf0f7ae31d9fb7494f55234" @@ -79,4 +87,4 @@ "rev": "4bed17bfc82cddc5691743db6228992cdc2740f4", "sha256": "c265efedfb9268de8fa7647c04268a89339e4d02d670d146eb4564fa4a694a7f" } -} \ No newline at end of file +} diff --git a/rs/ledger_suite/icrc1/BUILD.bazel b/rs/ledger_suite/icrc1/BUILD.bazel index 2d8fc6c620f..c6cfacc03aa 100644 --- a/rs/ledger_suite/icrc1/BUILD.bazel +++ b/rs/ledger_suite/icrc1/BUILD.bazel @@ -118,23 +118,31 @@ rust_test( crate_features = features, crate_root = "tests/golden_state_upgrade_downgrade.rs", data = [ + "//rs/ledger_suite/icrc1/archive:archive_canister" + name_suffix + ".wasm.gz", "//rs/ledger_suite/icrc1/index-ng:index_ng_canister" + name_suffix + ".wasm.gz", "//rs/ledger_suite/icrc1/ledger:ledger_canister" + name_suffix + ".wasm", "@mainnet_ckbtc-index-ng//file", + "@mainnet_ckbtc_ic-icrc1-archive//file", "@mainnet_ckbtc_ic-icrc1-ledger//file", "@mainnet_cketh-index-ng//file", + "@mainnet_cketh_ic-icrc1-archive-u256//file", "@mainnet_cketh_ic-icrc1-ledger-u256//file", + "@mainnet_ic-icrc1-archive//file", "@mainnet_ic-icrc1-index-ng//file", "@mainnet_ic-icrc1-ledger//file", ], env = { "CARGO_MANIFEST_DIR": "rs/ledger_suite/icrc1", + "CKBTC_IC_ICRC1_ARCHIVE_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_ckbtc_ic-icrc1-archive//file)", "CKBTC_IC_ICRC1_INDEX_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_ckbtc-index-ng//file)", "CKBTC_IC_ICRC1_LEDGER_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_ckbtc_ic-icrc1-ledger//file)", + "CKETH_IC_ICRC1_ARCHIVE_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_cketh_ic-icrc1-archive-u256//file)", "CKETH_IC_ICRC1_INDEX_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_cketh-index-ng//file)", "CKETH_IC_ICRC1_LEDGER_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_cketh_ic-icrc1-ledger-u256//file)", + "IC_ICRC1_ARCHIVE_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_ic-icrc1-archive//file)", "IC_ICRC1_INDEX_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_ic-icrc1-index-ng//file)", "IC_ICRC1_LEDGER_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_ic-icrc1-ledger//file)", + "IC_ICRC1_ARCHIVE_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/archive:archive_canister" + name_suffix + ".wasm.gz)", "IC_ICRC1_INDEX_NG_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/index-ng:index_ng_canister" + name_suffix + ".wasm.gz)", "IC_ICRC1_LEDGER_WASM_PATH": "$(rootpath //rs/ledger_suite/icrc1/ledger:ledger_canister" + name_suffix + ".wasm)", }, diff --git a/rs/ledger_suite/icrc1/tests/golden_state_upgrade_downgrade.rs b/rs/ledger_suite/icrc1/tests/golden_state_upgrade_downgrade.rs index 59b001706a8..7a836379572 100644 --- a/rs/ledger_suite/icrc1/tests/golden_state_upgrade_downgrade.rs +++ b/rs/ledger_suite/icrc1/tests/golden_state_upgrade_downgrade.rs @@ -7,7 +7,8 @@ use ic_ledger_suite_state_machine_tests::in_memory_ledger::{ ApprovalKey, BurnsWithoutSpender, InMemoryLedger, }; use ic_ledger_suite_state_machine_tests::{ - generate_transactions, get_all_ledger_and_archive_blocks, TransactionGenerationParameters, + generate_transactions, get_all_ledger_and_archive_blocks, list_archives, + TransactionGenerationParameters, }; use ic_nns_test_utils_golden_nns_state::new_state_machine_with_golden_fiduciary_state_or_panic; use ic_state_machine_tests::StateMachine; @@ -38,6 +39,9 @@ lazy_static! { )), Wasm::from_bytes(load_wasm_using_env_var( "CKBTC_IC_ICRC1_LEDGER_DEPLOYED_VERSION_WASM_PATH", + )), + Wasm::from_bytes(load_wasm_using_env_var( + "CKBTC_IC_ICRC1_ARCHIVE_DEPLOYED_VERSION_WASM_PATH", )) ); pub static ref MAINNET_SNS_WASMS: Wasms = Wasms::new( @@ -46,11 +50,15 @@ lazy_static! { )), Wasm::from_bytes(load_wasm_using_env_var( "IC_ICRC1_LEDGER_DEPLOYED_VERSION_WASM_PATH", + )), + Wasm::from_bytes(load_wasm_using_env_var( + "IC_ICRC1_ARCHIVE_DEPLOYED_VERSION_WASM_PATH", )) ); pub static ref MASTER_WASMS: Wasms = Wasms::new( Wasm::from_bytes(index_ng_wasm()), - Wasm::from_bytes(ledger_wasm()) + Wasm::from_bytes(ledger_wasm()), + Wasm::from_bytes(archive_wasm()) ); } @@ -62,24 +70,30 @@ lazy_static! { )), Wasm::from_bytes(load_wasm_using_env_var( "CKETH_IC_ICRC1_LEDGER_DEPLOYED_VERSION_WASM_PATH", + )), + Wasm::from_bytes(load_wasm_using_env_var( + "CKETH_IC_ICRC1_ARCHIVE_DEPLOYED_VERSION_WASM_PATH", )) ); pub static ref MASTER_WASMS: Wasms = Wasms::new( Wasm::from_bytes(index_ng_wasm()), - Wasm::from_bytes(ledger_wasm()) + Wasm::from_bytes(ledger_wasm()), + Wasm::from_bytes(archive_wasm()) ); } pub struct Wasms { index_wasm: Wasm, ledger_wasm: Wasm, + archive_wasm: Wasm, } impl Wasms { - fn new(index_wasm: Wasm, ledger_wasm: Wasm) -> Self { + fn new(index_wasm: Wasm, ledger_wasm: Wasm, archive_wasm: Wasm) -> Self { Self { index_wasm, ledger_wasm, + archive_wasm, } } } @@ -164,6 +178,26 @@ impl LedgerSuiteConfig { } } + fn upgrade_archives(&self, state_machine: &StateMachine, wasm: &Wasm) { + let canister_id = + CanisterId::unchecked_from_principal(PrincipalId::from_str(self.ledger_id).unwrap()); + let archives = list_archives(state_machine, canister_id); + let num_archives = archives.len(); + for archive in archives { + let archive_canister_id = + CanisterId::unchecked_from_principal(PrincipalId(archive.canister_id)); + state_machine + .upgrade_canister(archive_canister_id, wasm.clone().bytes(), vec![]) + .unwrap_or_else(|e| { + panic!( + "should successfully upgrade archive '{}': {}", + archive_canister_id, e + ) + }); + } + println!("Upgraded {} archive(s)", num_archives); + } + fn upgrade_index(&self, state_machine: &StateMachine, wasm: &Wasm) { let canister_id = CanisterId::unchecked_from_principal(PrincipalId::from_str(self.index_id).unwrap()); @@ -198,6 +232,8 @@ impl LedgerSuiteConfig { self.upgrade_index(state_machine, &self.mainnet_wasms.index_wasm); self.upgrade_ledger(state_machine, &self.mainnet_wasms.ledger_wasm); self.upgrade_ledger(state_machine, &self.mainnet_wasms.ledger_wasm); + self.upgrade_archives(state_machine, &self.mainnet_wasms.archive_wasm); + self.upgrade_archives(state_machine, &self.mainnet_wasms.archive_wasm); } fn upgrade_to_master(&self, state_machine: &StateMachine) { @@ -206,6 +242,8 @@ impl LedgerSuiteConfig { self.upgrade_index(state_machine, &self.master_wasms.index_wasm); self.upgrade_ledger(state_machine, &self.master_wasms.ledger_wasm); self.upgrade_ledger(state_machine, &self.master_wasms.ledger_wasm); + self.upgrade_archives(state_machine, &self.master_wasms.archive_wasm); + self.upgrade_archives(state_machine, &self.master_wasms.archive_wasm); } } @@ -693,3 +731,7 @@ fn should_upgrade_icrc_sns_canisters_with_golden_state() { canister_config.perform_upgrade_downgrade_testing(&state_machine); } } + +fn archive_wasm() -> Vec { + load_wasm_using_env_var("IC_ICRC1_ARCHIVE_WASM_PATH") +} diff --git a/rs/ledger_suite/tests/sm-tests/src/lib.rs b/rs/ledger_suite/tests/sm-tests/src/lib.rs index 1ff4cbf6a85..5815923ac38 100644 --- a/rs/ledger_suite/tests/sm-tests/src/lib.rs +++ b/rs/ledger_suite/tests/sm-tests/src/lib.rs @@ -260,7 +260,7 @@ pub fn transfer( ) } -fn list_archives(env: &StateMachine, ledger: CanisterId) -> Vec { +pub fn list_archives(env: &StateMachine, ledger: CanisterId) -> Vec { Decode!( &env.query(ledger, "archives", Encode!().unwrap()) .expect("failed to query archives") From 9c09cffeffc2ae130e0d716994f3e564b9102880 Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Thu, 24 Oct 2024 15:45:40 +0200 Subject: [PATCH 11/22] fix(IDX): use official docker images (#2230) In the past, there were some official docker images (for `static-file-server` and `jaeger`) that we could not pull using `rules_oci`. The issue seems to have been fixed in Bazel 7. See: https://github.com/bazel-contrib/rules_oci/issues/695 --- MODULE.bazel | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 4831451c1b4..57c403884fa 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -128,11 +128,7 @@ oci = use_extension("@rules_oci//oci:extensions.bzl", "oci") # file server used in tests oci.pull( name = "static-file-server", - # $ docker pull halverneus/static-file-server - # $ docker tag halverneus/static-file-server dfinitydev/halverneus-static-file-server:latest - # $ docker push dfinitydev/halverneus-static-file-server:latest - #latest: digest: sha256:... - image = "docker.io/dfinitydev/halverneus-static-file-server@sha256:80eb204716e0928e27e378ed817056c1167b2b1a878b1ac4ce496964dd9a3ccd", + image = "docker.io/halverneus/static-file-server@sha256:9e46688910b1cf9328c3b55784f08a63c53e70a276ccaf76bfdaaf2fbd0019fa", platforms = [ "linux/amd64", ], @@ -150,16 +146,9 @@ oci.pull( use_repo(oci, "bitcoind", "bitcoind_linux_amd64") # Tracing image used in tests -# we can't use the official image: https://github.com/bazel-contrib/rules_oci/issues/695 -# -# Instead we copy the official image to our repository: -# $ docker pull jaegertracing/all-in-one -# $ docker tag jaegertracing/all-in-one dfinitydev/jaegertracing-all-in-one:latest -# $ docker push jaegertracing-all-in-one:latest -# > latest: digest: sha256:... oci.pull( name = "jaeger", - image = "docker.io/dfinitydev/jaegertracing-all-in-one@sha256:b85a6bbb949a62377010b8418d7a860c9d0ea7058d83e7cb5ade4fba046c4a76", + image = "docker.io/jaegertracing/all-in-one@sha256:836e9b69c88afbedf7683ea7162e179de63b1f981662e83f5ebb68badadc710f", platforms = [ "linux/amd64", ], From b29e83ac7c4ade6fe055afc56deaee707b6471ff Mon Sep 17 00:00:00 2001 From: Maksym Arutyunyan <103510076+maksymar@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:01:44 +0200 Subject: [PATCH 12/22] feat: enable allowed_viewers feature for canister log visibility (#2244) This PR enables `allowed_viewers` for canister log visibility. EXC-1697 --- rs/config/src/execution_environment.rs | 2 +- rs/execution_environment/tests/canister_logging.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/rs/config/src/execution_environment.rs b/rs/config/src/execution_environment.rs index c9e68687c4d..e55768e39bc 100644 --- a/rs/config/src/execution_environment.rs +++ b/rs/config/src/execution_environment.rs @@ -360,7 +360,7 @@ impl Default for Config { dirty_page_logging: FlagStatus::Disabled, max_canister_http_requests_in_flight: MAX_CANISTER_HTTP_REQUESTS_IN_FLIGHT, default_wasm_memory_limit: DEFAULT_WASM_MEMORY_LIMIT, - allowed_viewers_feature: FlagStatus::Disabled, + allowed_viewers_feature: FlagStatus::Enabled, } } } diff --git a/rs/execution_environment/tests/canister_logging.rs b/rs/execution_environment/tests/canister_logging.rs index 6e697e1d432..afd2bc2a9a7 100644 --- a/rs/execution_environment/tests/canister_logging.rs +++ b/rs/execution_environment/tests/canister_logging.rs @@ -280,8 +280,7 @@ fn test_log_visibility_of_fetch_canister_logs() { ( LogVisibilityV2::AllowedViewers(allowed_viewers.clone()), allowed_viewer, - // TODO(EXC-1675): when disabled works as for controllers, change to ok when enabled. - not_allowed_error(&allowed_viewer), + ok.clone(), ), ( LogVisibilityV2::AllowedViewers(allowed_viewers.clone()), From 993fc858670783db5977100137e343e60418d75d Mon Sep 17 00:00:00 2001 From: nabdullindfinity <135595192+nabdullindfinity@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:56:02 +0200 Subject: [PATCH 13/22] feat: initial draft of custom metric tool and its systemd timer (#1963) To support the performance monitoring on mainnet, add a tool where custom metrics can be calculated and exported to prometheus's `node_exporter` through the `textfile` collector. Currently, the total number of TLB shootdowns across all CPUs will be exposed as `sum_tlb_shootdowns`, collected once per minute, as the latest `node_exporter` does not allow filtering of data of its built-in `interrupts` collector that could otherwise do it for us (until https://github.com/prometheus/node_exporter/pull/3028 is included in the release branches) and will add many metrics with high cardinality otherwise. NODE-1445 --- Cargo.lock | 8 + Cargo.toml | 1 + ic-os/components/guestos.bzl | 2 + .../custom-metrics/metrics_tool.service | 33 ++++ .../custom-metrics/metrics_tool.timer | 10 + ic-os/guestos/defs.bzl | 1 + rs/ic_os/metrics_tool/BUILD.bazel | 54 +++++ rs/ic_os/metrics_tool/Cargo.toml | 12 ++ rs/ic_os/metrics_tool/src/lib.rs | 187 ++++++++++++++++++ rs/ic_os/metrics_tool/src/main.rs | 62 ++++++ rs/ic_os/release/BUILD.bazel | 1 + 11 files changed, 371 insertions(+) create mode 100644 ic-os/components/monitoring/custom-metrics/metrics_tool.service create mode 100644 ic-os/components/monitoring/custom-metrics/metrics_tool.timer create mode 100644 rs/ic_os/metrics_tool/BUILD.bazel create mode 100644 rs/ic_os/metrics_tool/Cargo.toml create mode 100644 rs/ic_os/metrics_tool/src/lib.rs create mode 100644 rs/ic_os/metrics_tool/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 90a6ea8321a..83e39dd41c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9586,6 +9586,14 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b5c7628eac357aecda461130f8074468be5aa4d258a002032d82d817f79f1f8" +[[package]] +name = "ic-metrics-tool" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.5.20", +] + [[package]] name = "ic-nervous-system-agent" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 3cec060dbc2..bd2a68f8f09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,6 +126,7 @@ members = [ "rs/ic_os/build_tools/diroid", "rs/ic_os/config", "rs/ic_os/fstrim_tool", + "rs/ic_os/metrics_tool", "rs/ic_os/os_tools/guestos_tool", "rs/ic_os/os_tools/hostos_tool", "rs/ic_os/build_tools/inject_files", diff --git a/ic-os/components/guestos.bzl b/ic-os/components/guestos.bzl index 5bf638bafc7..c21dcbf4976 100644 --- a/ic-os/components/guestos.bzl +++ b/ic-os/components/guestos.bzl @@ -85,6 +85,8 @@ component_files = { Label("monitoring/journald.conf"): "/etc/systemd/journald.conf", Label("monitoring/nft-exporter/nft-exporter.service"): "/etc/systemd/system/nft-exporter.service", Label("monitoring/nft-exporter/nft-exporter.timer"): "/etc/systemd/system/nft-exporter.timer", + Label("monitoring/custom-metrics/metrics_tool.service"): "/etc/systemd/system/metrics_tool.service", + Label("monitoring/custom-metrics/metrics_tool.timer"): "/etc/systemd/system/metrics_tool.timer", # networking Label("networking/generate-network-config/guestos/generate-network-config.service"): "/etc/systemd/system/generate-network-config.service", diff --git a/ic-os/components/monitoring/custom-metrics/metrics_tool.service b/ic-os/components/monitoring/custom-metrics/metrics_tool.service new file mode 100644 index 00000000000..c03e1125ab8 --- /dev/null +++ b/ic-os/components/monitoring/custom-metrics/metrics_tool.service @@ -0,0 +1,33 @@ +[Unit] +Description=Report custom metrics once per minute + +[Service] +Type=oneshot +ExecStart=/opt/ic/bin/metrics_tool --metrics /run/node_exporter/collector_textfile/custom_metrics.prom +DeviceAllow=/dev/vda +IPAddressDeny=any +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateDevices=no +PrivateNetwork=yes +PrivateTmp=yes +PrivateUsers=no +ProtectClock=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectSystem=strict +ReadOnlyPaths=/proc/interrupts +ReadWritePaths=/run/node_exporter/collector_textfile +RestrictAddressFamilies=AF_UNIX +RestrictAddressFamilies=~AF_UNIX +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service +UMask=022 diff --git a/ic-os/components/monitoring/custom-metrics/metrics_tool.timer b/ic-os/components/monitoring/custom-metrics/metrics_tool.timer new file mode 100644 index 00000000000..7015869a5f0 --- /dev/null +++ b/ic-os/components/monitoring/custom-metrics/metrics_tool.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Collect custom metrics every minute + +[Timer] +OnBootSec=60s +OnUnitActiveSec=60s +Unit=metrics_tool.service + +[Install] +WantedBy=timers.target diff --git a/ic-os/guestos/defs.bzl b/ic-os/guestos/defs.bzl index 30e3d49787b..177302685d2 100644 --- a/ic-os/guestos/defs.bzl +++ b/ic-os/guestos/defs.bzl @@ -50,6 +50,7 @@ def image_deps(mode, malicious = False): "//cpp:infogetty": "/opt/ic/bin/infogetty:0755", # Terminal manager that replaces the login shell. "//cpp:prestorecon": "/opt/ic/bin/prestorecon:0755", # Parallel restorecon replacement for filesystem relabeling. "//rs/ic_os/release:metrics-proxy": "/opt/ic/bin/metrics-proxy:0755", # Proxies, filters, and serves public node metrics. + "//rs/ic_os/release:metrics_tool": "/opt/ic/bin/metrics_tool:0755", # Collects and reports custom metrics. # additional libraries to install "//rs/ic_os/release:nss_icos": "/usr/lib/x86_64-linux-gnu/libnss_icos.so.2:0644", # Allows referring to the guest IPv6 by name guestos from host, and host as hostos from guest. diff --git a/rs/ic_os/metrics_tool/BUILD.bazel b/rs/ic_os/metrics_tool/BUILD.bazel new file mode 100644 index 00000000000..b182a7a8a93 --- /dev/null +++ b/rs/ic_os/metrics_tool/BUILD.bazel @@ -0,0 +1,54 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test", "rust_test_suite") + +package(default_visibility = ["//rs:ic-os-pkg"]) + +DEPENDENCIES = [ + # Keep sorted. + "//rs/sys", + "@crate_index//:anyhow", + "@crate_index//:clap", +] + +DEV_DEPENDENCIES = [ + # Keep sorted. +] + +MACRO_DEPENDENCIES = [] + +ALIASES = {} + +rust_library( + name = "metrics_tool", + srcs = glob( + ["src/**/*.rs"], + exclude = ["src/main.rs"], + ), + aliases = ALIASES, + crate_name = "ic_metrics_tool", + proc_macro_deps = MACRO_DEPENDENCIES, + visibility = ["//rs:system-tests-pkg"], + deps = DEPENDENCIES, +) + +rust_binary( + name = "metrics_tool_bin", + srcs = ["src/main.rs"], + aliases = ALIASES, + proc_macro_deps = MACRO_DEPENDENCIES, + deps = DEPENDENCIES + [":metrics_tool"], +) + +rust_test( + name = "metrics_tool_test", + crate = ":metrics_tool", + deps = DEPENDENCIES + DEV_DEPENDENCIES, +) + +rust_test_suite( + name = "metrics_tool_integration", + srcs = glob(["tests/**/*.rs"]), + target_compatible_with = [ + "@platforms//os:linux", + ], + deps = [":metrics_tool_bin"] + DEPENDENCIES + DEV_DEPENDENCIES, +) diff --git a/rs/ic_os/metrics_tool/Cargo.toml b/rs/ic_os/metrics_tool/Cargo.toml new file mode 100644 index 00000000000..4e6b604e829 --- /dev/null +++ b/rs/ic_os/metrics_tool/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ic-metrics-tool" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "metrics_tool" +path = "src/main.rs" + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } \ No newline at end of file diff --git a/rs/ic_os/metrics_tool/src/lib.rs b/rs/ic_os/metrics_tool/src/lib.rs new file mode 100644 index 00000000000..02bb129c55d --- /dev/null +++ b/rs/ic_os/metrics_tool/src/lib.rs @@ -0,0 +1,187 @@ +// TODO: refactor/merge this with fstrim_tool and guestos_tool metrics functionality +use std::fs::File; +use std::io::{self, Write}; +use std::path::Path; + +// TODO: everything is floating point for now +pub struct Metric { + name: String, + value: f64, + annotation: String, + labels: Vec<(String, String)>, +} + +impl Metric { + pub fn new(name: &str, value: f64) -> Self { + Self { + name: name.to_string(), + value, + annotation: "Custom metric".to_string(), + labels: Vec::new(), + } + } + pub fn with_annotation(name: &str, value: f64, annotation: &str) -> Self { + Self { + name: name.to_string(), + value, + annotation: annotation.to_string(), + labels: Vec::new(), + } + } + + pub fn add_annotation(mut self, annotation: &str) -> Self { + self.annotation = annotation.to_string(); + self + } + + pub fn add_label(mut self, key: &str, value: &str) -> Self { + self.labels.push((key.to_string(), value.to_string())); + self + } + + // TODO: formatting of floats + // Convert to prometheus exposition format + pub fn to_prom_string(&self) -> String { + let labels_str = if self.labels.is_empty() { + String::new() + } else { + let labels: Vec = self + .labels + .iter() + .map(|(k, v)| format!("{}=\"{}\"", k, v)) + .collect(); + format!("{{{}}}", labels.join(",")) + }; + format!( + "# HELP {} {}\n\ + # TYPE {} counter\n\ + {}{} {}", + self.name, self.annotation, self.name, self.name, labels_str, self.value + ) + } +} + +pub struct MetricsWriter { + file_path: String, +} + +impl MetricsWriter { + pub fn new(file_path: &str) -> Self { + Self { + file_path: file_path.to_string(), + } + } + + pub fn write_metrics(&self, metrics: &[Metric]) -> io::Result<()> { + let path = Path::new(&self.file_path); + let mut file = File::create(path)?; + for metric in metrics { + writeln!(file, "{}", metric.to_prom_string())?; + } + Ok(()) + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_metric_to_string() { + let metric = Metric::new("test_metric", 123.45) + .add_label("label1", "value1") + .add_label("label2", "value2"); + assert_eq!( + metric.to_prom_string(), + "# HELP test_metric Custom metric\n\ + # TYPE test_metric counter\n\ + test_metric{label1=\"value1\",label2=\"value2\"} 123.45" + ); + } + + #[test] + fn test_write_metrics() { + let metrics = vec![ + Metric::new("metric1", 1.0), + Metric::new("metric2", 2.0).add_label("label", "value"), + ]; + let writer = MetricsWriter::new("/tmp/test_metrics.prom"); + writer.write_metrics(&metrics).unwrap(); + let content = std::fs::read_to_string("/tmp/test_metrics.prom").unwrap(); + assert!(content.contains( + "# HELP metric1 Custom metric\n\ + # TYPE metric1 counter\n\ + metric1 1" + )); + assert!(content.contains( + "# HELP metric2 Custom metric\n\ + # TYPE metric2 counter\n\ + metric2{label=\"value\"} 2" + )); + } + + #[test] + fn test_metric_large_value() { + let metric = Metric::new("large_value_metric", 1.0e64); + assert_eq!( + metric.to_prom_string(), + "# HELP large_value_metric Custom metric\n\ + # TYPE large_value_metric counter\n\ + large_value_metric 10000000000000000000000000000000000000000000000000000000000000000" + ); + } + + #[test] + fn test_metric_without_labels() { + let metric = Metric::new("no_label_metric", 42.0); + assert_eq!( + metric.to_prom_string(), + "# HELP no_label_metric Custom metric\n\ + # TYPE no_label_metric counter\n\ + no_label_metric 42" + ); + } + + #[test] + fn test_metric_with_annotation() { + let metric = Metric::with_annotation("annotated_metric", 99.9, "This is a test metric"); + assert_eq!( + metric.to_prom_string(), + "# HELP annotated_metric This is a test metric\n\ + # TYPE annotated_metric counter\n\ + annotated_metric 99.9" + ); + } + + #[test] + fn test_write_empty_metrics() { + let metrics: Vec = Vec::new(); + let writer = MetricsWriter::new("/tmp/test_empty_metrics.prom"); + writer.write_metrics(&metrics).unwrap(); + let content = std::fs::read_to_string("/tmp/test_empty_metrics.prom").unwrap(); + assert!(content.is_empty()); + } + + #[test] + fn test_metric_with_multiple_labels() { + let metric = Metric::new("multi_label_metric", 10.0) + .add_label("foo", "bar") + .add_label("version", "1.0.0"); + assert_eq!( + metric.to_prom_string(), + "# HELP multi_label_metric Custom metric\n\ + # TYPE multi_label_metric counter\n\ + multi_label_metric{foo=\"bar\",version=\"1.0.0\"} 10" + ); + } + + #[test] + fn test_metric_with_empty_annotation() { + let metric = Metric::with_annotation("empty_annotation_metric", 5.5, ""); + assert_eq!( + metric.to_prom_string(), + "# HELP empty_annotation_metric \n\ + # TYPE empty_annotation_metric counter\n\ + empty_annotation_metric 5.5" + ); + } +} diff --git a/rs/ic_os/metrics_tool/src/main.rs b/rs/ic_os/metrics_tool/src/main.rs new file mode 100644 index 00000000000..cfd4a295c3f --- /dev/null +++ b/rs/ic_os/metrics_tool/src/main.rs @@ -0,0 +1,62 @@ +use anyhow::Result; +use clap::Parser; + +use std::fs::File; +use std::io::{self, BufRead}; +use std::path::Path; + +use ic_metrics_tool::{Metric, MetricsWriter}; + +const INTERRUPT_FILTER: &str = "TLB shootdowns"; +const INTERRUPT_SOURCE: &str = "/proc/interrupts"; +const CUSTOM_METRICS_PROM: &str = "/run/node_exporter/collector_textfile/custom_metrics.prom"; +const TLB_SHOOTDOWN_METRIC_NAME: &str = "sum_tlb_shootdowns"; +const TLB_SHOOTDOWN_METRIC_ANNOTATION: &str = "Total TLB shootdowns"; + +#[derive(Parser)] +struct MetricToolArgs { + #[arg( + short = 'm', + long = "metrics", + default_value = CUSTOM_METRICS_PROM + )] + /// Filename to write the prometheus metrics for node_exporter generation. + /// Fails badly if the directory doesn't exist. + metrics_filename: String, +} + +fn get_sum_tlb_shootdowns() -> Result { + let path = Path::new(INTERRUPT_SOURCE); + let file = File::open(path)?; + let reader = io::BufReader::new(file); + + let mut total_tlb_shootdowns = 0; + + for line in reader.lines() { + let line = line?; + if line.contains(INTERRUPT_FILTER) { + for part in line.split_whitespace().skip(1) { + if let Ok(value) = part.parse::() { + total_tlb_shootdowns += value; + } + } + } + } + + Ok(total_tlb_shootdowns) +} + +pub fn main() -> Result<()> { + let opts = MetricToolArgs::parse(); + let mpath = Path::new(&opts.metrics_filename); + let tlb_shootdowns = get_sum_tlb_shootdowns()?; + + let metrics = vec![ + Metric::new(TLB_SHOOTDOWN_METRIC_NAME, tlb_shootdowns as f64) + .add_annotation(TLB_SHOOTDOWN_METRIC_ANNOTATION), + ]; + let writer = MetricsWriter::new(mpath.to_str().unwrap()); + writer.write_metrics(&metrics).unwrap(); + + Ok(()) +} diff --git a/rs/ic_os/release/BUILD.bazel b/rs/ic_os/release/BUILD.bazel index d5a5eb40598..902e58a3082 100644 --- a/rs/ic_os/release/BUILD.bazel +++ b/rs/ic_os/release/BUILD.bazel @@ -13,6 +13,7 @@ OBJECTS = { "vsock_host": "//rs/ic_os/vsock/host:vsock_host", "metrics-proxy": "@crate_index//:metrics-proxy__metrics-proxy", "nss_icos": "//rs/ic_os/nss_icos", + "metrics_tool": "//rs/ic_os/metrics_tool:metrics_tool_bin", } [release_strip_binary( From c705212d5b08b808262cf314d161d658527792b3 Mon Sep 17 00:00:00 2001 From: NikolasHai <113891786+NikolasHai@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:07:16 +0200 Subject: [PATCH 14/22] feat(ICP-Rosetta): FI-1540: add disburse of neuron functionality (#2182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This MR proposes the following changes: 1. Add the functionality of disbursing a neuron to the ICP Rosetta client --------- Co-authored-by: Mathias Björkqvist --- Cargo.lock | 1 + rs/rosetta-api/icp/BUILD.bazel | 9 +- rs/rosetta-api/icp/Cargo.toml | 1 + rs/rosetta-api/icp/client/src/lib.rs | 135 ++++++-- .../common/system_test_environment.rs | 101 ++++-- .../icp/tests/system_tests/common/utils.rs | 15 + .../test_cases/neuron_management.rs | 162 +++++++++- rs/tests/Cargo.toml | 4 - .../rosetta/BUILD.bazel | 21 -- .../rosetta/rosetta_neuron_disburse_test.rs | 21 -- rs/tests/src/rosetta_tests/tests.rs | 1 - .../rosetta_tests/tests/neuron_disburse.rs | 300 ------------------ 12 files changed, 369 insertions(+), 402 deletions(-) delete mode 100644 rs/tests/financial_integrations/rosetta/rosetta_neuron_disburse_test.rs delete mode 100644 rs/tests/src/rosetta_tests/tests/neuron_disburse.rs diff --git a/Cargo.lock b/Cargo.lock index 83e39dd41c9..91cc39c4a32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11481,6 +11481,7 @@ dependencies = [ "prost 0.13.3", "rand 0.8.5", "rand_chacha 0.3.1", + "registry-canister", "reqwest 0.12.8", "rolling-file", "rosetta-core", diff --git a/rs/rosetta-api/icp/BUILD.bazel b/rs/rosetta-api/icp/BUILD.bazel index da51dfa29e6..388bfcbdfdb 100644 --- a/rs/rosetta-api/icp/BUILD.bazel +++ b/rs/rosetta-api/icp/BUILD.bazel @@ -79,6 +79,7 @@ DEV_DEPENDENCIES = [ "//rs/nns/governance/init", "//rs/nns/handlers/root/impl:root", "//rs/nns/test_utils", + "//rs/registry/canister", "//rs/rosetta-api/icp:rosetta-api", "//rs/rosetta-api/icp/client:ic-icp-rosetta-client", "//rs/rosetta-api/icp/ledger_canister_blocks_synchronizer/test_utils", @@ -168,9 +169,11 @@ rust_test_suite_with_extra_srcs( "//rs/canister_sandbox", "//rs/canister_sandbox:sandbox_launcher", "//rs/ledger_suite/icp/ledger:ledger-canister-wasm-notify-method", - "//rs/nns/governance:governance-canister", + "//rs/nns/governance:governance-canister-test", + "//rs/nns/handlers/lifeline/impl:lifeline_canister", "//rs/nns/handlers/root/impl:root-canister", "//rs/pocket_ic_server:pocket-ic-server", + "//rs/registry/canister:registry-canister", "//rs/replica", "//rs/rosetta-api/icp:ic-rosetta-api-rosetta-blocks", "//rs/rosetta-api/icp:rosetta-api", @@ -184,8 +187,10 @@ rust_test_suite_with_extra_srcs( "ROSETTA_BIN_PATH": "$(rootpath //rs/rosetta-api/icp:ic-rosetta-api-rosetta-blocks)", "SANDBOX_LAUNCHER": "$(rootpath //rs/canister_sandbox:sandbox_launcher)", "ICP_LEDGER_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_icp_ledger_canister//file)", - "GOVERNANCE_CANISTER_WASM_PATH": "$(rootpath //rs/nns/governance:governance-canister)", + "GOVERNANCE_CANISTER_WASM_PATH": "$(rootpath //rs/nns/governance:governance-canister-test)", "ROOT_CANISTER_WASM_PATH": "$(rootpath //rs/nns/handlers/root/impl:root-canister)", + "REGISTRY_CANISTER_WASM_PATH": "$(rootpath //rs/registry/canister:registry-canister)", + "LIFELINE_CANISTER_WASM_PATH": "$(rootpath //rs/nns/handlers/lifeline/impl:lifeline_canister)", }, extra_srcs = glob([ "tests/system_tests/common/*.rs", diff --git a/rs/rosetta-api/icp/Cargo.toml b/rs/rosetta-api/icp/Cargo.toml index baf8963db53..a013e7f430c 100644 --- a/rs/rosetta-api/icp/Cargo.toml +++ b/rs/rosetta-api/icp/Cargo.toml @@ -38,6 +38,7 @@ num-bigint = { workspace = true } on_wire = { path = "../../rust_canisters/on_wire" } prometheus = { workspace = true } rand = { workspace = true } +registry-canister = { path = "../../registry/canister" } reqwest = { workspace = true } rolling-file = { workspace = true } rosetta-core = { path = "../common/rosetta_core" } diff --git a/rs/rosetta-api/icp/client/src/lib.rs b/rs/rosetta-api/icp/client/src/lib.rs index e5e0fe455f9..934728fbbc7 100644 --- a/rs/rosetta-api/icp/client/src/lib.rs +++ b/rs/rosetta-api/icp/client/src/lib.rs @@ -11,6 +11,7 @@ use ic_rosetta_api::models::ConstructionMetadataRequestOptions; use ic_rosetta_api::models::ConstructionPayloadsRequestMetadata; use ic_rosetta_api::models::OperationIdentifier; use ic_rosetta_api::request_types::ChangeAutoStakeMaturityMetadata; +use ic_rosetta_api::request_types::DisburseMetadata; use ic_rosetta_api::request_types::NeuronIdentifierMetadata; use ic_rosetta_api::request_types::RequestType; use ic_rosetta_api::request_types::SetDissolveTimestampMetadata; @@ -327,6 +328,35 @@ impl RosettaClient { }]) } + pub async fn build_disburse_neuron_operations( + signer_principal: Principal, + neuron_index: u64, + recipient: Option, + ) -> anyhow::Result> { + Ok(vec![Operation { + operation_identifier: OperationIdentifier { + index: 0, + network_index: None, + }, + related_operations: None, + type_: "DISBURSE".to_string(), + status: None, + account: Some(rosetta_core::identifiers::AccountIdentifier::from( + AccountIdentifier::new(PrincipalId(signer_principal), None), + )), + amount: None, + coin_change: None, + metadata: Some( + DisburseMetadata { + neuron_index, + recipient, + } + .try_into() + .map_err(|e| anyhow::anyhow!("Failed to convert metadata: {:?}", e))?, + ), + }]) + } + pub async fn network_list(&self) -> anyhow::Result { self.call_endpoint("/network/list", &MetadataRequest { metadata: None }) .await @@ -899,6 +929,33 @@ impl RosettaClient { ) .await } + + /// If a neuron is in the state DISSOLVED you can disburse the neuron with this function. + pub async fn disburse_neuron( + &self, + network_identifier: NetworkIdentifier, + signer_keypair: &T, + disburse_neuron_args: RosettaDisburseNeuronArgs, + ) -> anyhow::Result + where + T: RosettaSupportedKeyPair, + { + let disburse_neuron_operations = RosettaClient::build_disburse_neuron_operations( + signer_keypair.generate_principal_id()?.0, + disburse_neuron_args.neuron_index, + disburse_neuron_args.recipient, + ) + .await?; + + self.make_submit_and_wait_for_transaction( + signer_keypair, + network_identifier, + disburse_neuron_operations, + None, + None, + ) + .await + } } pub struct RosettaTransferArgs { @@ -1083,6 +1140,41 @@ impl RosettaIncreaseNeuronStakeArgs { } } +pub struct RosettaIncreaseNeuronStakeArgsBuilder { + additional_stake: Nat, + neuron_index: Option, + // The subaccount from which the ICP should be transferred + from_subaccount: Option<[u8; 32]>, +} + +impl RosettaIncreaseNeuronStakeArgsBuilder { + pub fn new(additional_stake: Nat) -> Self { + Self { + additional_stake, + neuron_index: None, + from_subaccount: None, + } + } + + pub fn with_neuron_index(mut self, neuron_index: u64) -> Self { + self.neuron_index = Some(neuron_index); + self + } + + pub fn with_from_subaccount(mut self, from_subaccount: Subaccount) -> Self { + self.from_subaccount = Some(from_subaccount); + self + } + + pub fn build(self) -> RosettaIncreaseNeuronStakeArgs { + RosettaIncreaseNeuronStakeArgs { + additional_stake: self.additional_stake, + neuron_index: self.neuron_index, + from_subaccount: self.from_subaccount, + } + } +} + pub struct RosettaChangeAutoStakeMaturityArgs { pub neuron_index: Option, pub requested_setting_for_auto_stake_maturity: bool, @@ -1122,38 +1214,39 @@ impl RosettaChangeAutoStakeMaturityArgsBuilder { } } } +pub struct RosettaDisburseNeuronArgs { + pub neuron_index: u64, + pub recipient: Option, +} -pub struct RosettaIncreaseNeuronStakeArgsBuilder { - additional_stake: Nat, - neuron_index: Option, - // The subaccount from which the ICP should be transferred - from_subaccount: Option<[u8; 32]>, +impl RosettaDisburseNeuronArgs { + pub fn builder(neuron_index: u64) -> RosettaDisburseNeuronArgsBuilder { + RosettaDisburseNeuronArgsBuilder::new(neuron_index) + } } -impl RosettaIncreaseNeuronStakeArgsBuilder { - pub fn new(additional_stake: Nat) -> Self { +pub struct RosettaDisburseNeuronArgsBuilder { + neuron_index: u64, + recipient: Option, +} + +impl RosettaDisburseNeuronArgsBuilder { + pub fn new(neuron_index: u64) -> Self { Self { - additional_stake, - neuron_index: None, - from_subaccount: None, + neuron_index, + recipient: None, } } - pub fn with_neuron_index(mut self, neuron_index: u64) -> Self { - self.neuron_index = Some(neuron_index); + pub fn with_recipient(mut self, recipient: AccountIdentifier) -> Self { + self.recipient = Some(recipient); self } - pub fn with_from_subaccount(mut self, from_subaccount: Subaccount) -> Self { - self.from_subaccount = Some(from_subaccount); - self - } - - pub fn build(self) -> RosettaIncreaseNeuronStakeArgs { - RosettaIncreaseNeuronStakeArgs { - additional_stake: self.additional_stake, + pub fn build(self) -> RosettaDisburseNeuronArgs { + RosettaDisburseNeuronArgs { neuron_index: self.neuron_index, - from_subaccount: self.from_subaccount, + recipient: self.recipient, } } } diff --git a/rs/rosetta-api/icp/tests/system_tests/common/system_test_environment.rs b/rs/rosetta-api/icp/tests/system_tests/common/system_test_environment.rs index 4dd59ea837d..a27b8c7e2fa 100644 --- a/rs/rosetta-api/icp/tests/system_tests/common/system_test_environment.rs +++ b/rs/rosetta-api/icp/tests/system_tests/common/system_test_environment.rs @@ -1,5 +1,6 @@ use crate::common::utils::get_custom_agent; -use crate::common::utils::wait_for_rosetta_to_sync_up_to_block; +use crate::common::utils::get_test_agent; +use crate::common::utils::wait_for_rosetta_to_catch_up_with_icp_ledger; use crate::common::{ constants::{DEFAULT_INITIAL_BALANCE, STARTING_CYCLES_PER_CANISTER}, utils::test_identity, @@ -16,12 +17,16 @@ use ic_icrc1_test_utils::LedgerEndpointArg; use ic_icrc1_tokens_u256::U256; use ic_ledger_test_utils::build_ledger_wasm; use ic_ledger_test_utils::pocket_ic_helpers::ledger::LEDGER_CANISTER_ID; +use ic_nns_common::init::LifelineCanisterInitPayloadBuilder; use ic_nns_constants::GOVERNANCE_CANISTER_ID; use ic_nns_constants::LIFELINE_CANISTER_ID; +use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_constants::ROOT_CANISTER_ID; use ic_nns_governance_init::GovernanceCanisterInitPayloadBuilder; use ic_nns_handler_root::init::RootCanisterInitPayloadBuilder; use ic_nns_test_utils::common::build_governance_wasm; +use ic_nns_test_utils::common::build_lifeline_wasm; +use ic_nns_test_utils::common::build_registry_wasm; use ic_nns_test_utils::common::build_root_wasm; use ic_rosetta_test_utils::path_from_env; use ic_types::PrincipalId; @@ -32,6 +37,7 @@ use num_traits::cast::ToPrimitive; use pocket_ic::CanisterSettings; use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder}; use prost::Message; +use registry_canister::init::RegistryCanisterInitPayloadBuilder; use rosetta_core::identifiers::NetworkIdentifier; use std::collections::HashMap; use tempfile::TempDir; @@ -132,14 +138,6 @@ impl RosettaTestingEnvironment { } pub async fn restart_rosetta_node(mut self, options: RosettaOptions) -> Self { - let ledger_tip = self - .rosetta_client - .network_status(self.network_identifier.clone()) - .await - .unwrap() - .current_block_identifier - .index; - self.rosetta_context.kill_rosetta_process(); let rosetta_bin = path_from_env("ROSETTA_BIN_PATH"); @@ -149,13 +147,12 @@ impl RosettaTestingEnvironment { self.rosetta_client = RosettaClient::from_str_url(&format!("http://localhost:{}", self.rosetta_context.port)) .expect("Unable to parse url"); - wait_for_rosetta_to_sync_up_to_block( + wait_for_rosetta_to_catch_up_with_icp_ledger( &self.rosetta_client, self.network_identifier.clone(), - ledger_tip, + &get_test_agent(self.pocket_ic.url().unwrap().port().unwrap()).await, ) - .await - .unwrap(); + .await; self } } @@ -280,7 +277,9 @@ impl RosettaTestingEnvironmentBuilder { Some(nns_root_canister_controller), ) .await; - + pocket_ic + .add_cycles(nns_root_canister_id, STARTING_CYCLES_PER_CANISTER) + .await; let governance_canister_wasm = build_governance_wasm(); let governance_canister_id = Principal::from(GOVERNANCE_CANISTER_ID); let governance_canister_controller = ROOT_CANISTER_ID.get().0; @@ -316,6 +315,61 @@ impl RosettaTestingEnvironmentBuilder { .advance_time(std::time::Duration::from_secs(60)) .await; pocket_ic.tick().await; + + let nns_lifeline_canister_wasm = build_lifeline_wasm(); + let nns_lifeline_canister_id = Principal::from(LIFELINE_CANISTER_ID); + let nns_lifeline_canister_controller = ROOT_CANISTER_ID.get().0; + let nns_lifeline_canister = pocket_ic + .create_canister_with_id( + Some(nns_lifeline_canister_controller), + Some(CanisterSettings { + controllers: Some(vec![nns_lifeline_canister_controller]), + ..Default::default() + }), + nns_lifeline_canister_id, + ) + .await + .expect("Unable to create the NNS Lifeline canister"); + + pocket_ic + .install_canister( + nns_lifeline_canister, + nns_lifeline_canister_wasm.bytes().to_vec(), + Encode!(&LifelineCanisterInitPayloadBuilder::new().build()).unwrap(), + Some(nns_lifeline_canister_controller), + ) + .await; + pocket_ic + .add_cycles(nns_lifeline_canister_id, STARTING_CYCLES_PER_CANISTER) + .await; + + let nns_registry_canister_wasm = build_registry_wasm(); + let nns_registry_canister_id = Principal::from(REGISTRY_CANISTER_ID); + let nns_registry_canister_controller = ROOT_CANISTER_ID.get().0; + let nns_registry_canister = pocket_ic + .create_canister_with_id( + Some(nns_registry_canister_controller), + Some(CanisterSettings { + controllers: Some(vec![nns_registry_canister_controller]), + ..Default::default() + }), + nns_registry_canister_id, + ) + .await + .expect("Unable to create the NNS Registry canister"); + + pocket_ic + .install_canister( + nns_registry_canister, + nns_registry_canister_wasm.bytes().to_vec(), + Encode!(&RegistryCanisterInitPayloadBuilder::new().build()).unwrap(), + Some(nns_registry_canister_controller), + ) + .await; + + pocket_ic + .add_cycles(nns_registry_canister_id, STARTING_CYCLES_PER_CANISTER) + .await; } let replica_url = pocket_ic.make_live(None).await; @@ -384,19 +438,12 @@ impl RosettaTestingEnvironmentBuilder { .unwrap(); // Wait for rosetta to catch up with the ledger - if let Some(last_block_idx) = block_idxes.last() { - let rosetta_last_block_idx = wait_for_rosetta_to_sync_up_to_block( - &rosetta_client, - network_identifier.clone(), - *last_block_idx, - ) - .await; - assert_eq!( - Some(*last_block_idx), - rosetta_last_block_idx, - "Wait for rosetta sync failed." - ); - } + wait_for_rosetta_to_catch_up_with_icp_ledger( + &rosetta_client, + network_identifier.clone(), + &get_test_agent(replica_port).await, + ) + .await; RosettaTestingEnvironment { pocket_ic, diff --git a/rs/rosetta-api/icp/tests/system_tests/common/utils.rs b/rs/rosetta-api/icp/tests/system_tests/common/utils.rs index ecaf0867c0e..1d98d82455b 100644 --- a/rs/rosetta-api/icp/tests/system_tests/common/utils.rs +++ b/rs/rosetta-api/icp/tests/system_tests/common/utils.rs @@ -10,6 +10,7 @@ use ic_nns_constants::GOVERNANCE_CANISTER_ID; use ic_nns_constants::LEDGER_CANISTER_ID; use ic_nns_governance::pb::v1::ListNeurons; use ic_nns_governance::pb::v1::ListNeuronsResponse; +use ic_nns_governance_api::pb::v1::GovernanceError; use ic_rosetta_api::convert::to_hash; use icp_ledger::GetBlocksArgs; use icp_ledger::QueryEncodedBlocksResponse; @@ -190,3 +191,17 @@ pub async fn list_neurons(agent: &Agent) -> ListNeuronsResponse { ) .unwrap() } + +pub async fn update_neuron(agent: &Agent, neuron: ic_nns_governance_api::pb::v1::Neuron) { + let result = Decode!( + &agent + .update(&GOVERNANCE_CANISTER_ID.into(), "update_neuron") + .with_arg(Encode!(&neuron).unwrap()) + .call_and_wait() + .await + .unwrap(), + Option + ) + .unwrap(); + assert!(result.is_none(), "Failed to update neuron: {:?}", result); +} diff --git a/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs b/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs index 248b1e8b8b6..c59a09c3453 100644 --- a/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs +++ b/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs @@ -1,3 +1,4 @@ +use crate::common::utils::update_neuron; use crate::common::utils::wait_for_rosetta_to_catch_up_with_icp_ledger; use crate::common::{ system_test_environment::RosettaTestingEnvironment, @@ -6,14 +7,17 @@ use crate::common::{ use ic_agent::{identity::BasicIdentity, Identity}; use ic_icp_rosetta_client::RosettaChangeAutoStakeMaturityArgs; use ic_icp_rosetta_client::RosettaIncreaseNeuronStakeArgs; -use ic_icp_rosetta_client::{RosettaCreateNeuronArgs, RosettaSetNeuronDissolveDelayArgs}; +use ic_icp_rosetta_client::{ + RosettaCreateNeuronArgs, RosettaDisburseNeuronArgs, RosettaSetNeuronDissolveDelayArgs, +}; use ic_nns_governance::pb::v1::neuron::DissolveState; -use ic_rosetta_api::request::transaction_operation_results::TransactionOperationResults; +use ic_rosetta_api::{ + models::AccountBalanceRequest, + request::transaction_operation_results::TransactionOperationResults, +}; use ic_types::PrincipalId; -use icp_ledger::AccountIdentifier; -use icp_ledger::DEFAULT_TRANSFER_FEE; +use icp_ledger::{AccountIdentifier, DEFAULT_TRANSFER_FEE}; use lazy_static::lazy_static; -use rosetta_core::request_types::AccountBalanceRequest; use std::{ sync::Arc, time::{SystemTime, UNIX_EPOCH}, @@ -499,3 +503,151 @@ fn test_change_auto_stake_maturity() { assert!(neuron.auto_stake_maturity.is_none()); }); } + +#[test] +fn test_disburse_neuron() { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let initial_balance = 100_000_000_000; + let env = RosettaTestingEnvironment::builder() + .with_initial_balances( + vec![( + AccountIdentifier::from(TEST_IDENTITY.sender().unwrap()), + // A hundred million ICP should be enough + icp_ledger::Tokens::from_e8s(initial_balance), + )] + .into_iter() + .collect(), + ) + .with_governance_canister() + .build() + .await; + + // Stake the minimum amount 100 million e8s + let staked_amount = initial_balance/10; + let neuron_index = 0; + let from_subaccount = [0; 32]; + + env.rosetta_client + .create_neuron( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + RosettaCreateNeuronArgs::builder(staked_amount.into()) + .with_from_subaccount(from_subaccount) + .with_neuron_index(neuron_index) + .build(), + ) + .await + .unwrap(); + // See if the neuron was created successfully + let agent = get_test_agent(env.pocket_ic.url().unwrap().port().unwrap()).await; + + TransactionOperationResults::try_from( + env.rosetta_client + .start_dissolving_neuron( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + neuron_index, + ) + .await + .unwrap() + .metadata, + ) + .unwrap(); + + let mut neuron = list_neurons(&agent).await.full_neurons[0].to_owned(); + // If we try to disburse the neuron when it is not yet DISSOLVED we expect an error + match env + .rosetta_client + .disburse_neuron( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + RosettaDisburseNeuronArgs::builder(neuron_index) + .with_recipient(TEST_IDENTITY.sender().unwrap().into()) + .build(), + ) + .await + { + Err(e) if e.to_string().contains(&format!("Could not disburse: PreconditionFailed: Neuron {} has NOT been dissolved. It is in state Dissolving",neuron.id.unwrap().id)) => (), + Err(e) => panic!("Unexpected error: {}", e), + Ok(_) => panic!("Expected an error but got success"), + } + // Let rosetta catch up with the transfer that happended when creating the neuron + wait_for_rosetta_to_catch_up_with_icp_ledger( + &env.rosetta_client, + env.network_identifier.clone(), + &agent, + ).await; + let balance_before_disburse = env + .rosetta_client + .account_balance( + AccountBalanceRequest::builder( + env.network_identifier.clone(), + AccountIdentifier::from(TEST_IDENTITY.sender().unwrap()).into(), + ) + .build(), + ) + .await + .unwrap() + .balances + .first() + .unwrap() + .clone() + .value.parse::().unwrap(); + + // We now update the neuron so it is in state DISSOLVED + let now = env.pocket_ic.get_time().await.duration_since(UNIX_EPOCH).unwrap().as_secs(); + neuron.dissolve_state = Some(DissolveState::WhenDissolvedTimestampSeconds(now - 1)); + update_neuron(&agent, neuron.into()).await; + + match list_neurons(&agent).await.full_neurons[0].dissolve_state.unwrap() { + DissolveState::WhenDissolvedTimestampSeconds (d) => { + // The neuron should now be in DISSOLVED state + assert!(d panic!( + "Neuron should be in DissolveDelaySeconds state, but is instead: {:?}", + k + ), + } + + // Now we should be able to disburse the neuron + env.rosetta_client + .disburse_neuron( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + RosettaDisburseNeuronArgs::builder(neuron_index) + .with_recipient(TEST_IDENTITY.sender().unwrap().into()) + .build(), + ) + .await + .unwrap(); + + // Wait for the ledger to sync up to the block where the disbursement happened + wait_for_rosetta_to_catch_up_with_icp_ledger( + &env.rosetta_client, + env.network_identifier.clone(), + &agent, + ) + .await; + + // The recipient should have received the disbursed amount + let balance_after_disburse = env + .rosetta_client + .account_balance( + AccountBalanceRequest::builder( + env.network_identifier.clone(), + AccountIdentifier::from(TEST_IDENTITY.sender().unwrap()).into(), + ) + .build(), + ) + .await + .unwrap() + .balances + .first() + .unwrap().clone() + .value.parse::().unwrap(); + // The balance should be the same as before the creation of the neuron minus the transfer fee + assert_eq!(balance_after_disburse, balance_before_disburse + staked_amount - DEFAULT_TRANSFER_FEE.get_e8s()); + }); +} diff --git a/rs/tests/Cargo.toml b/rs/tests/Cargo.toml index cc9e688fd06..0db2d67e31f 100644 --- a/rs/tests/Cargo.toml +++ b/rs/tests/Cargo.toml @@ -195,10 +195,6 @@ path = "financial_integrations/rosetta/rosetta_make_transactions_test.rs" name = "ic-systest-rosetta-network-test" path = "financial_integrations/rosetta/rosetta_network_test.rs" -[[bin]] -name = "ic-systest-rosetta-neuron-disburse-test" -path = "financial_integrations/rosetta/rosetta_neuron_disburse_test.rs" - [[bin]] name = "ic-systest-rosetta-neuron-dissolve-test" path = "financial_integrations/rosetta/rosetta_neuron_dissolve_test.rs" diff --git a/rs/tests/financial_integrations/rosetta/BUILD.bazel b/rs/tests/financial_integrations/rosetta/BUILD.bazel index e8792bcc3cb..8646f37569f 100644 --- a/rs/tests/financial_integrations/rosetta/BUILD.bazel +++ b/rs/tests/financial_integrations/rosetta/BUILD.bazel @@ -78,27 +78,6 @@ system_test_nns( deps = DEPENDENCIES + ["//rs/tests"], ) -system_test_nns( - name = "rosetta_neuron_disburse_test", - extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. - flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, - tags = [ - "k8s", - "long_test", # since it takes longer than 5 minutes. - ], - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = - GUESTOS_RUNTIME_DEPS + - UNIVERSAL_VM_RUNTIME_DEPS + [ - "//rs/rosetta-api/icp:ic-rosetta-api", - "//rs/rosetta-api/icp:rosetta_image.tar", - "//rs/tests:rosetta_workspace", - "@rosetta-cli//:rosetta-cli", - ], - deps = DEPENDENCIES + ["//rs/tests"], -) - system_test_nns( name = "rosetta_neuron_dissolve_test", flaky = True, diff --git a/rs/tests/financial_integrations/rosetta/rosetta_neuron_disburse_test.rs b/rs/tests/financial_integrations/rosetta/rosetta_neuron_disburse_test.rs deleted file mode 100644 index 4a738f442f8..00000000000 --- a/rs/tests/financial_integrations/rosetta/rosetta_neuron_disburse_test.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[rustfmt::skip] -use anyhow::Result; - -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::driver::test_env::TestEnv; -use ic_system_test_driver::systest; -use ic_tests::rosetta_tests; -use rosetta_tests::setup::{ROSETTA_TESTS_OVERALL_TIMEOUT, ROSETTA_TESTS_PER_TEST_TIMEOUT}; -use rosetta_tests::tests; - -fn main() -> Result<()> { - SystemTestGroup::new() - .with_setup(group_setup) - .with_overall_timeout(ROSETTA_TESTS_OVERALL_TIMEOUT) - .with_timeout_per_test(ROSETTA_TESTS_PER_TEST_TIMEOUT) - .add_test(systest!(tests::neuron_disburse::test)) - .execute_from_args()?; - Ok(()) -} - -fn group_setup(_env: TestEnv) {} diff --git a/rs/tests/src/rosetta_tests/tests.rs b/rs/tests/src/rosetta_tests/tests.rs index 4e6485e0135..58d4cea2c0b 100644 --- a/rs/tests/src/rosetta_tests/tests.rs +++ b/rs/tests/src/rosetta_tests/tests.rs @@ -3,7 +3,6 @@ pub mod list_known_neurons; pub mod list_neurons; pub mod make_transaction; pub mod network; -pub mod neuron_disburse; pub mod neuron_dissolve; pub mod neuron_follow; pub mod neuron_hotkey; diff --git a/rs/tests/src/rosetta_tests/tests/neuron_disburse.rs b/rs/tests/src/rosetta_tests/tests/neuron_disburse.rs deleted file mode 100644 index 4e97ad50bdb..00000000000 --- a/rs/tests/src/rosetta_tests/tests/neuron_disburse.rs +++ /dev/null @@ -1,300 +0,0 @@ -use crate::rosetta_tests::{ - ledger_client::LedgerClient, - lib::{ - check_balance, create_ledger_client, do_multiple_txn, make_user_ed25519, - one_day_from_now_nanos, sign_txn, to_public_key, NeuronDetails, - }, - rosetta_client::RosettaApiClient, - setup::setup, - test_neurons::TestNeurons, -}; -use ic_ledger_core::{ - tokens::{CheckedAdd, CheckedSub}, - Tokens, -}; -use ic_nns_governance_api::pb::v1::{neuron::DissolveState, Neuron}; -use ic_rosetta_api::{ - models::{ConstructionPayloadsResponse, SignedTransaction}, - request::{ - request_result::RequestResult, transaction_operation_results::TransactionOperationResults, - Request, - }, - request_types::{Disburse, Status}, -}; -use ic_rosetta_test_utils::RequestInfo; -use ic_system_test_driver::{driver::test_env::TestEnv, util::block_on}; -use icp_ledger::{AccountIdentifier, DEFAULT_TRANSFER_FEE}; -use rosetta_core::objects::ObjectMap; -use serde_json::json; -use slog::Logger; -use std::{collections::HashMap, str::FromStr, sync::Arc, time::UNIX_EPOCH}; - -const PORT: u32 = 8104; -const VM_NAME: &str = "neuron-disburse"; - -pub fn test(env: TestEnv) { - let logger = env.logger(); - - let mut ledger_balances = HashMap::new(); - - // Create neurons. - let neuron_setup = |neuron: &mut Neuron| { - neuron.dissolve_state = Some(DissolveState::WhenDissolvedTimestampSeconds(0)) - }; - - let one_year_from_now = 60 * 60 * 24 * 365 - + std::time::SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let mut neurons = TestNeurons::new(2000, &mut ledger_balances); - - let neuron1 = neurons.create(neuron_setup); - let neuron2 = neurons.create(neuron_setup); - let neuron3 = neurons.create(neuron_setup); - let neuron4 = neurons.create(|neuron| { - neuron.dissolve_state = Some(DissolveState::WhenDissolvedTimestampSeconds( - one_year_from_now, - )) - }); - let neuron5 = neurons.create(neuron_setup); - let neuron6 = neurons.create(neuron_setup); - let neuron7 = neurons.create(neuron_setup); - - // Create Rosetta and ledger clients. - let neurons = neurons.get_neurons(); - let client = setup(&env, PORT, VM_NAME, Some(ledger_balances), Some(neurons)); - let ledger_client = create_ledger_client(&env, &client); - - block_on(async { - test_disburse_raw(&client, &ledger_client, &neuron1, None, None, &logger) - .await - .expect("Failed test raw disburse"); - - test_disburse(&client, &ledger_client, &neuron2, None, None) - .await - .expect("Failed test disburse"); - - // Disburse to custom recipient. - let (recipient, _, _, _) = make_user_ed25519(102); - test_disburse(&client, &ledger_client, &neuron3, None, Some(recipient)) - .await - .expect("Failed test disburse to custom recipient"); - - // Disburse before neuron is dissolved (fail expected). - test_disburse(&client, &ledger_client, &neuron4, None, Some(recipient)) - .await - .unwrap_err(); - - // Disburse an amount. - test_disburse( - &client, - &ledger_client, - &neuron5, - Some(Tokens::new(5, 0).unwrap()), - None, - ) - .await - .expect("Failed test disburse an amount"); - - // Disburse full stake. - test_disburse( - &client, - &ledger_client, - &neuron6, - Some(Tokens::new(10, 0).unwrap()), - None, - ) - .await - .expect("Failed test disburse full stake"); - - // Disburse more than staked amount. - test_disburse( - &client, - &ledger_client, - &neuron7, - Some(Tokens::new(11, 0).unwrap()), - None, - ) - .await - .unwrap_err() - }); -} - -#[allow(clippy::too_many_arguments)] -async fn test_disburse( - ros: &RosettaApiClient, - ledger_client: &LedgerClient, - neuron_info: &NeuronDetails, - amount: Option, - recipient: Option, -) -> Result<(), ic_rosetta_api::models::Error> { - let neuron = &neuron_info.neuron; - let acc = neuron_info.account_id; - let key_pair = Arc::new(neuron_info.key_pair.clone()); - let neuron_index = neuron_info.neuron_subaccount_identifier; - - let pre_disburse = ledger_client.get_account_balance(acc).await; - let (_, tip_idx) = ledger_client.get_tip().await; - - let res = do_multiple_txn( - ros, - &[RequestInfo { - request: Request::Disburse(Disburse { - account: acc, - amount, - recipient, - neuron_index, - }), - sender_keypair: Arc::clone(&key_pair), - }], - false, - Some(one_day_from_now_nanos()), - None, - ) - .await - .map(|(tx_id, results, _)| { - assert!(!tx_id.is_transfer()); - assert!(matches!( - results.operations.first().unwrap(), - RequestResult { - _type: Request::Disburse(_), - status: Status::Completed, - .. - } - )); - results - })?; - - let amount = amount.unwrap_or_else(|| Tokens::from_e8s(neuron.cached_neuron_stake_e8s)); - - let expected_idx = tip_idx + 1; - - if let Some(h) = res.last_block_index() { - assert_eq!(h, expected_idx); - } - let _ = ros.wait_for_block_at(expected_idx).await.unwrap(); - - // governance assumes the default fee for disburse and that's why this check uses the - // DEFAULT_TRANSFER_FEE. - check_balance( - ros, - ledger_client, - &recipient.unwrap_or(acc), - pre_disburse - .checked_add(&amount) - .unwrap() - .checked_sub(&DEFAULT_TRANSFER_FEE) - .unwrap(), - ) - .await; - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -async fn test_disburse_raw( - ros: &RosettaApiClient, - ledger_client: &LedgerClient, - neuron_info: &NeuronDetails, - amount: Option, - recipient: Option, - _logger: &Logger, -) -> Result<(), ic_rosetta_api::models::Error> { - let neuron = &neuron_info.neuron; - let acc = neuron_info.account_id; - let key_pair = Arc::new(neuron_info.key_pair.clone()); - let neuron_index = neuron_info.neuron_subaccount_identifier; - - let pre_disburse = ledger_client.get_account_balance(acc).await; - let (_, tip_idx) = ledger_client.get_tip().await; - let req = json!({ - "network_identifier": &ros.network_id(), - "operations": [ - { - "operation_identifier": { - "index": 0 - }, - "type": "DISBURSE", - "account": { - "address": &acc - }, - "metadata": { - "neuron_index": &neuron_index - } - } - ] - }); - let req = req.to_string(); - - let metadata: ObjectMap = serde_json::from_slice( - &ros.raw_construction_endpoint("metadata", req.as_bytes()) - .await - .unwrap() - .0, - ) - .unwrap(); - - let mut req: ObjectMap = serde_json::from_str(&req).unwrap(); - req.insert("metadata".to_string(), metadata.into()); - req.insert( - "public_keys".to_string(), - serde_json::to_value(vec![to_public_key(&key_pair)]).unwrap(), - ); - - let payloads: ConstructionPayloadsResponse = serde_json::from_slice( - &ros.raw_construction_endpoint("payloads", &serde_json::to_vec_pretty(&req).unwrap()) - .await - .unwrap() - .0, - ) - .unwrap(); - - let signed = sign_txn(ros, &[key_pair.clone()], payloads).await.unwrap(); - - let hash_res = ros - .construction_hash(signed.signed_transaction.clone()) - .await - .unwrap()?; - - let submit_res = ros - .construction_submit(SignedTransaction::from_str(&signed.signed_transaction).unwrap()) - .await - .unwrap()?; - - assert_eq!( - hash_res.transaction_identifier, - submit_res.transaction_identifier - ); - - for op in TransactionOperationResults::try_from(submit_res.metadata) - .unwrap() - .operations - .iter() - { - assert_eq!( - op.status.as_ref().expect("Expecting status to be set."), - "COMPLETED", - "Operation didn't complete." - ); - } - - let amount = amount.unwrap_or_else(|| Tokens::from_e8s(neuron.cached_neuron_stake_e8s)); - let expected_idx = tip_idx + 1; - let _ = ros.wait_for_block_at(expected_idx).await.unwrap(); - - // governance assumes the default fee for disburse and that's why this check uses the - // DEFAULT_TRANSFER_FEE. - check_balance( - ros, - ledger_client, - &recipient.unwrap_or(acc), - pre_disburse - .checked_add(&amount) - .unwrap() - .checked_sub(&DEFAULT_TRANSFER_FEE) - .unwrap(), - ) - .await; - Ok(()) -} From e3c408cd0dd5cc347f64515a54e451efc0dadefb Mon Sep 17 00:00:00 2001 From: kpop-dfinity <125868903+kpop-dfinity@users.noreply.github.com> Date: Thu, 24 Oct 2024 20:23:44 +0200 Subject: [PATCH 15/22] feat(consensus): push all ingress messages (#2233) Currently, we push ingress messages only if the size of the message is below 1024 bytes (see [link](https://sourcegraph.com/github.com/dfinity/ic/-/blob/rs/p2p/consensus_manager/src/sender.rs?L232-234)), meaning that for large enough ingress messages, a node first has to see an advert and then request to download the advertized ingress message from a peer, before it can include the message in a block, potentially adding a couple hundreds of milliseconds to the end-to-end ingress message latency. The benefits are even bigger in the context of hashes-in-blocks feature, where we rely on existence of ingress messages in the pool when validating blocks received from peers - pushing ingress messages should increase the chances that all the ingress messages referenced by a "stripped block" are already in the ingress pool, so we don't have to fetch them from peers. One downside of always pushing ingress messages could be a wasted bandwith, i.e. node might receive an ingress message from a peer and then immediately discard it because the [bouncer function](https://sourcegraph.com/github.com/dfinity/ic/-/blob/rs/ingress_manager/src/bouncer.rs?L32-40) deemed the ingress message to be not needed (e.g. if the ingress message already expired, from the node point's of view), but this should not be a rather rare occurrence (we actively purge expired ingress messages). --- rs/artifact_pool/src/ingress_pool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/artifact_pool/src/ingress_pool.rs b/rs/artifact_pool/src/ingress_pool.rs index c5c42f8e715..542894e7b47 100644 --- a/rs/artifact_pool/src/ingress_pool.rs +++ b/rs/artifact_pool/src/ingress_pool.rs @@ -286,7 +286,7 @@ impl MutablePool for IngressPoolImpl { if unvalidated_artifact.peer_id == self.node_id { transmits.push(ArtifactTransmit::Deliver(ArtifactWithOpt { artifact: unvalidated_artifact.message.signed_ingress.clone(), - is_latency_sensitive: false, + is_latency_sensitive: true, })); } self.validated.insert( From 34ab896101809abbef6f77fa1e2c180d10b0d5e1 Mon Sep 17 00:00:00 2001 From: Andre Popovitch Date: Thu, 24 Oct 2024 20:32:47 -0500 Subject: [PATCH 16/22] feat(sns): Add simple global lock for the periodic task that relates to upgrades (#2193) ## What This PR adds a lock around checking the upgrade status and refreshing the cached upgrade steps. This ensures that they do not interleave with one another (or themselves). ## Why This is useful because it simplifies our reasoning about any logic that may be impacted by such an interleaving. ## Additional Notes This implementation is not ideal, because we attempt to acquire the lock unconditionally, which means that in cases where the lock was acquired it's impossible to say why. And in the unlikely even that the lock is not released, it would be useful to know what actual work necessitated it be acquired. #2124 builds upon this PR to create a more robust locking mechanism. --- rs/sns/governance/src/governance.rs | 111 ++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 6 deletions(-) diff --git a/rs/sns/governance/src/governance.rs b/rs/sns/governance/src/governance.rs index c8bef9c9bbe..ec71d672646 100644 --- a/rs/sns/governance/src/governance.rs +++ b/rs/sns/governance/src/governance.rs @@ -157,9 +157,13 @@ pub fn log_prefix() -> String { /// The static MEMO used when calculating the SNS Treasury subaccount. pub const TREASURY_SUBACCOUNT_NONCE: u64 = 0; -// How frequently the canister should attempt to refresh the cached_upgrade_steps +/// How frequently the canister should attempt to refresh the cached_upgrade_steps pub const UPGRADE_STEPS_INTERVAL_REFRESH_BACKOFF_SECONDS: u64 = 60 * 60; // 1 hour +/// The maximum duration for which the upgrade periodic task lock may be held. +/// Past this duration, the lock will be automatically released. +const UPGRADE_PERIODIC_TASK_LOCK_TIMEOUT_SECONDS: u64 = 600; + /// Converts bytes to a subaccountpub fn bytes_to_subaccount(bytes: &[u8]) -> Result { pub fn bytes_to_subaccount( bytes: &[u8], @@ -696,6 +700,12 @@ pub struct Governance { /// The number of proposals after the last time "garbage collection" was run. pub latest_gc_num_proposals: usize, + /// Global lock for all periodic tasks that relate to upgrades - this is used to + /// guarantee that they don't interleave with one another outside of rare circumstances (e.g. timeouts). + /// `None` means that the lock is not currently held by any task. + /// `Some(x)` means that a task is has been holding the lock since timestamp `x`. + pub upgrade_periodic_task_lock: Option, + /// Whether test features are enabled. /// Test features should not be exposed in production. But, code that should /// not run in production can be gated behind a check for this flag as an @@ -777,6 +787,7 @@ impl Governance { closest_proposal_deadline_timestamp_seconds: 0, latest_gc_timestamp_seconds: 0, latest_gc_num_proposals: 0, + upgrade_periodic_task_lock: None, test_features_enabled: false, }; @@ -4623,8 +4634,24 @@ impl Governance { self.process_proposals(); - if self.should_check_upgrade_status() { - self.check_upgrade_status().await; + // None of the upgrade-related tasks should interleave with one another or themselves, so we acquire a global + // lock for the duration of their execution. This will return `false` if the lock has already been acquired less + // than 10 minutes ago by a previous invocation of `run_periodic_tasks`, in which case we skip the + // upgrade-related tasks. + if self.acquire_upgrade_periodic_task_lock() { + // We only want to check the upgrade status if we are currently executing an upgrade. + if self.should_check_upgrade_status() { + self.check_upgrade_status().await; + } + + if self.should_refresh_cached_upgrade_steps() { + // We only want to refresh the cached_upgrade_steps every UPGRADE_STEPS_INTERVAL_REFRESH_BACKOFF_SECONDS + // seconds, so we first lock the refresh operation (which will automatically unlock after that interval) + self.temporarily_lock_refresh_cached_upgrade_steps(); + self.refresh_cached_upgrade_steps().await; + } + + self.release_upgrade_periodic_task_lock(); } let should_distribute_rewards = self.should_distribute_rewards(); @@ -4656,13 +4683,32 @@ impl Governance { self.maybe_move_staked_maturity(); self.maybe_gc(); + } - if self.should_refresh_cached_upgrade_steps() { - self.temporarily_lock_refresh_cached_upgrade_steps(); - self.refresh_cached_upgrade_steps().await; + // Acquires the "upgrade periodic task lock" (a lock shared between all periodic tasks that relate to upgrades) + // if it is currently released or was last acquired over UPGRADE_PERIODIC_TASK_LOCK_TIMEOUT_SECONDS ago. + fn acquire_upgrade_periodic_task_lock(&mut self) -> bool { + let now = self.env.now(); + match self.upgrade_periodic_task_lock { + Some(time_acquired) + if now + > time_acquired.saturating_add(UPGRADE_PERIODIC_TASK_LOCK_TIMEOUT_SECONDS) => + { + self.upgrade_periodic_task_lock = Some(now); + true + } + Some(_) => false, + None => { + self.upgrade_periodic_task_lock = Some(now); + true + } } } + fn release_upgrade_periodic_task_lock(&mut self) { + self.upgrade_periodic_task_lock = None; + } + pub fn temporarily_lock_refresh_cached_upgrade_steps(&mut self) { if let Some(ref mut cached_upgrade_steps) = self.proto.cached_upgrade_steps { cached_upgrade_steps.requested_timestamp_seconds = Some(self.env.now()); @@ -8239,6 +8285,59 @@ mod tests { ); } + #[test] + fn test_upgrade_periodic_task_lock() { + let env = NativeEnvironment::new(Some(*TEST_GOVERNANCE_CANISTER_ID)); + let mut gov = Governance::new( + basic_governance_proto().try_into().unwrap(), + Box::new(env), + Box::new(DoNothingLedger {}), + Box::new(DoNothingLedger {}), + Box::new(FakeCmc::new()), + ); + + // The lock is initially None + assert!(gov.upgrade_periodic_task_lock.is_none()); + + // Test acquiring it + assert!(gov.acquire_upgrade_periodic_task_lock()); + assert!(gov.upgrade_periodic_task_lock.is_some()); // the lock is now engaged + assert!(!gov.acquire_upgrade_periodic_task_lock()); // acquiring it twice fails + assert!(!gov.acquire_upgrade_periodic_task_lock()); // acquiring it a third time fails + assert!(gov.upgrade_periodic_task_lock.is_some()); // the lock is still engaged + + // Test releasing it + gov.release_upgrade_periodic_task_lock(); + assert!(gov.upgrade_periodic_task_lock.is_none()); + + // Releasing twice is fine + gov.release_upgrade_periodic_task_lock(); + assert!(gov.upgrade_periodic_task_lock.is_none()); + } + + #[test] + fn test_upgrade_periodic_task_lock_times_out() { + let env = NativeEnvironment::new(Some(*TEST_GOVERNANCE_CANISTER_ID)); + let mut gov = Governance::new( + basic_governance_proto().try_into().unwrap(), + Box::new(env), + Box::new(DoNothingLedger {}), + Box::new(DoNothingLedger {}), + Box::new(FakeCmc::new()), + ); + + assert!(gov.acquire_upgrade_periodic_task_lock()); + assert!(!gov.acquire_upgrade_periodic_task_lock()); + assert!(gov.upgrade_periodic_task_lock.is_some()); + + // advance time + gov.env.set_time_warp(TimeWarp { + delta_s: UPGRADE_PERIODIC_TASK_LOCK_TIMEOUT_SECONDS as i64 + 1, + }); + assert!(gov.acquire_upgrade_periodic_task_lock()); // The lock should successfully be acquired, since the previous one timed out + assert!(!gov.acquire_upgrade_periodic_task_lock()); + } + #[test] fn test_check_upgrade_can_succeed_if_archives_out_of_sync() { let root_canister_id = *TEST_ROOT_CANISTER_ID; From 71d3e2f6b4cd2db3667344f010b175eb8b5db2f7 Mon Sep 17 00:00:00 2001 From: Bas van Dijk Date: Fri, 25 Oct 2024 09:22:13 +0200 Subject: [PATCH 17/22] test(IDX): let team research own the spec_compliance tests (#2249) As discussed with @mraszyk the spec_compliance tests should be owned by team Research since they also own the spec. Additionally IDX should not be in the business of owning tests that check the IC. IDX should only own tests that check the CI and development infrastructure. --- .github/CODEOWNERS | 3 +- Cargo.lock | 11 +- Cargo.toml | 3 +- rs/BUILD.bazel | 2 +- rs/pocket_ic_server/BUILD.bazel | 6 +- rs/pocket_ic_server/Cargo.toml | 2 +- rs/tests/BUILD.bazel | 16 +-- rs/tests/research/BUILD.bazel | 125 ++++++++++++++++++ rs/tests/research/Cargo.toml | 33 +++++ .../spec_compliance/BUILD.bazel | 0 .../spec_compliance/Cargo.toml | 0 .../spec_compliance/spec_compliance.rs | 4 +- ...spec_compliance_application_subnet_test.rs | 1 - ...liance_group_01_application_subnet_test.rs | 1 - ..._compliance_group_01_system_subnet_test.rs | 1 - ..._compliance_group_02_system_subnet_test.rs | 1 - .../spec_compliance_system_subnet_test.rs | 1 - rs/tests/testing_verification/BUILD.bazel | 110 +-------------- rs/tests/testing_verification/Cargo.toml | 21 --- 19 files changed, 182 insertions(+), 159 deletions(-) create mode 100644 rs/tests/research/BUILD.bazel create mode 100644 rs/tests/research/Cargo.toml rename rs/tests/{testing_verification => research}/spec_compliance/BUILD.bazel (100%) rename rs/tests/{testing_verification => research}/spec_compliance/Cargo.toml (100%) rename rs/tests/{testing_verification => research}/spec_compliance/spec_compliance.rs (98%) rename rs/tests/{testing_verification => research}/spec_compliance_application_subnet_test.rs (99%) rename rs/tests/{testing_verification => research}/spec_compliance_group_01_application_subnet_test.rs (99%) rename rs/tests/{testing_verification => research}/spec_compliance_group_01_system_subnet_test.rs (99%) rename rs/tests/{testing_verification => research}/spec_compliance_group_02_system_subnet_test.rs (99%) rename rs/tests/{testing_verification => research}/spec_compliance_system_subnet_test.rs (99%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3e7d4dca9c9..ab77e1384bf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,7 +6,7 @@ /.devcontainer/ @dfinity/idx /buf.yaml @dfinity/ic-message-routing-owners /cpp/ @dfinity/node -/hs/ @dfinity/utopia +/hs/spec_compliance @dfinity/research /licenses/ @dfinity/idx /bin/ict @dfinity/idx /bin/ @dfinity/idx @@ -230,6 +230,7 @@ go_deps.bzl @dfinity/idx /rs/test_utilities/src/cycles_account_manager.rs @dfinity/execution /rs/test_utilities/types/src/batch/ @dfinity/consensus /rs/tests/ @dfinity/idx +/rs/tests/research @dfinity/research @dfinity/idx /rs/tests/dashboards/IC/execution-metrics.json @dfinity/execution @dfinity/idx /rs/tests/dashboards/IC/bitcoin.json @dfinity/execution @dfinity/idx /rs/tests/driver/src/driver/simulate_network.rs @dfinity/networking diff --git a/Cargo.lock b/Cargo.lock index 91cc39c4a32..8f60eaa2b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18103,6 +18103,16 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "research-systests" +version = "0.9.0" +dependencies = [ + "anyhow", + "ic-registry-subnet-type", + "ic-system-test-driver", + "spec-compliance", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -20083,7 +20093,6 @@ dependencies = [ "ic-registry-subnet-type", "ic-system-test-driver", "slog", - "spec-compliance", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bd2a68f8f09..d79b05e8763 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -388,13 +388,14 @@ members = [ "rs/tests/nns/ic_mainnet_nns_recovery", "rs/tests/nns/nns_dapp", "rs/tests/node", + "rs/tests/research", + "rs/tests/research/spec_compliance", "rs/tests/sdk", "rs/tests/test_canisters/http_counter", "rs/tests/test_canisters/kv_store", "rs/tests/test_canisters/message", "rs/tests/testing_verification", "rs/tests/testing_verification/wabt-tests", - "rs/tests/testing_verification/spec_compliance", "rs/tests/testing_verification/testnets", "rs/tla_instrumentation/local_key", "rs/tla_instrumentation/tla_instrumentation", diff --git a/rs/BUILD.bazel b/rs/BUILD.bazel index a0eea36ac62..55160758827 100644 --- a/rs/BUILD.bazel +++ b/rs/BUILD.bazel @@ -31,7 +31,7 @@ # //rs/ic_os/dev_test_tools/launch-single-vm:__pkg__ depends on # //rs/tests/driver:ic-system-test-driver # //rs/pocket_ic_server:__pkg__ depends on -# //rs/tests:ic-hs +# //rs/tests/research:ic-hs # //rs/tests/httpbin-rs:httpbin # //rs/tests/testing_verification:spec_compliance package_group( diff --git a/rs/pocket_ic_server/BUILD.bazel b/rs/pocket_ic_server/BUILD.bazel index 199c6963178..c26db96b33a 100644 --- a/rs/pocket_ic_server/BUILD.bazel +++ b/rs/pocket_ic_server/BUILD.bazel @@ -112,7 +112,7 @@ TEST_DEPENDENCIES = [ ] SPEC_TEST_DEPENDENCIES = TEST_DEPENDENCIES + [ - "//rs/tests/testing_verification/spec_compliance", + "//rs/tests/research/spec_compliance", "//rs/registry/subnet_type", ] @@ -193,13 +193,13 @@ rust_test( ], data = [ ":pocket-ic-server", - "//rs/tests:ic-hs", "//rs/tests/httpbin-rs:httpbin", + "//rs/tests/research:ic-hs", ], env = { "HTTPBIN_BIN": "$(rootpath //rs/tests/httpbin-rs:httpbin)", "POCKET_IC_BIN": "$(rootpath :pocket-ic-server)", - "IC_REF_TEST_ROOT": "rs/tests/ic-hs", + "IC_REF_TEST_ROOT": "rs/tests/research/ic-hs", }, tags = ["cpu:8"], deps = SPEC_TEST_DEPENDENCIES, diff --git a/rs/pocket_ic_server/Cargo.toml b/rs/pocket_ic_server/Cargo.toml index 669dfa73800..5fd8be0ca78 100644 --- a/rs/pocket_ic_server/Cargo.toml +++ b/rs/pocket_ic_server/Cargo.toml @@ -91,5 +91,5 @@ rcgen = { workspace = true } registry-canister = { path = "../registry/canister" } reqwest = { workspace = true } serde_json = { workspace = true } -spec-compliance = { path = "../tests/testing_verification/spec_compliance" } +spec-compliance = { path = "../tests/research/spec_compliance" } slog = { workspace = true } diff --git a/rs/tests/BUILD.bazel b/rs/tests/BUILD.bazel index 86c8cbe73af..63005757e40 100644 --- a/rs/tests/BUILD.bazel +++ b/rs/tests/BUILD.bazel @@ -5,7 +5,7 @@ load("@rules_distroless//apt:defs.bzl", "dpkg_status") load("@rules_distroless//distroless:defs.bzl", "passwd") load("@rules_oci//oci:defs.bzl", "oci_image") load("@rules_rust//rust:defs.bzl", "rust_library") -load("//bazel:defs.bzl", "symlink_dir", "symlink_dir_test", "symlink_dirs") +load("//bazel:defs.bzl", "symlink_dir", "symlink_dir_test") load("//rs/tests:common.bzl", "DEPENDENCIES", "MACRO_DEPENDENCIES") load(":system_tests.bzl", "oci_tar", "uvm_config_image") @@ -386,20 +386,6 @@ run_binary( tool = "//rs/tests/testing_verification/wabt-tests:generator", ) -symlink_dirs( - name = "ic-hs", - target_compatible_with = ["@platforms//os:linux"], - targets = { - "//hs/spec_compliance:ic-ref-test": "bin", - "//rs/universal_canister/impl:universal_canister.wasm.gz": "test-data", - "//rs/tests:wabt-tests": "test-data", - }, - visibility = [ - "//rs:system-tests-pkg", - "//rs/pocket_ic_server:__pkg__", - ], -) - oci_tar( name = "jaeger.tar", image = "@jaeger", diff --git a/rs/tests/research/BUILD.bazel b/rs/tests/research/BUILD.bazel new file mode 100644 index 00000000000..b3f3d7a0c06 --- /dev/null +++ b/rs/tests/research/BUILD.bazel @@ -0,0 +1,125 @@ +load("//bazel:defs.bzl", "symlink_dirs") +load("//rs/tests:common.bzl", "BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS", "CANISTER_HTTP_RUNTIME_DEPS", "GUESTOS_RUNTIME_DEPS", "UNIVERSAL_VM_RUNTIME_DEPS") +load("//rs/tests:system_tests.bzl", "system_test_nns") + +package(default_visibility = ["//rs:system-tests-pkg"]) + +system_test_nns( + name = "spec_compliance_application_subnet_test", + extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. + flaky = True, + tags = [ + "cpu:4", + "long_test", # since it takes longer than 5 minutes. + ], + target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS + runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + [ + ":ic-hs", + "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", + ], + deps = [ + # Keep sorted. + "//rs/registry/subnet_type", + "//rs/tests/driver:ic-system-test-driver", + "//rs/tests/research/spec_compliance", + "@crate_index//:anyhow", + ], +) + +system_test_nns( + name = "spec_compliance_system_subnet_test", + extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. + flaky = True, + tags = [ + "cpu:4", + "long_test", + ], + target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS + runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + [ + ":ic-hs", + "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", + ], + deps = [ + # Keep sorted. + "//rs/registry/subnet_type", + "//rs/tests/driver:ic-system-test-driver", + "//rs/tests/research/spec_compliance", + "@crate_index//:anyhow", + ], +) + +system_test_nns( + name = "spec_compliance_group_01_application_subnet_test", + extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. + flaky = True, + tags = [ + "cpu:4", + "long_test", # since it takes longer than 5 minutes. + ], + target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS + runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + [ + ":ic-hs", + "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", + ], + deps = [ + # Keep sorted. + "//rs/registry/subnet_type", + "//rs/tests/driver:ic-system-test-driver", + "//rs/tests/research/spec_compliance", + "@crate_index//:anyhow", + ], +) + +system_test_nns( + name = "spec_compliance_group_01_system_subnet_test", + extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. + flaky = True, + tags = [ + "cpu:4", + "long_test", + ], + target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS + runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + [ + ":ic-hs", + "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", + ], + deps = [ + # Keep sorted. + "//rs/registry/subnet_type", + "//rs/tests/driver:ic-system-test-driver", + "//rs/tests/research/spec_compliance", + "@crate_index//:anyhow", + ], +) + +system_test_nns( + name = "spec_compliance_group_02_system_subnet_test", + flaky = True, + tags = ["cpu:4"], + target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS + runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + [ + ":ic-hs", + "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", + ], + deps = [ + # Keep sorted. + "//rs/registry/subnet_type", + "//rs/tests/driver:ic-system-test-driver", + "//rs/tests/research/spec_compliance", + "@crate_index//:anyhow", + ], +) + +symlink_dirs( + name = "ic-hs", + target_compatible_with = ["@platforms//os:linux"], + targets = { + "//hs/spec_compliance:ic-ref-test": "bin", + "//rs/universal_canister/impl:universal_canister.wasm.gz": "test-data", + "//rs/tests:wabt-tests": "test-data", + }, + visibility = [ + "//rs:system-tests-pkg", + "//rs/pocket_ic_server:__pkg__", + ], +) diff --git a/rs/tests/research/Cargo.toml b/rs/tests/research/Cargo.toml new file mode 100644 index 00000000000..c900f00256d --- /dev/null +++ b/rs/tests/research/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "research-systests" +version.workspace = true +authors.workspace = true +edition.workspace = true +description.workspace = true +documentation.workspace = true + +[dependencies] +anyhow = { workspace = true } +ic-registry-subnet-type = { path = "../../registry/subnet_type" } +ic-system-test-driver = { path = "../driver" } +spec-compliance = { path = "./spec_compliance" } + +[[bin]] +name = "spec_compliance_application_subnet_test" +path = "spec_compliance_application_subnet_test.rs" + +[[bin]] +name = "spec_compliance_system_subnet_test" +path = "spec_compliance_system_subnet_test.rs" + +[[bin]] +name = "spec_compliance_group_01_application_subnet_testt" +path = "spec_compliance_group_01_application_subnet_test.rs" + +[[bin]] +name = "spec_compliance_group_01_system_subnet_test" +path = "spec_compliance_group_01_system_subnet_test.rs" + +[[bin]] +name = "spec_compliance_group_02_system_subnet_test" +path = "spec_compliance_group_02_system_subnet_test.rs" diff --git a/rs/tests/testing_verification/spec_compliance/BUILD.bazel b/rs/tests/research/spec_compliance/BUILD.bazel similarity index 100% rename from rs/tests/testing_verification/spec_compliance/BUILD.bazel rename to rs/tests/research/spec_compliance/BUILD.bazel diff --git a/rs/tests/testing_verification/spec_compliance/Cargo.toml b/rs/tests/research/spec_compliance/Cargo.toml similarity index 100% rename from rs/tests/testing_verification/spec_compliance/Cargo.toml rename to rs/tests/research/spec_compliance/Cargo.toml diff --git a/rs/tests/testing_verification/spec_compliance/spec_compliance.rs b/rs/tests/research/spec_compliance/spec_compliance.rs similarity index 98% rename from rs/tests/testing_verification/spec_compliance/spec_compliance.rs rename to rs/tests/research/spec_compliance/spec_compliance.rs index 7ae62f2a8a4..3bf9d8ccdd1 100644 --- a/rs/tests/testing_verification/spec_compliance/spec_compliance.rs +++ b/rs/tests/research/spec_compliance/spec_compliance.rs @@ -205,7 +205,7 @@ pub fn test_subnet( } else { None }; - let ic_ref_test_path = get_dependency_path("rs/tests/ic-hs/bin/ic-ref-test") + let ic_ref_test_path = get_dependency_path("rs/tests/research/ic-hs/bin/ic-ref-test") .into_os_string() .into_string() .unwrap(); @@ -331,7 +331,7 @@ pub fn with_endpoint( httpbin_proto, httpbin, ic_ref_test_path, - get_dependency_path("rs/tests/ic-hs/test-data"), + get_dependency_path("rs/tests/research/ic-hs/test-data"), endpoint, test_subnet_config, peer_subnet_config, diff --git a/rs/tests/testing_verification/spec_compliance_application_subnet_test.rs b/rs/tests/research/spec_compliance_application_subnet_test.rs similarity index 99% rename from rs/tests/testing_verification/spec_compliance_application_subnet_test.rs rename to rs/tests/research/spec_compliance_application_subnet_test.rs index 98faeb014a2..6381024c330 100644 --- a/rs/tests/testing_verification/spec_compliance_application_subnet_test.rs +++ b/rs/tests/research/spec_compliance_application_subnet_test.rs @@ -12,7 +12,6 @@ Success:: The ic-ref-test binary does not return an error. end::catalog[] */ use anyhow::Result; - use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; diff --git a/rs/tests/testing_verification/spec_compliance_group_01_application_subnet_test.rs b/rs/tests/research/spec_compliance_group_01_application_subnet_test.rs similarity index 99% rename from rs/tests/testing_verification/spec_compliance_group_01_application_subnet_test.rs rename to rs/tests/research/spec_compliance_group_01_application_subnet_test.rs index 3e3a83913af..2a95ed47270 100644 --- a/rs/tests/testing_verification/spec_compliance_group_01_application_subnet_test.rs +++ b/rs/tests/research/spec_compliance_group_01_application_subnet_test.rs @@ -12,7 +12,6 @@ Success:: The ic-ref-test binary does not return an error. end::catalog[] */ use anyhow::Result; - use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; diff --git a/rs/tests/testing_verification/spec_compliance_group_01_system_subnet_test.rs b/rs/tests/research/spec_compliance_group_01_system_subnet_test.rs similarity index 99% rename from rs/tests/testing_verification/spec_compliance_group_01_system_subnet_test.rs rename to rs/tests/research/spec_compliance_group_01_system_subnet_test.rs index 97d983ac233..12ad376215d 100644 --- a/rs/tests/testing_verification/spec_compliance_group_01_system_subnet_test.rs +++ b/rs/tests/research/spec_compliance_group_01_system_subnet_test.rs @@ -12,7 +12,6 @@ Success:: The ic-ref-test binary does not return an error. end::catalog[] */ use anyhow::Result; - use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; diff --git a/rs/tests/testing_verification/spec_compliance_group_02_system_subnet_test.rs b/rs/tests/research/spec_compliance_group_02_system_subnet_test.rs similarity index 99% rename from rs/tests/testing_verification/spec_compliance_group_02_system_subnet_test.rs rename to rs/tests/research/spec_compliance_group_02_system_subnet_test.rs index 4372ae5a7c2..6242e064ed8 100644 --- a/rs/tests/testing_verification/spec_compliance_group_02_system_subnet_test.rs +++ b/rs/tests/research/spec_compliance_group_02_system_subnet_test.rs @@ -12,7 +12,6 @@ Success:: The ic-ref-test binary does not return an error. end::catalog[] */ use anyhow::Result; - use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; diff --git a/rs/tests/testing_verification/spec_compliance_system_subnet_test.rs b/rs/tests/research/spec_compliance_system_subnet_test.rs similarity index 99% rename from rs/tests/testing_verification/spec_compliance_system_subnet_test.rs rename to rs/tests/research/spec_compliance_system_subnet_test.rs index a047bc4d1c7..f130544afe5 100644 --- a/rs/tests/testing_verification/spec_compliance_system_subnet_test.rs +++ b/rs/tests/research/spec_compliance_system_subnet_test.rs @@ -12,7 +12,6 @@ Success:: The ic-ref-test binary does not return an error. end::catalog[] */ use anyhow::Result; - use ic_registry_subnet_type::SubnetType; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::driver::test_env::TestEnv; diff --git a/rs/tests/testing_verification/BUILD.bazel b/rs/tests/testing_verification/BUILD.bazel index 0e51bd1b5ca..c509cd24577 100644 --- a/rs/tests/testing_verification/BUILD.bazel +++ b/rs/tests/testing_verification/BUILD.bazel @@ -1,14 +1,9 @@ load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") -load("//rs/tests:common.bzl", "BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS", "CANISTER_HTTP_RUNTIME_DEPS", "COUNTER_CANISTER_RUNTIME_DEPS", "GRAFANA_RUNTIME_DEPS", "GUESTOS_RUNTIME_DEPS", "UNIVERSAL_VM_RUNTIME_DEPS") -load("//rs/tests:system_tests.bzl", "system_test", "system_test_nns") +load("//rs/tests:common.bzl", "COUNTER_CANISTER_RUNTIME_DEPS", "GRAFANA_RUNTIME_DEPS", "GUESTOS_RUNTIME_DEPS", "UNIVERSAL_VM_RUNTIME_DEPS") +load("//rs/tests:system_tests.bzl", "system_test") package(default_visibility = ["//rs:system-tests-pkg"]) -IC_HS_RUNTIME_DEPS = [ - # Keep sorted. - "//rs/tests:ic-hs", -] - ALIASES = { "//rs/utils": "utils", } @@ -29,107 +24,6 @@ rust_binary( ], ) -system_test_nns( - name = "spec_compliance_application_subnet_test", - extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. - flaky = True, - tags = [ - "cpu:4", - "long_test", # since it takes longer than 5 minutes. - ], - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + IC_HS_RUNTIME_DEPS + [ - "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", - ], - deps = [ - # Keep sorted. - "//rs/registry/subnet_type", - "//rs/tests/driver:ic-system-test-driver", - "//rs/tests/testing_verification/spec_compliance", - "@crate_index//:anyhow", - ], -) - -system_test_nns( - name = "spec_compliance_system_subnet_test", - extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. - flaky = True, - tags = [ - "cpu:4", - "long_test", - ], - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + IC_HS_RUNTIME_DEPS + [ - "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", - ], - deps = [ - # Keep sorted. - "//rs/registry/subnet_type", - "//rs/tests/driver:ic-system-test-driver", - "//rs/tests/testing_verification/spec_compliance", - "@crate_index//:anyhow", - ], -) - -system_test_nns( - name = "spec_compliance_group_01_application_subnet_test", - extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. - flaky = True, - tags = [ - "cpu:4", - "long_test", # since it takes longer than 5 minutes. - ], - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + IC_HS_RUNTIME_DEPS + [ - "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", - ], - deps = [ - # Keep sorted. - "//rs/registry/subnet_type", - "//rs/tests/driver:ic-system-test-driver", - "//rs/tests/testing_verification/spec_compliance", - "@crate_index//:anyhow", - ], -) - -system_test_nns( - name = "spec_compliance_group_01_system_subnet_test", - extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. - flaky = True, - tags = [ - "cpu:4", - "long_test", - ], - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + IC_HS_RUNTIME_DEPS + [ - "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", - ], - deps = [ - # Keep sorted. - "//rs/registry/subnet_type", - "//rs/tests/driver:ic-system-test-driver", - "//rs/tests/testing_verification/spec_compliance", - "@crate_index//:anyhow", - ], -) - -system_test_nns( - name = "spec_compliance_group_02_system_subnet_test", - flaky = True, - tags = ["cpu:4"], - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = BOUNDARY_NODE_GUESTOS_RUNTIME_DEPS + GUESTOS_RUNTIME_DEPS + UNIVERSAL_VM_RUNTIME_DEPS + CANISTER_HTTP_RUNTIME_DEPS + IC_HS_RUNTIME_DEPS + [ - "//ic-os/components:networking/dev-certs/canister_http_test_ca.cert", - ], - deps = [ - # Keep sorted. - "//rs/registry/subnet_type", - "//rs/tests/driver:ic-system-test-driver", - "//rs/tests/testing_verification/spec_compliance", - "@crate_index//:anyhow", - ], -) - system_test( name = "basic_health_test", flaky = True, diff --git a/rs/tests/testing_verification/Cargo.toml b/rs/tests/testing_verification/Cargo.toml index 115f45ccc2f..0bb99bd06cd 100644 --- a/rs/tests/testing_verification/Cargo.toml +++ b/rs/tests/testing_verification/Cargo.toml @@ -11,28 +11,7 @@ anyhow = { workspace = true } ic-registry-subnet-type = { path = "../../registry/subnet_type" } ic-system-test-driver = { path = "../driver" } slog = { workspace = true } -spec-compliance = { path = "./spec_compliance" } [[bin]] name = "ic-systest-basic-health-test" path = "basic_health_test.rs" - -[[bin]] -name = "ic-systest-spec-compliance-application-subnet" -path = "spec_compliance_application_subnet_test.rs" - -[[bin]] -name = "ic-systest-spec-compliance-system-subnet" -path = "spec_compliance_system_subnet_test.rs" - -[[bin]] -name = "ic-systest-spec-compliance-group-01-application-subnet" -path = "spec_compliance_group_01_application_subnet_test.rs" - -[[bin]] -name = "ic-systest-spec-compliance-group-01-system-subnet" -path = "spec_compliance_group_01_system_subnet_test.rs" - -[[bin]] -name = "ic-systest-spec-compliance-group-02-system-subnet" -path = "spec_compliance_group_02_system_subnet_test.rs" From a00685bd42a1d33e7c8c821b0216cb83f8e6f798 Mon Sep 17 00:00:00 2001 From: oggy-dfin <89794951+oggy-dfin@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:38:48 +0200 Subject: [PATCH 18/22] test(IC-1579): Isolate TLA tests in Rust-based (non-canister) tests (#2241) Switch to a `LocalKey`-based approach for Rust-based tests, and add an annotation (proc macro) that scopes the `LocalKey` to the test. Previously, we used only a global `RwLock`, which would cause traces of one test to be picked up by other tests. In contract, a `LocalKey` is task-local, eliminating the interference. We still use the `RwLock` as a fallback to store traces if the `LocalKey` is not present (i.e., no scope has been opened). This means that, first, tests that aren't annotated still collect the traces, but they aren't checked, which is a bit wasteful but should be OK as these aren't extremely heavy operations. Second, and more importantly, this means that the instrumentation will still work in canister-based tests, allowing a separate query method to pick the traces up from the global variable. --- rs/nns/governance/src/governance.rs | 2 +- rs/nns/governance/src/governance/tla/mod.rs | 29 +++++++-- rs/nns/governance/src/governance/tla/store.rs | 4 +- rs/nns/governance/tests/governance.rs | 37 ++++------- .../tla_instrumentation/src/lib.rs | 2 +- .../tests/multiple_calls.rs | 9 ++- .../tla_instrumentation/tests/structs.rs | 26 ++++++-- .../src/lib.rs | 62 ++++++++++++++++++- 8 files changed, 127 insertions(+), 44 deletions(-) diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 0f8dc5404c1..be098e14eb0 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -135,7 +135,7 @@ pub mod tla; #[cfg(feature = "tla")] pub use tla::{ claim_neuron_desc, split_neuron_desc, tla_update_method, InstrumentationState, ToTla, - TLA_INSTRUMENTATION_STATE, TLA_TRACES, + TLA_INSTRUMENTATION_STATE, TLA_TRACES_LKEY, TLA_TRACES_MUTEX, }; // 70 KB (for executing NNS functions that are not canister upgrades) diff --git a/rs/nns/governance/src/governance/tla/mod.rs b/rs/nns/governance/src/governance/tla/mod.rs index f4c7c8b5e4a..d7dd568b4f5 100644 --- a/rs/nns/governance/src/governance/tla/mod.rs +++ b/rs/nns/governance/src/governance/tla/mod.rs @@ -21,7 +21,7 @@ mod store; pub use common::{account_to_tla, opt_subaccount_to_tla, subaccount_to_tla}; use common::{function_domain_union, governance_account_id}; -pub use store::{TLA_INSTRUMENTATION_STATE, TLA_TRACES}; +pub use store::{TLA_INSTRUMENTATION_STATE, TLA_TRACES_LKEY, TLA_TRACES_MUTEX}; mod split_neuron; pub use split_neuron::split_neuron_desc; @@ -213,10 +213,25 @@ pub fn check_traces() { // improving that later, for now we introduce a hard limit on the state size, and // skip checking states larger than the limit const STATE_SIZE_LIMIT: u64 = 500; + fn is_under_limit(p: &ResolvedStatePair) -> bool { + p.start.size() < STATE_SIZE_LIMIT && p.end.size() < STATE_SIZE_LIMIT + } + + fn print_stats(traces: &Vec) { + println!("Checking {} traces with TLA/Apalache", traces.len()); + for t in traces { + let total_len = t.state_pairs.len(); + let under_limit_len = t.state_pairs.iter().filter(|p| is_under_limit(p)).count(); + println!( + "TLA/Apalache checks: keeping {}/{} states for update {}", + under_limit_len, total_len, t.update.process_id + ); + } + } + let traces = { - // Introduce a scope to drop the write lock immediately, in order - // not to poison the lock if we panic later - let mut t = TLA_TRACES.write().unwrap(); + let t = TLA_TRACES_LKEY.get(); + let mut t = t.borrow_mut(); std::mem::take(&mut (*t)) }; @@ -230,11 +245,13 @@ pub fn check_traces() { panic!("bad apalache bin from 'TLA_APALACHE_BIN': '{:?}'", apalache); } + print_stats(&traces); + let chunk_size = 20; let all_pairs = traces.into_iter().flat_map(|t| { t.state_pairs .into_iter() - .filter(|p| p.start.size() < STATE_SIZE_LIMIT && p.end.size() < STATE_SIZE_LIMIT) + .filter(is_under_limit) .map(move |p| (t.update.clone(), t.constants.clone(), p)) }); let chunks = all_pairs.chunks(chunk_size); @@ -267,7 +284,7 @@ pub fn check_traces() { println!("Possible divergence from the TLA model detected when interacting with the ledger!"); println!("If you did not expect to change the interaction between governance and the ledger, reconsider whether your change is safe. You can find additional data on the step that triggered the error below."); println!("If you are confident that your change is correct, please contact the #formal-models Slack channel and describe the problem."); - println!("You can edit nervous_system/tla/feature_flags.bzl to disable TLA checks in the CI and get on with your business."); + println!("You can edit nns/governance/feature_flags.bzl to disable TLA checks in the CI and get on with your business."); println!("-------------------"); println!("Error occured while checking the state pair:\n{:#?}\nwith constants:\n{:#?}", e.pair, e.constants); println!("Apalache returned:\n{:#?}", e.apalache_error); diff --git a/rs/nns/governance/src/governance/tla/store.rs b/rs/nns/governance/src/governance/tla/store.rs index 78f5008767d..049b9a643d4 100644 --- a/rs/nns/governance/src/governance/tla/store.rs +++ b/rs/nns/governance/src/governance/tla/store.rs @@ -1,4 +1,5 @@ use local_key::task_local; +use std::cell::RefCell; use std::sync::RwLock; pub use tla_instrumentation::{InstrumentationState, UpdateTrace}; @@ -8,7 +9,8 @@ pub use tla_instrumentation::{InstrumentationState, UpdateTrace}; #[cfg(feature = "tla")] task_local! { pub static TLA_INSTRUMENTATION_STATE: InstrumentationState; + pub static TLA_TRACES_LKEY: RefCell>; } #[cfg(feature = "tla")] -pub static TLA_TRACES: RwLock> = RwLock::new(Vec::new()); +pub static TLA_TRACES_MUTEX: RwLock> = RwLock::new(Vec::new()); diff --git a/rs/nns/governance/tests/governance.rs b/rs/nns/governance/tests/governance.rs index 7443b6014ed..1f1dc85f474 100644 --- a/rs/nns/governance/tests/governance.rs +++ b/rs/nns/governance/tests/governance.rs @@ -131,7 +131,9 @@ use std::{ }; #[cfg(feature = "tla")] -use ic_nns_governance::governance::tla; +use ic_nns_governance::governance::tla::{check_traces as tla_check_traces, TLA_TRACES_LKEY}; +#[cfg(feature = "tla")] +use tla_instrumentation_proc_macros::with_tla_trace_check; /// The 'fake' module is the old scheme for providing NNS test fixtures, aka /// the FakeDriver. It is being used here until the older tests have been @@ -4650,6 +4652,7 @@ fn claim_neuron_by_memo( /// Tests that the controller of a neuron (the principal whose hash was used /// to build the subaccount) can claim a neuron just with the memo. #[test] +#[cfg_attr(feature = "tla", with_tla_trace_check)] fn test_claim_neuron_by_memo_only() { let owner = *TEST_NEURON_1_OWNER_PRINCIPAL; let memo = 1234u64; @@ -4669,12 +4672,10 @@ fn test_claim_neuron_by_memo_only() { let neuron = gov.neuron_store.with_neuron(&nid, |n| n.clone()).unwrap(); assert_eq!(neuron.controller(), owner); assert_eq!(neuron.cached_neuron_stake_e8s, stake.get_e8s()); - - #[cfg(feature = "tla")] - tla::check_traces(); } #[test] +#[cfg_attr(feature = "tla", with_tla_trace_check)] fn test_claim_neuron_without_minimum_stake_fails() { let owner = *TEST_NEURON_1_OWNER_PRINCIPAL; let memo = 1234u64; @@ -4692,9 +4693,6 @@ fn test_claim_neuron_without_minimum_stake_fails() { } _ => panic!("Invalid response."), }; - - #[cfg(feature = "tla")] - tla::check_traces(); } fn do_test_claim_neuron_by_memo_and_controller(owner: PrincipalId, caller: PrincipalId) { @@ -4731,14 +4729,12 @@ fn do_test_claim_neuron_by_memo_and_controller(owner: PrincipalId, caller: Princ let neuron = gov.neuron_store.with_neuron(&nid, |n| n.clone()).unwrap(); assert_eq!(neuron.controller(), owner); assert_eq!(neuron.cached_neuron_stake_e8s, stake.get_e8s()); - - #[cfg(feature = "tla")] - tla::check_traces(); } /// Like the above, but explicitly sets the controller in the MemoAndController /// struct. #[test] +#[cfg_attr(feature = "tla", with_tla_trace_check)] fn test_claim_neuron_memo_and_controller_by_controller() { let owner = *TEST_NEURON_1_OWNER_PRINCIPAL; do_test_claim_neuron_by_memo_and_controller(owner, owner); @@ -4747,6 +4743,7 @@ fn test_claim_neuron_memo_and_controller_by_controller() { /// Tests that a non-controller can claim a neuron for the controller (the /// principal whose id was used to build the subaccount). #[test] +#[cfg_attr(feature = "tla", with_tla_trace_check)] fn test_claim_neuron_memo_and_controller_by_proxy() { let owner = *TEST_NEURON_1_OWNER_PRINCIPAL; let caller = *TEST_NEURON_2_OWNER_PRINCIPAL; @@ -4755,6 +4752,7 @@ fn test_claim_neuron_memo_and_controller_by_proxy() { /// Tests that a non-controller can't claim a neuron for themselves. #[test] +#[cfg_attr(feature = "tla", with_tla_trace_check)] fn test_non_controller_cant_claim_neuron_for_themselves() { let owner = *TEST_NEURON_1_OWNER_PRINCIPAL; let claimer = *TEST_NEURON_2_OWNER_PRINCIPAL; @@ -4783,9 +4781,6 @@ fn test_non_controller_cant_claim_neuron_for_themselves() { CommandResponse::Error(_) => (), _ => panic!("Claim should have failed."), }; - - #[cfg(feature = "tla")] - tla::check_traces(); } fn refresh_neuron_by_memo(owner: PrincipalId, caller: PrincipalId) { @@ -4953,6 +4948,7 @@ fn test_refresh_neuron_by_subaccount_by_proxy() { } #[test] +#[cfg_attr(feature = "tla", with_tla_trace_check)] fn test_claim_or_refresh_neuron_does_not_overflow() { let (mut driver, mut gov, neuron) = create_mature_neuron(true); let nid = neuron.id.unwrap(); @@ -5017,9 +5013,6 @@ fn test_claim_or_refresh_neuron_does_not_overflow() { .stake_e8s, previous_stake_e8s + 100_000_000_000_000 ); - - #[cfg(feature = "tla")] - tla::check_traces(); } #[test] @@ -5254,6 +5247,7 @@ fn test_cant_disburse_without_paying_fees() { /// * the list of all neurons remained unchanged /// * the list of accounts is unchanged #[test] +#[cfg_attr(feature = "tla", with_tla_trace_check)] fn test_neuron_split_fails() { let from = *TEST_NEURON_1_OWNER_PRINCIPAL; // Compute the subaccount to which the transfer would have been made @@ -5376,12 +5370,10 @@ fn test_neuron_split_fails() { assert_eq!(gov.neuron_store.len(), 1); // There is still only one ledger account. driver.assert_num_neuron_accounts_exist(1); - - #[cfg(feature = "tla")] - tla::check_traces(); } #[test] +#[cfg_attr(feature = "tla", with_tla_trace_check)] fn test_neuron_split() { let from = *TEST_NEURON_1_OWNER_PRINCIPAL; // Compute the subaccount to which the transfer would have been made @@ -5482,12 +5474,10 @@ fn test_neuron_split() { let mut expected_neuron_ids = vec![id, child_nid]; expected_neuron_ids.sort_unstable(); assert_eq!(neuron_ids, expected_neuron_ids); - - #[cfg(feature = "tla")] - tla::check_traces(); } #[test] +#[cfg_attr(feature = "tla", with_tla_trace_check)] fn test_seed_neuron_split() { let from = *TEST_NEURON_1_OWNER_PRINCIPAL; // Compute the subaccount to which the transfer would have been made @@ -5558,9 +5548,6 @@ fn test_seed_neuron_split() { assert_eq!(child_neuron.dissolve_state, parent_neuron.dissolve_state); assert_eq!(child_neuron.kyc_verified, true); assert_eq!(child_neuron.neuron_type, Some(NeuronType::Seed as i32)); - - #[cfg(feature = "tla")] - tla::check_traces(); } // Spawn neurons has the least priority in the periodic tasks, so we need to run diff --git a/rs/tla_instrumentation/tla_instrumentation/src/lib.rs b/rs/tla_instrumentation/tla_instrumentation/src/lib.rs index 6b754008bcd..59eb8230bc5 100644 --- a/rs/tla_instrumentation/tla_instrumentation/src/lib.rs +++ b/rs/tla_instrumentation/tla_instrumentation/src/lib.rs @@ -41,7 +41,7 @@ pub struct Update { pub post_process: fn(&mut Vec) -> TlaConstantAssignment, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct UpdateTrace { pub update: Update, pub state_pairs: Vec, diff --git a/rs/tla_instrumentation/tla_instrumentation/tests/multiple_calls.rs b/rs/tla_instrumentation/tla_instrumentation/tests/multiple_calls.rs index 9430adb11a5..a6e0f0e189f 100644 --- a/rs/tla_instrumentation/tla_instrumentation/tests/multiple_calls.rs +++ b/rs/tla_instrumentation/tla_instrumentation/tests/multiple_calls.rs @@ -35,9 +35,10 @@ mod tla_stuff { task_local! { pub static TLA_INSTRUMENTATION_STATE: InstrumentationState; + pub static TLA_TRACES_LKEY: std::cell::RefCell>; } - pub static TLA_TRACES: RwLock> = RwLock::new(Vec::new()); + pub static TLA_TRACES_MUTEX: RwLock> = RwLock::new(Vec::new()); pub fn tla_get_globals(c: &StructCanister) -> GlobalState { let mut state = GlobalState::new(); @@ -110,7 +111,9 @@ mod tla_stuff { } } -use tla_stuff::{my_f_desc, CAN_NAME, PID, TLA_INSTRUMENTATION_STATE, TLA_TRACES}; +use tla_stuff::{ + my_f_desc, CAN_NAME, PID, TLA_INSTRUMENTATION_STATE, TLA_TRACES_LKEY, TLA_TRACES_MUTEX, +}; struct StructCanister { pub counter: u64, @@ -162,7 +165,7 @@ fn multiple_calls_test() { let canister = &mut *addr_of_mut!(GLOBAL); tokio_test::block_on(canister.my_method()); } - let trace = &TLA_TRACES.read().unwrap()[0]; + let trace = &TLA_TRACES_MUTEX.read().unwrap()[0]; assert_eq!( trace.constants.to_map().get("MAX_COUNTER"), Some(&3_u64.to_string()) diff --git a/rs/tla_instrumentation/tla_instrumentation/tests/structs.rs b/rs/tla_instrumentation/tla_instrumentation/tests/structs.rs index 9cae519c109..27843d7e0a8 100644 --- a/rs/tla_instrumentation/tla_instrumentation/tests/structs.rs +++ b/rs/tla_instrumentation/tla_instrumentation/tests/structs.rs @@ -10,7 +10,7 @@ use tla_instrumentation::{ tla_value::{TlaValue, ToTla}, Destination, InstrumentationState, }; -use tla_instrumentation_proc_macros::tla_update_method; +use tla_instrumentation_proc_macros::{tla_update_method, with_tla_trace_check}; mod common; use common::check_tla_trace; @@ -35,9 +35,10 @@ mod tla_stuff { task_local! { pub static TLA_INSTRUMENTATION_STATE: InstrumentationState; + pub static TLA_TRACES_LKEY: std::cell::RefCell>; } - pub static TLA_TRACES: RwLock> = RwLock::new(Vec::new()); + pub static TLA_TRACES_MUTEX: RwLock> = RwLock::new(Vec::new()); pub fn tla_get_globals(c: &StructCanister) -> GlobalState { let mut state = GlobalState::new(); @@ -110,7 +111,9 @@ mod tla_stuff { } } -use tla_stuff::{my_f_desc, CAN_NAME, PID, TLA_INSTRUMENTATION_STATE, TLA_TRACES}; +use tla_stuff::{ + my_f_desc, CAN_NAME, PID, TLA_INSTRUMENTATION_STATE, TLA_TRACES_LKEY, TLA_TRACES_MUTEX, +}; struct StructCanister { pub counter: u64, @@ -155,7 +158,7 @@ fn struct_test() { let canister = &mut *addr_of_mut!(GLOBAL); tokio_test::block_on(canister.my_method()); } - let trace = &TLA_TRACES.read().unwrap()[0]; + let trace = &TLA_TRACES_MUTEX.read().unwrap()[0]; assert_eq!( trace.constants.to_map().get("MAX_COUNTER"), Some(&2_u64.to_string()) @@ -263,3 +266,18 @@ fn struct_test() { check_tla_trace(trace); } + +fn tla_check_traces() { + let traces = TLA_TRACES_LKEY.get(); + let traces = traces.borrow(); + for t in &*traces { + check_tla_trace(t) + } +} + +#[test] +#[with_tla_trace_check] +fn annotated_test() { + let canister = &mut StructCanister { counter: 0 }; + tokio_test::block_on(canister.my_method()); +} diff --git a/rs/tla_instrumentation/tla_instrumentation_proc_macros/src/lib.rs b/rs/tla_instrumentation/tla_instrumentation_proc_macros/src/lib.rs index 3d5fd641186..2d466ad0f3d 100644 --- a/rs/tla_instrumentation/tla_instrumentation_proc_macros/src/lib.rs +++ b/rs/tla_instrumentation/tla_instrumentation_proc_macros/src/lib.rs @@ -76,6 +76,16 @@ pub fn tla_update(attr: TokenStream, item: TokenStream) -> TokenStream { output.into() } +/// Marks the method as the starting point of a TLA transition (or more concretely, a PlusCal process). +/// Assumes that the following are in scope: +/// 1. TLA_INSTRUMENTATION_STATE LocalKey storing a Rc> +/// 2. TLA_TRACES_MUTEX RwLock storing a Vec +/// 3. TLA_TRACES_LKEY LocalKey storing a RefCell> +/// 4. tla_get_globals! a macro which takes a self parameter iff this is a method +/// 5. tla_instrumentation crate +/// +/// It records the trace (sequence of states) resulting from `tla_log_request!` and `tla_log_response!` +/// macro calls in either the #[proc_macro_attribute] pub fn tla_update_method(attr: TokenStream, item: TokenStream) -> TokenStream { // Parse the input tokens of the attribute and the function @@ -146,12 +156,21 @@ pub fn tla_update_method(attr: TokenStream, item: TokenStream) -> TokenStream { let mut pairs = trace.state_pairs.borrow_mut().clone(); let constants = (update.post_process)(&mut pairs); // println!("State pairs in the expanded macro: {:?}", pairs); - let mut traces = TLA_TRACES.write().unwrap(); - traces.push(tla_instrumentation::UpdateTrace { + let trace = tla_instrumentation::UpdateTrace { update, state_pairs: pairs, constants, - } ); + }; + match TLA_TRACES_LKEY.try_with(|t| { + let mut traces = t.borrow_mut(); + traces.push(trace.clone()); + }) { + Ok(_) => (), + Err(_) => { + let mut traces = TLA_TRACES_MUTEX.write().unwrap(); + traces.push(trace); + }, + } res } } @@ -249,3 +268,40 @@ pub fn tla_function(_attr: TokenStream, item: TokenStream) -> TokenStream { output.into() } + +/// An annotation for tests whose TLA traces should be checked. +/// Assumes that the following are in scope: +/// 1. a LocalKey variable `TLA_TRACES_LKEY` of type Vec,and +/// 2. a function tla_check_traces() (presumably looking at the `TLA_TRACES_LKEY` +#[proc_macro_attribute] +pub fn with_tla_trace_check(_attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the input tokens of the attribute and the function + let input_fn = parse_macro_input!(item as ItemFn); + + let mut modified_fn = input_fn.clone(); + + // Deconstruct the function elements + let ItemFn { + attrs, + vis, + sig, + block: _, + } = input_fn; + + let mangled_name = syn::Ident::new(&format!("_tla_check_impl_{}", sig.ident), sig.ident.span()); + modified_fn.sig.ident = mangled_name.clone(); + let args: Vec<_> = sig.inputs.iter().collect(); + + let output = quote! { + #modified_fn + + #(#attrs)* #vis #sig { + TLA_TRACES_LKEY.sync_scope(std::cell::RefCell::new(Vec::new()), || { + let res = #mangled_name(#(#args),*); + tla_check_traces(); + res + }) + } + }; + output.into() +} From 412669657fc277a56bd5d1578fbd944a033a09ae Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Fri, 25 Oct 2024 12:21:40 +0200 Subject: [PATCH 19/22] chore(sns): Make SNS Governance use common timer-related API types (#2211) This PR unifies the timer API in SNS Governance and Swap. < [Previous PR](https://github.com/dfinity/ic/pull/2204) | [Next PR](https://github.com/dfinity/ic/pull/2214) > --- rs/sns/governance/canister/canister.rs | 12 ++-- rs/sns/governance/canister/governance.did | 1 + .../governance/canister/governance_test.did | 1 + .../ic_sns_governance/pb/v1/governance.proto | 15 +---- .../src/gen/ic_sns_governance.pb.v1.rs | 60 +------------------ rs/sns/integration_tests/src/timers.rs | 38 ++++++------ 6 files changed, 32 insertions(+), 95 deletions(-) diff --git a/rs/sns/governance/canister/canister.rs b/rs/sns/governance/canister/canister.rs index 78cbaf03b81..7c7742c08f8 100644 --- a/rs/sns/governance/canister/canister.rs +++ b/rs/sns/governance/canister/canister.rs @@ -24,6 +24,9 @@ use ic_nervous_system_common::{ dfn_core_stable_mem_utils::{BufferedStableMemReader, BufferedStableMemWriter}, serve_logs, serve_logs_v2, serve_metrics, }; +use ic_nervous_system_proto::pb::v1::{ + GetTimersRequest, GetTimersResponse, ResetTimersRequest, ResetTimersResponse, Timers, +}; use ic_nervous_system_runtime::CdkRuntime; use ic_nns_constants::LEDGER_CANISTER_ID as NNS_LEDGER_CANISTER_ID; #[cfg(feature = "test")] @@ -43,11 +46,10 @@ use ic_sns_governance::{ GetModeResponse, GetNeuron, GetNeuronResponse, GetProposal, GetProposalResponse, GetRunningSnsVersionRequest, GetRunningSnsVersionResponse, GetSnsInitializationParametersRequest, GetSnsInitializationParametersResponse, - GetTimersRequest, GetTimersResponse, GetUpgradeJournalRequest, GetUpgradeJournalResponse, - Governance as GovernanceProto, ListNervousSystemFunctionsResponse, ListNeurons, - ListNeuronsResponse, ListProposals, ListProposalsResponse, ManageNeuron, - ManageNeuronResponse, NervousSystemParameters, ResetTimersRequest, ResetTimersResponse, - RewardEvent, SetMode, SetModeResponse, Timers, + GetUpgradeJournalRequest, GetUpgradeJournalResponse, Governance as GovernanceProto, + ListNervousSystemFunctionsResponse, ListNeurons, ListNeuronsResponse, ListProposals, + ListProposalsResponse, ManageNeuron, ManageNeuronResponse, NervousSystemParameters, + RewardEvent, SetMode, SetModeResponse, }, types::{Environment, HeapGrowthPotential}, }; diff --git a/rs/sns/governance/canister/governance.did b/rs/sns/governance/canister/governance.did index 396f00ffac8..dcadb5f603c 100644 --- a/rs/sns/governance/canister/governance.did +++ b/rs/sns/governance/canister/governance.did @@ -295,6 +295,7 @@ type Governance = record { }; type Timers = record { + requires_periodic_tasks : opt bool; last_reset_timestamp_seconds : opt nat64; last_spawned_timestamp_seconds : opt nat64; }; diff --git a/rs/sns/governance/canister/governance_test.did b/rs/sns/governance/canister/governance_test.did index fcf9e75015e..965b384e4fc 100644 --- a/rs/sns/governance/canister/governance_test.did +++ b/rs/sns/governance/canister/governance_test.did @@ -304,6 +304,7 @@ type Governance = record { }; type Timers = record { + requires_periodic_tasks : opt bool; last_reset_timestamp_seconds : opt nat64; last_spawned_timestamp_seconds : opt nat64; }; diff --git a/rs/sns/governance/proto/ic_sns_governance/pb/v1/governance.proto b/rs/sns/governance/proto/ic_sns_governance/pb/v1/governance.proto index 25b2b917e70..8d7544919e8 100644 --- a/rs/sns/governance/proto/ic_sns_governance/pb/v1/governance.proto +++ b/rs/sns/governance/proto/ic_sns_governance/pb/v1/governance.proto @@ -1472,20 +1472,7 @@ message Governance { CachedUpgradeSteps cached_upgrade_steps = 29; // Information about the timers that perform periodic tasks of this Governance canister. - optional Timers timers = 31; -} - -message Timers { - optional uint64 last_reset_timestamp_seconds = 1; - optional uint64 last_spawned_timestamp_seconds = 2; -} - -message ResetTimersRequest {} -message ResetTimersResponse {} - -message GetTimersRequest {} -message GetTimersResponse { - optional Timers timers = 1; + optional ic_nervous_system.pb.v1.Timers timers = 31; } // Request message for 'get_metadata'. diff --git a/rs/sns/governance/src/gen/ic_sns_governance.pb.v1.rs b/rs/sns/governance/src/gen/ic_sns_governance.pb.v1.rs index 4fd45e35232..c926e3465df 100644 --- a/rs/sns/governance/src/gen/ic_sns_governance.pb.v1.rs +++ b/rs/sns/governance/src/gen/ic_sns_governance.pb.v1.rs @@ -1649,7 +1649,7 @@ pub struct Governance { pub cached_upgrade_steps: ::core::option::Option, /// Information about the timers that perform periodic tasks of this Governance canister. #[prost(message, optional, tag = "31")] - pub timers: ::core::option::Option, + pub timers: ::core::option::Option<::ic_nervous_system_proto::pb::v1::Timers>, } /// Nested message and enum types in `Governance`. pub mod governance { @@ -1996,64 +1996,6 @@ pub mod governance { } } } -#[derive( - candid::CandidType, - candid::Deserialize, - comparable::Comparable, - Clone, - Copy, - PartialEq, - ::prost::Message, -)] -pub struct Timers { - #[prost(uint64, optional, tag = "1")] - pub last_reset_timestamp_seconds: ::core::option::Option, - #[prost(uint64, optional, tag = "2")] - pub last_spawned_timestamp_seconds: ::core::option::Option, -} -#[derive( - candid::CandidType, - candid::Deserialize, - comparable::Comparable, - Clone, - Copy, - PartialEq, - ::prost::Message, -)] -pub struct ResetTimersRequest {} -#[derive( - candid::CandidType, - candid::Deserialize, - comparable::Comparable, - Clone, - Copy, - PartialEq, - ::prost::Message, -)] -pub struct ResetTimersResponse {} -#[derive( - candid::CandidType, - candid::Deserialize, - comparable::Comparable, - Clone, - Copy, - PartialEq, - ::prost::Message, -)] -pub struct GetTimersRequest {} -#[derive( - candid::CandidType, - candid::Deserialize, - comparable::Comparable, - Clone, - Copy, - PartialEq, - ::prost::Message, -)] -pub struct GetTimersResponse { - #[prost(message, optional, tag = "1")] - pub timers: ::core::option::Option, -} /// Request message for 'get_metadata'. #[derive( candid::CandidType, diff --git a/rs/sns/integration_tests/src/timers.rs b/rs/sns/integration_tests/src/timers.rs index 5e30138e867..e487101fbba 100644 --- a/rs/sns/integration_tests/src/timers.rs +++ b/rs/sns/integration_tests/src/timers.rs @@ -1,8 +1,10 @@ use assert_matches::assert_matches; use candid::{Decode, Encode, Principal}; -use ic_nervous_system_proto::pb::v1::{ResetTimersRequest, ResetTimersResponse, Timers}; +use ic_nervous_system_proto::pb::v1::{ + GetTimersRequest, GetTimersResponse, ResetTimersRequest, ResetTimersResponse, Timers, +}; use ic_nns_test_utils::sns_wasm::build_governance_sns_wasm; -use ic_sns_governance::{init::GovernanceCanisterInitPayloadBuilder, pb::v1 as governance_pb}; +use ic_sns_governance::{init::GovernanceCanisterInitPayloadBuilder, pb::v1::Governance}; use ic_sns_swap::pb::v1::{ GetStateRequest, GetStateResponse, Init, Lifecycle, NeuronBasketConstructionParameters, }; @@ -49,7 +51,7 @@ fn swap_init(now: SystemTime) -> Init { } } -fn governance_proto() -> governance_pb::Governance { +fn governance_proto() -> Governance { GovernanceCanisterInitPayloadBuilder::new() .with_root_canister_id(PrincipalId::new_anonymous()) .with_ledger_canister_id(PrincipalId::new_anonymous()) @@ -237,18 +239,19 @@ fn test_governance_reset_timers() { // Helpers. let get_timers = || { - let payload = Encode!(&governance_pb::GetTimersRequest {}).unwrap(); + let payload = Encode!(&GetTimersRequest {}).unwrap(); let response = state_machine .execute_ingress(canister_id, "get_timers", payload) .expect("Unable to call get_timers on the Governance canister"); - let response = Decode!(&response.bytes(), governance_pb::GetTimersResponse).unwrap(); + let response = Decode!(&response.bytes(), GetTimersResponse).unwrap(); response.timers }; let last_spawned_timestamp_seconds = { - let last_reset_timestamp_seconds = assert_matches!(get_timers(), Some(governance_pb::Timers { + let last_reset_timestamp_seconds = assert_matches!(get_timers(), Some(Timers { last_reset_timestamp_seconds: Some(last_reset_timestamp_seconds), last_spawned_timestamp_seconds: None, + .. }) => last_reset_timestamp_seconds); // Resetting the timers cannot be done sooner than `RESET_TIMERS_COOL_DOWN_INTERVAL` after @@ -256,9 +259,10 @@ fn test_governance_reset_timers() { state_machine.advance_time(Duration::from_secs(1000)); state_machine.tick(); - let last_spawned_timestamp_seconds = assert_matches!(get_timers(), Some(governance_pb::Timers { + let last_spawned_timestamp_seconds = assert_matches!(get_timers(), Some(Timers { last_reset_timestamp_seconds: Some(last_reset_timestamp_seconds_1), last_spawned_timestamp_seconds: Some(last_spawned_timestamp_seconds), + .. }) => { assert_eq!(last_reset_timestamp_seconds_1, last_reset_timestamp_seconds); last_spawned_timestamp_seconds @@ -284,9 +288,10 @@ fn test_governance_reset_timers() { { let last_spawned_before_reset_timestamp_seconds = last_spawned_timestamp_seconds; - let last_reset_timestamp_seconds = assert_matches!(get_timers(), Some(governance_pb::Timers { + let last_reset_timestamp_seconds = assert_matches!(get_timers(), Some(Timers { last_reset_timestamp_seconds: Some(last_reset_timestamp_seconds), last_spawned_timestamp_seconds: None, + .. }) => last_reset_timestamp_seconds); // last_spawned_before_reset_timestamp_seconds is from before the reset, as time did not yet @@ -299,9 +304,10 @@ fn test_governance_reset_timers() { state_machine.advance_time(Duration::from_secs(100)); state_machine.tick(); - let last_spawned_timestamp_seconds = assert_matches!(get_timers(), Some(governance_pb::Timers { + let last_spawned_timestamp_seconds = assert_matches!(get_timers(), Some(Timers { last_reset_timestamp_seconds: Some(last_reset_timestamp_seconds_1), last_spawned_timestamp_seconds: Some(last_spawned_timestamp_seconds), + .. }) => { assert_eq!(last_reset_timestamp_seconds_1, last_reset_timestamp_seconds); last_spawned_timestamp_seconds @@ -405,13 +411,11 @@ fn test_governance_reset_timers_cannot_be_spammed() { .unwrap(); // Helpers. - let try_reset_timers = || -> Result { - let payload = Encode!(&governance_pb::ResetTimersRequest {}).unwrap(); + let try_reset_timers = || -> Result { + let payload = Encode!(&ResetTimersRequest {}).unwrap(); let response = state_machine.execute_ingress(canister_id, "reset_timers", payload); match response { - Ok(response) => { - Ok(Decode!(&response.bytes(), governance_pb::ResetTimersResponse).unwrap()) - } + Ok(response) => Ok(Decode!(&response.bytes(), ResetTimersResponse).unwrap()), Err(err) => Err(err.to_string()), } }; @@ -421,15 +425,15 @@ fn test_governance_reset_timers_cannot_be_spammed() { let get_last_spawned_timestamp_seconds = || { let timers = { - let payload = Encode!(&governance_pb::GetTimersRequest {}).unwrap(); + let payload = Encode!(&GetTimersRequest {}).unwrap(); let response = state_machine .execute_ingress(canister_id, "get_timers", payload) .expect("Unable to call get_state on the Governance canister"); - let response = Decode!(&response.bytes(), governance_pb::GetTimersResponse).unwrap(); + let response = Decode!(&response.bytes(), GetTimersResponse).unwrap(); response.timers }; - let last_reset_timestamp_seconds = assert_matches!(timers, Some(governance_pb::Timers { + let last_reset_timestamp_seconds = assert_matches!(timers, Some(Timers { last_reset_timestamp_seconds: Some(last_reset_timestamp_seconds), .. }) => last_reset_timestamp_seconds); From 90c685f376f5462979e6fdcf13415cce37d25b02 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:32:26 +0200 Subject: [PATCH 20/22] chore: use WASM chunk store in spec compliance tests (#2251) This PR uses the WASM chunk store to deploy the universal canister in spec compliance tests so that the universal canister WASM blob is not sent raw every time we deploy the universal canister. This optimization is supposed to save ingress message bandwidth and thereby optimize the spec compliance test runtime. --- hs/spec_compliance/bin/ic-ref-test.hs | 3 +- hs/spec_compliance/src/IC/Management.hs | 8 + hs/spec_compliance/src/IC/Test/Agent.hs | 12 +- .../src/IC/Test/Agent/SafeCalls.hs | 20 + .../src/IC/Test/Agent/UnsafeCalls.hs | 32 + hs/spec_compliance/src/IC/Test/Spec.hs | 5363 +++++++++-------- .../src/IC/Test/Spec/CanisterHistory.hs | 18 +- hs/spec_compliance/src/IC/Test/Spec/Timer.hs | 39 +- hs/spec_compliance/src/IC/Test/Spec/Utils.hs | 42 +- rs/pocket_ic_server/tests/spec_test.rs | 2 +- 10 files changed, 2812 insertions(+), 2727 deletions(-) diff --git a/hs/spec_compliance/bin/ic-ref-test.hs b/hs/spec_compliance/bin/ic-ref-test.hs index d7236041cdc..848e00292b2 100644 --- a/hs/spec_compliance/bin/ic-ref-test.hs +++ b/hs/spec_compliance/bin/ic-ref-test.hs @@ -26,7 +26,8 @@ main = do ac <- preFlight os let TestSubnet my_sub = lookupOption os let PeerSubnet other_sub = lookupOption os - defaultMainWithIngredients ingredients (icTests my_sub other_sub ac) + tests <- icTests my_sub other_sub ac + defaultMainWithIngredients ingredients tests where ingredients = [ rerunningTests diff --git a/hs/spec_compliance/src/IC/Management.hs b/hs/spec_compliance/src/IC/Management.hs index 21c95491dde..07ecdbe9d63 100644 --- a/hs/spec_compliance/src/IC/Management.hs +++ b/hs/spec_compliance/src/IC/Management.hs @@ -62,6 +62,14 @@ type InstallMode = V.Var ("install" R..== () R..+ "reinstall" R..== () R..+ "upg type InstallCodeArgs = R.Rec ("mode" R..== InstallMode R..+ "canister_id" R..== Principal R..+ "wasm_module" R..== Blob R..+ "arg" R..== Blob R..+ "sender_canister_version" R..== Maybe W.Word64) +-- Canister installation using WASM chunks + +type ChunkHash = R.Rec ("hash" R..== Blob) + +type InstallChunkedCodeArgs = R.Rec ("mode" R..== InstallMode R..+ "target_canister" R..== Principal R..+ "store_canister" R..== Principal R..+ "chunk_hashes_list" R..== Vec.Vector ChunkHash R..+ "wasm_module_hash" R..== Blob R..+ "arg" R..== Blob R..+ "sender_canister_version" R..== Maybe W.Word64) + +type UploadChunkArgs = R.Rec ("canister_id" R..== Principal R..+ "chunk" R..== Blob) + -- Canister history type CandidChangeFromUser = R.Rec ("user_id" R..== Principal) diff --git a/hs/spec_compliance/src/IC/Test/Agent.hs b/hs/spec_compliance/src/IC/Test/Agent.hs index dee505a95d6..98e3cd022fb 100644 --- a/hs/spec_compliance/src/IC/Test/Agent.hs +++ b/hs/spec_compliance/src/IC/Test/Agent.hs @@ -224,7 +224,9 @@ data AgentConfig = AgentConfig tc_subnets :: [AgentSubnetConfig], tc_httpbin_proto :: String, tc_httpbin :: String, - tc_timeout :: Int + tc_timeout :: Int, + tc_ucan_chunk_hash :: Maybe Blob, + tc_store_canister_id :: Maybe Blob } makeAgentConfig :: Bool -> String -> [AgentSubnetConfig] -> String -> String -> Int -> IO AgentConfig @@ -258,7 +260,9 @@ makeAgentConfig allow_self_signed_certs ep' subnets httpbin_proto httpbin' to = tc_subnets = subnets, tc_httpbin_proto = httpbin_proto, tc_httpbin = httpbin, - tc_timeout = to + tc_timeout = to, + tc_ucan_chunk_hash = Nothing, + tc_store_canister_id = Nothing } where -- strip trailing slash @@ -288,8 +292,8 @@ preFlight os = do type HasAgentConfig = (?agentConfig :: AgentConfig) -withAgentConfig :: (forall. (HasAgentConfig) => a) -> AgentConfig -> a -withAgentConfig act tc = let ?agentConfig = tc in act +withAgentConfig :: AgentConfig -> (forall. (HasAgentConfig) => a) -> a +withAgentConfig tc act = let ?agentConfig = tc in act agentConfig :: (HasAgentConfig) => AgentConfig agentConfig = ?agentConfig diff --git a/hs/spec_compliance/src/IC/Test/Agent/SafeCalls.hs b/hs/spec_compliance/src/IC/Test/Agent/SafeCalls.hs index 2694826004a..cb690d3ec78 100644 --- a/hs/spec_compliance/src/IC/Test/Agent/SafeCalls.hs +++ b/hs/spec_compliance/src/IC/Test/Agent/SafeCalls.hs @@ -25,6 +25,7 @@ module IC.Test.Agent.SafeCalls ic_http_head_request', ic_long_url_http_request', ic_install', + ic_install_single_chunk', ic_install_with_sender_canister_version', ic_provisional_create', ic_provisional_create_with_sender_canister_version', @@ -159,6 +160,25 @@ ic_install_with_sender_canister_version' ic00 mode canister_id wasm_module arg s ic_install' :: (HasAgentConfig) => IC00 -> InstallMode -> Blob -> Blob -> Blob -> IO ReqResponse ic_install' ic00 mode canister_id wasm_module arg = ic_install_with_sender_canister_version' ic00 mode canister_id wasm_module arg Nothing +ic_install_single_chunk' :: (HasCallStack, HasAgentConfig) => IC00 -> InstallMode -> Blob -> Blob -> Blob -> Blob -> IO ReqResponse +ic_install_single_chunk' ic00 mode target_canister store_canister chunk_hash arg = do + callIC' ic00 target_canister #install_chunked_code $ + empty + .+ #mode + .== mode + .+ #target_canister + .== Principal target_canister + .+ #store_canister + .== Principal store_canister + .+ #chunk_hashes_list + .== Vec.fromList [empty .+ #hash .== chunk_hash] + .+ #wasm_module_hash + .== chunk_hash + .+ #arg + .== arg + .+ #sender_canister_version + .== (Nothing :: Maybe W.Word64) + ic_update_settings_with_sender_canister_version' :: (HasAgentConfig) => IC00 -> Blob -> Maybe W.Word64 -> CanisterSettings -> IO ReqResponse ic_update_settings_with_sender_canister_version' ic00 canister_id sender_canister_version r = do callIC' ic00 canister_id #update_settings arg diff --git a/hs/spec_compliance/src/IC/Test/Agent/UnsafeCalls.hs b/hs/spec_compliance/src/IC/Test/Agent/UnsafeCalls.hs index 4331a590c46..1f855b51f2e 100644 --- a/hs/spec_compliance/src/IC/Test/Agent/UnsafeCalls.hs +++ b/hs/spec_compliance/src/IC/Test/Agent/UnsafeCalls.hs @@ -26,6 +26,7 @@ module IC.Test.Agent.UnsafeCalls ic_http_head_request, ic_long_url_http_request, ic_install, + ic_install_single_chunk, ic_install_with_sender_canister_version, ic_provisional_create, ic_provisional_create_with_sender_canister_version, @@ -38,6 +39,7 @@ module IC.Test.Agent.UnsafeCalls ic_top_up, ic_uninstall, ic_uninstall_with_sender_canister_version, + ic_upload_chunk, ic_update_settings, ic_update_settings_with_sender_canister_version, ) @@ -145,6 +147,36 @@ ic_install_with_sender_canister_version ic00 mode canister_id wasm_module arg se ic_install :: (HasCallStack, HasAgentConfig) => IC00 -> InstallMode -> Blob -> Blob -> Blob -> IO () ic_install ic00 mode canister_id wasm_module arg = ic_install_with_sender_canister_version ic00 mode canister_id wasm_module arg Nothing +ic_install_single_chunk :: (HasCallStack, HasAgentConfig) => IC00 -> InstallMode -> Blob -> Blob -> Blob -> Blob -> IO () +ic_install_single_chunk ic00 mode target_canister store_canister chunk_hash arg = do + callIC ic00 target_canister #install_chunked_code $ + empty + .+ #mode + .== mode + .+ #target_canister + .== Principal target_canister + .+ #store_canister + .== Principal store_canister + .+ #chunk_hashes_list + .== Vec.fromList [empty .+ #hash .== chunk_hash] + .+ #wasm_module_hash + .== chunk_hash + .+ #arg + .== arg + .+ #sender_canister_version + .== (Nothing :: Maybe W.Word64) + +ic_upload_chunk :: (HasCallStack, HasAgentConfig) => IC00 -> Blob -> Blob -> IO Blob +ic_upload_chunk ic00 canister_id chunk = do + chunk_hash :: ChunkHash <- + callIC ic00 canister_id #upload_chunk $ + empty + .+ #canister_id + .== Principal canister_id + .+ #chunk + .== chunk + return $ chunk_hash .! #hash + ic_uninstall_with_sender_canister_version :: (HasCallStack, HasAgentConfig) => IC00 -> Blob -> Maybe W.Word64 -> IO () ic_uninstall_with_sender_canister_version ic00 canister_id sender_canister_version = do callIC ic00 canister_id #uninstall_code $ diff --git a/hs/spec_compliance/src/IC/Test/Spec.hs b/hs/spec_compliance/src/IC/Test/Spec.hs index a9af3e2f184..990361dbf9b 100644 --- a/hs/spec_compliance/src/IC/Test/Spec.hs +++ b/hs/spec_compliance/src/IC/Test/Spec.hs @@ -59,2731 +59,2746 @@ import Test.Tasty.HUnit -- * The test suite (see below for helper functions) -icTests :: TestSubnetConfig -> TestSubnetConfig -> AgentConfig -> TestTree -icTests my_sub other_sub = +icTests :: TestSubnetConfig -> TestSubnetConfig -> AgentConfig -> IO TestTree +icTests my_sub other_sub conf = let (my_subnet_id_as_entity, my_type, my_nodes, my_ranges, _) = my_sub in let ((ecid_as_word64, last_canister_id_as_word64) : _) = my_ranges in let (_, last_canister_id_as_word64) = last my_ranges - in let (other_subnet_id_as_entity, _, other_nodes, ((other_ecid_as_word64, _) : _), _) = other_sub + in let (other_subnet_id_as_entity, _, other_nodes, ((other_ecid_as_word64, other_last_canister_id_as_word64) : _), _) = other_sub in let my_subnet_id = rawEntityId my_subnet_id_as_entity in let other_subnet_id = rawEntityId other_subnet_id_as_entity in let my_is_root = isRootTestSubnet my_sub in let ecid = rawEntityId $ wordToId ecid_as_word64 in let other_ecid = rawEntityId $ wordToId other_ecid_as_word64 in let specified_canister_id = rawEntityId $ wordToId last_canister_id_as_word64 - in let unused_canister_id = rawEntityId $ wordToId (last_canister_id_as_word64 - 1) - in let initial_cycles = case my_type of - System -> 0 - _ -> (2 ^ (60 :: Int)) - in withAgentConfig $ - testGroup "Interface Spec acceptance tests" $ - let test_subnet_msg sub subnet_id subnet_id' cid = do - cid2 <- ic_create (ic00viaWithCyclesSubnet subnet_id cid 20_000_000_000_000) ecid Nothing - ic_install (ic00viaWithCyclesSubnet subnet_id cid 0) (enum #install) cid2 trivialWasmModule "" - cid3 <- ic_provisional_create (ic00viaWithCyclesSubnet subnet_id cid 20_000_000_000_000) ecid Nothing Nothing Nothing - ic_install (ic00viaWithCyclesSubnet subnet_id cid 0) (enum #install) cid3 trivialWasmModule "" - ic_install (ic00viaWithCyclesSubnet subnet_id cid 0) (enum #reinstall) cid3 trivialWasmModule "" - ic_install' (ic00viaWithCyclesSubnet' subnet_id' cid 0) (enum #reinstall) cid3 trivialWasmModule "" >>= isReject [3] - _ <- ic_raw_rand (ic00viaWithCyclesSubnet subnet_id cid 0) ecid - return () - in let test_subnet_msg_canister_http sub subnet_id cid = do - _ <- ic_http_get_request (ic00viaWithCyclesSubnet subnet_id cid) sub httpbin_proto ("equal_bytes/8") Nothing Nothing cid - return () - in let test_subnet_msg' sub subnet_id cid = do - ic_create' (ic00viaWithCyclesSubnet' subnet_id cid 20_000_000_000_000) ecid Nothing >>= isReject [3] - ic_provisional_create' (ic00viaWithCyclesSubnet' subnet_id cid 20_000_000_000_000) ecid Nothing Nothing Nothing >>= isReject [3] - cid2 <- ic_create (ic00viaWithCycles cid 20_000_000_000_000) ecid Nothing - ic_install' (ic00viaWithCyclesSubnet' subnet_id cid 0) (enum #install) cid2 trivialWasmModule "" >>= isReject [3] - ic_raw_rand' (ic00viaWithCyclesSubnet' subnet_id cid 0) ecid >>= isReject [3] - in let test_subnet_msg_canister_http' sub subnet_id cid = do - ic_http_get_request' (ic00viaWithCyclesSubnet' subnet_id cid) sub httpbin_proto ("equal_bytes/8") Nothing Nothing cid >>= isReject [3] - in let install_with_cycles_at_id n cycles prog = do - let ecid = rawEntityId $ wordToId n - let specified_id = entityIdToPrincipal $ EntityId ecid - cid <- ic_provisional_create ic00 ecid (Just specified_id) (Just cycles) Nothing - assertBool "canister was not created at its specified ID" $ ecid == cid - installAt cid prog - return cid - in [ testCase "NNS canisters" $ do - registry <- install_with_cycles_at_id 0 initial_cycles noop - governance <- install_with_cycles_at_id 1 initial_cycles noop - ledger <- install_with_cycles_at_id 2 initial_cycles noop - root <- install_with_cycles_at_id 3 initial_cycles noop - cmc <- install_with_cycles_at_id 4 initial_cycles noop - lifeline <- install_with_cycles_at_id 5 initial_cycles noop - genesis <- install_with_cycles_at_id 6 initial_cycles noop - sns <- install_with_cycles_at_id 7 initial_cycles noop - identity <- install_with_cycles_at_id 8 initial_cycles noop - ui <- install_with_cycles_at_id 9 initial_cycles noop - - cid <- install_with_cycles_at_id 10 initial_cycles noop - - let mint = replyData . i64tob . mintCycles . int64 - call' root (mint 0) >>= isReject [5] - - let transfer_args cycles = - defArgs - { other_side = (replyData $ i64tob $ acceptCycles $ int64 cycles), - cycles = cycles - } - let mint_and_transfer cycles = - ( (ignore $ mintCycles $ int64 cycles) - >>> (inter_update cid (transfer_args cycles)) - ) - - when (isRootTestSubnet my_sub) $ do - let transfer_cycles = (2 ^ (61 :: Int)) - call cmc (mint_and_transfer transfer_cycles) >>= isRelay >>= isReply >>= asWord64 >>= is transfer_cycles - ] - ++ [ after AllFinish "($0 ~ /NNS canisters/)" $ - testGroup - "regular canisters" - [ simpleTestCase "create and install" ecid $ \_ -> - return (), - testCase "create_canister necessary" $ - ic_install'' defaultUser (enum #install) doesn'tExist trivialWasmModule "" - >>= isErrOrReject [3, 5], - testGroup - "calls to a subnet ID" - [ let ic_install_subnet'' user subnet_id canister_id wasm_module arg = - callICWithSubnet'' subnet_id user canister_id #install_code tmp - where - tmp :: InstallCodeArgs - tmp = - empty - .+ #mode - .== enum #install - .+ #canister_id - .== Principal canister_id - .+ #wasm_module - .== wasm_module - .+ #arg - .== arg - .+ #sender_canister_version - .== Nothing - in testCase "as user" $ do - cid <- create ecid - ic_install_subnet'' defaultUser my_subnet_id cid trivialWasmModule "" >>= isErrOrReject [] - ic_install_subnet'' defaultUser other_subnet_id cid trivialWasmModule "" >>= isErrOrReject [], - testCase "as canister to own subnet" $ do - cid <- install ecid noop - if my_is_root - then test_subnet_msg my_sub my_subnet_id other_subnet_id cid - else test_subnet_msg' my_sub my_subnet_id cid, - testCase "canister http outcalls to own subnet" $ do - cid <- install ecid noop - if my_is_root - then test_subnet_msg_canister_http my_sub my_subnet_id cid - else test_subnet_msg_canister_http' my_sub my_subnet_id cid, - testCase "as canister to other subnet" $ do - cid <- install ecid noop - if my_is_root - then test_subnet_msg other_sub other_subnet_id my_subnet_id cid - else test_subnet_msg' other_sub other_subnet_id cid, - testCase "canister http outcalls to other subnet" $ do - cid <- install ecid noop - if my_is_root - then test_subnet_msg_canister_http other_sub other_subnet_id cid - else test_subnet_msg_canister_http' other_sub other_subnet_id cid - ], - testGroup - "provisional_create_canister_with_cycles" - [ testCase "specified_id" $ do - let specified_id = entityIdToPrincipal $ EntityId specified_canister_id - ic_provisional_create ic00 ecid (Just specified_id) (Just (2 ^ (60 :: Int))) Nothing >>= is specified_canister_id, - simpleTestCase "specified_id already taken" ecid $ \cid -> do - let specified_id = entityIdToPrincipal $ EntityId cid - ic_provisional_create' ic00 ecid (Just specified_id) (Just (2 ^ (60 :: Int))) Nothing >>= isReject [5], - testCase "specified_id does not belong to the subnet's canister ranges" $ do - let specified_id = entityIdToPrincipal $ EntityId doesn'tExist - ic_provisional_create' ic00 ecid (Just specified_id) (Just (2 ^ (60 :: Int))) Nothing >>= isReject [4] - ], - let inst name = do - cid <- create ecid - wasm <- getTestWasm (name ++ ".wasm") - ic_install ic00 (enum #install) cid wasm "" - return cid - in let good name = testCase ("valid: " ++ name) $ void $ inst name - in let bad name = testCase ("invalid: " ++ name) $ do - cid <- create ecid - wasm <- getTestWasm (name ++ ".wasm") - ic_install' ic00 (enum #install) cid wasm "" >>= isReject [5] - in let read cid m = - ( awaitCall cid $ - rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText m, - "arg" =: GBlob "" - ] - ) - >>= isReply - >>= asWord32 - in testGroup "WebAssembly module validation" $ - map good ["empty_custom_section_name", "large_custom_sections", "long_exported_function_names", "many_custom_sections", "many_exports", "many_functions", "many_globals", "valid_import"] - ++ map bad ["duplicate_custom_section", "invalid_canister_composite_query_cq_reta", "invalid_canister_composite_query_cq_retb", "invalid_canister_export", "invalid_canister_global_timer_reta", "invalid_canister_global_timer_retb", "invalid_canister_heartbeat_reta", "invalid_canister_heartbeat_retb", "invalid_canister_init_reta", "invalid_canister_init_retb", "invalid_canister_inspect_message_reta", "invalid_canister_inspect_message_retb", "invalid_canister_post_upgrade_reta", "invalid_canister_post_upgrade_retb", "invalid_canister_pre_upgrade_reta", "invalid_canister_pre_upgrade_retb", "invalid_canister_query_que_reta", "invalid_canister_query_que_retb", "invalid_canister_update_upd_reta", "invalid_canister_update_upd_retb", "invalid_custom_section", "invalid_empty_custom_section_name", "invalid_empty_query_name", "invalid_import", "name_clash_query_composite_query", "name_clash_update_composite_query", "name_clash_update_query", "too_large_custom_sections", "too_long_exported_function_names", "too_many_custom_sections", "too_many_exports", "too_many_functions", "too_many_globals"] - ++ [ testCase "(start) function" $ do - cid <- inst "start" - ctr <- read cid "read" - ctr @?= 4, -- (start) function was executed - testCase "no (start) function" $ do - cid <- inst "no_start" - ctr <- read cid "read" - ctr @?= 0, -- no (start) function was executed - testCase "empty query name" $ do - cid <- inst "empty_query_name" - void $ read cid "", - testCase "query name with spaces" $ do - cid <- inst "query_name_with_spaces" - void $ read cid "name with spaces", - testCase "empty custom section name" $ do - cid <- inst "empty_custom_section_name" - cert <- getStateCert otherUser cid [["canister", cid, "metadata", ""]] - lookupPath (cert_tree cert) ["canister", cid, "metadata", ""] @?= Found "a", - testCase "custom section name with spaces" $ do - cid <- inst "custom_section_name_with_spaces" - cert <- getStateCert otherUser cid [["canister", cid, "metadata", "name with spaces"]] - lookupPath (cert_tree cert) ["canister", cid, "metadata", "name with spaces"] @?= Found "a" - ], - testCaseSteps "management requests" $ \step -> do - step "Create (provisional)" - can_id <- create ecid - - step "Install" - ic_install ic00 (enum #install) can_id trivialWasmModule "" - - step "Install again fails" - ic_install'' defaultUser (enum #install) can_id trivialWasmModule "" - >>= isErrOrReject [3, 5] - - step "Reinstall" - ic_install ic00 (enum #reinstall) can_id trivialWasmModule "" - - step "Reinstall as wrong user" - ic_install'' otherUser (enum #reinstall) can_id trivialWasmModule "" - >>= isErrOrReject [3, 5] - - step "Upgrade" - ic_install ic00 (enumNothing #upgrade) can_id trivialWasmModule "" - - step "Upgrade as wrong user" - ic_install'' otherUser (enumNothing #upgrade) can_id trivialWasmModule "" - >>= isErrOrReject [3, 5] - - step "Change controller" - ic_set_controllers ic00 can_id [otherUser] - - step "Change controller (with wrong controller)" - ic_set_controllers'' defaultUser can_id [otherUser] - >>= isErrOrReject [3, 5] - - step "Reinstall as new controller" - ic_install (ic00as otherUser) (enum #reinstall) can_id trivialWasmModule "", - testCaseSteps "install (gzip compressed)" $ \step -> do - cid <- create ecid - let compressedModule = compress trivialWasmModule - - step "Install compressed module" - ic_install ic00 (enum #install) cid compressedModule "" - - cs <- ic_canister_status ic00 cid - cs .! #module_hash @?= Just (sha256 compressedModule) - - step "Reinstall compressed module" - ic_install ic00 (enum #reinstall) cid compressedModule "" - - cs <- ic_canister_status ic00 cid - cs .! #module_hash @?= Just (sha256 compressedModule) - - step "Install raw module" - ic_install ic00 (enum #reinstall) cid trivialWasmModule "" - - cs <- ic_canister_status ic00 cid - cs .! #module_hash @?= Just (sha256 trivialWasmModule) - - step "Upgrade to a compressed module" - ic_install ic00 (enumNothing #upgrade) cid compressedModule "" - - cs <- ic_canister_status ic00 cid - cs .! #module_hash @?= Just (sha256 compressedModule), - testCaseSteps "reinstall on empty" $ \step -> do - step "Create" - can_id <- create ecid - - step "Reinstall over empty canister" - ic_install ic00 (enum #reinstall) can_id trivialWasmModule "", - testCaseSteps "canister_status" $ \step -> do - step "Create empty" - cid <- create ecid - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #running - cs .! #settings .! #controllers @?= Vec.fromList [Principal defaultUser] - cs .! #module_hash @?= Nothing - - step "Install" - ic_install ic00 (enum #install) cid trivialWasmModule "" - cs <- ic_canister_status ic00 cid - cs .! #module_hash @?= Just (sha256 trivialWasmModule), - testCaseSteps "canister lifecycle" $ \step -> do - cid <- - install ecid $ - onPreUpgrade $ - callback $ - ignore (stableGrow (int 1)) - >>> stableWrite (int 0) (i2b getStatus) - - step "Is running (via management)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #running + in let store_canister_id = rawEntityId $ wordToId (last_canister_id_as_word64 - 1) + in let other_store_canister_id = rawEntityId $ wordToId other_last_canister_id_as_word64 + in let unused_canister_id = rawEntityId $ wordToId (last_canister_id_as_word64 - 2) + in let initialize_store_canister store_canister_id = do + universal_wasm <- getTestWasm "universal_canister.wasm.gz" + _ <- ic_provisional_create ic00 store_canister_id (Just $ entityIdToPrincipal $ EntityId store_canister_id) Nothing Nothing + ucan_chunk_hash <- ic_upload_chunk ic00 store_canister_id universal_wasm + ic_install_single_chunk ic00 (enum #install) store_canister_id store_canister_id ucan_chunk_hash "" + return ucan_chunk_hash + in let initial_cycles = case my_type of + System -> 0 + _ -> (2 ^ (60 :: Int)) + in do + ucan_chunk_hash <- withAgentConfig conf $ do + ucan_chunk_hash <- initialize_store_canister store_canister_id + return ucan_chunk_hash + let extended_conf = conf {tc_ucan_chunk_hash = Just ucan_chunk_hash, tc_store_canister_id = Just store_canister_id} + return $ + withAgentConfig extended_conf $ + testGroup "Interface Spec acceptance tests" $ + let test_subnet_msg sub subnet_id subnet_id' cid = do + cid2 <- ic_create (ic00viaWithCyclesSubnet subnet_id cid 20_000_000_000_000) ecid Nothing + ic_install (ic00viaWithCyclesSubnet subnet_id cid 0) (enum #install) cid2 trivialWasmModule "" + cid3 <- ic_provisional_create (ic00viaWithCyclesSubnet subnet_id cid 20_000_000_000_000) ecid Nothing Nothing Nothing + ic_install (ic00viaWithCyclesSubnet subnet_id cid 0) (enum #install) cid3 trivialWasmModule "" + ic_install (ic00viaWithCyclesSubnet subnet_id cid 0) (enum #reinstall) cid3 trivialWasmModule "" + ic_install' (ic00viaWithCyclesSubnet' subnet_id' cid 0) (enum #reinstall) cid3 trivialWasmModule "" >>= isReject [3] + _ <- ic_raw_rand (ic00viaWithCyclesSubnet subnet_id cid 0) ecid + return () + in let test_subnet_msg_canister_http sub subnet_id cid = do + _ <- ic_http_get_request (ic00viaWithCyclesSubnet subnet_id cid) sub httpbin_proto ("equal_bytes/8") Nothing Nothing cid + return () + in let test_subnet_msg' sub subnet_id cid = do + ic_create' (ic00viaWithCyclesSubnet' subnet_id cid 20_000_000_000_000) ecid Nothing >>= isReject [3] + ic_provisional_create' (ic00viaWithCyclesSubnet' subnet_id cid 20_000_000_000_000) ecid Nothing Nothing Nothing >>= isReject [3] + cid2 <- ic_create (ic00viaWithCycles cid 20_000_000_000_000) ecid Nothing + ic_install' (ic00viaWithCyclesSubnet' subnet_id cid 0) (enum #install) cid2 trivialWasmModule "" >>= isReject [3] + ic_raw_rand' (ic00viaWithCyclesSubnet' subnet_id cid 0) ecid >>= isReject [3] + in let test_subnet_msg_canister_http' sub subnet_id cid = do + ic_http_get_request' (ic00viaWithCyclesSubnet' subnet_id cid) sub httpbin_proto ("equal_bytes/8") Nothing Nothing cid >>= isReject [3] + in let install_with_cycles_at_id n nns_store_canister_id cycles prog = do + let specified_raw_id = rawEntityId $ wordToId n + let specified_id = entityIdToPrincipal $ EntityId specified_raw_id + cid <- ic_provisional_create ic00 specified_raw_id (Just specified_id) (Just cycles) Nothing + assertBool "canister was not created at its specified ID" $ cid == specified_raw_id + ic_install_single_chunk ic00 (enum #install) cid nns_store_canister_id ucan_chunk_hash (run prog) + return cid + in [ testCase "NNS canisters" $ do + nns_store_canister_id <- + if checkCanisterIdInRanges my_ranges (wordToId 0) + then return store_canister_id + else do + ucan_chunk_hash' <- initialize_store_canister other_store_canister_id + assertBool "universal canister chunk hashes should match for the test and other subnets" $ ucan_chunk_hash == ucan_chunk_hash' + return other_store_canister_id + registry <- install_with_cycles_at_id 0 nns_store_canister_id initial_cycles noop + governance <- install_with_cycles_at_id 1 nns_store_canister_id initial_cycles noop + ledger <- install_with_cycles_at_id 2 nns_store_canister_id initial_cycles noop + root <- install_with_cycles_at_id 3 nns_store_canister_id initial_cycles noop + cmc <- install_with_cycles_at_id 4 nns_store_canister_id initial_cycles noop + lifeline <- install_with_cycles_at_id 5 nns_store_canister_id initial_cycles noop + genesis <- install_with_cycles_at_id 6 nns_store_canister_id initial_cycles noop + sns <- install_with_cycles_at_id 7 nns_store_canister_id initial_cycles noop + identity <- install_with_cycles_at_id 8 nns_store_canister_id initial_cycles noop + ui <- install_with_cycles_at_id 9 nns_store_canister_id initial_cycles noop + + cid <- install_with_cycles_at_id 10 nns_store_canister_id initial_cycles noop + + let mint = replyData . i64tob . mintCycles . int64 + call' root (mint 0) >>= isReject [5] + + let transfer_args cycles = + defArgs + { other_side = (replyData $ i64tob $ acceptCycles $ int64 cycles), + cycles = cycles + } + let mint_and_transfer cycles = + ( (ignore $ mintCycles $ int64 cycles) + >>> (inter_update cid (transfer_args cycles)) + ) + + when (isRootTestSubnet my_sub) $ do + let transfer_cycles = (2 ^ (61 :: Int)) + call cmc (mint_and_transfer transfer_cycles) >>= isRelay >>= isReply >>= asWord64 >>= is transfer_cycles + ] + ++ [ after AllFinish "($0 ~ /NNS canisters/)" $ + testGroup + "regular canisters" + [ simpleTestCase "create and install" ecid $ \_ -> + return (), + testCase "create_canister necessary" $ + ic_install'' defaultUser (enum #install) doesn'tExist trivialWasmModule "" + >>= isErrOrReject [3, 5], + testGroup + "calls to a subnet ID" + [ let ic_install_subnet'' user subnet_id canister_id wasm_module arg = + callICWithSubnet'' subnet_id user canister_id #install_code tmp + where + tmp :: InstallCodeArgs + tmp = + empty + .+ #mode + .== enum #install + .+ #canister_id + .== Principal canister_id + .+ #wasm_module + .== wasm_module + .+ #arg + .== arg + .+ #sender_canister_version + .== Nothing + in testCase "as user" $ do + cid <- create ecid + ic_install_subnet'' defaultUser my_subnet_id cid trivialWasmModule "" >>= isErrOrReject [] + ic_install_subnet'' defaultUser other_subnet_id cid trivialWasmModule "" >>= isErrOrReject [], + testCase "as canister to own subnet" $ do + cid <- install ecid noop + if my_is_root + then test_subnet_msg my_sub my_subnet_id other_subnet_id cid + else test_subnet_msg' my_sub my_subnet_id cid, + testCase "canister http outcalls to own subnet" $ do + cid <- install ecid noop + if my_is_root + then test_subnet_msg_canister_http my_sub my_subnet_id cid + else test_subnet_msg_canister_http' my_sub my_subnet_id cid, + testCase "as canister to other subnet" $ do + cid <- install ecid noop + if my_is_root + then test_subnet_msg other_sub other_subnet_id my_subnet_id cid + else test_subnet_msg' other_sub other_subnet_id cid, + testCase "canister http outcalls to other subnet" $ do + cid <- install ecid noop + if my_is_root + then test_subnet_msg_canister_http other_sub other_subnet_id cid + else test_subnet_msg_canister_http' other_sub other_subnet_id cid + ], + testGroup + "provisional_create_canister_with_cycles" + [ testCase "specified_id" $ do + let specified_id = entityIdToPrincipal $ EntityId specified_canister_id + ic_provisional_create ic00 ecid (Just specified_id) (Just (2 ^ (60 :: Int))) Nothing >>= is specified_canister_id, + simpleTestCase "specified_id already taken" ecid $ \cid -> do + let specified_id = entityIdToPrincipal $ EntityId cid + ic_provisional_create' ic00 ecid (Just specified_id) (Just (2 ^ (60 :: Int))) Nothing >>= isReject [5], + testCase "specified_id does not belong to the subnet's canister ranges" $ do + let specified_id = entityIdToPrincipal $ EntityId doesn'tExist + ic_provisional_create' ic00 ecid (Just specified_id) (Just (2 ^ (60 :: Int))) Nothing >>= isReject [4] + ], + let inst name = do + cid <- create ecid + wasm <- getTestWasm (name ++ ".wasm") + ic_install ic00 (enum #install) cid wasm "" + return cid + in let good name = testCase ("valid: " ++ name) $ void $ inst name + in let bad name = testCase ("invalid: " ++ name) $ do + cid <- create ecid + wasm <- getTestWasm (name ++ ".wasm") + ic_install' ic00 (enum #install) cid wasm "" >>= isReject [5] + in let read cid m = + ( awaitCall cid $ + rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText m, + "arg" =: GBlob "" + ] + ) + >>= isReply + >>= asWord32 + in testGroup "WebAssembly module validation" $ + map good ["empty_custom_section_name", "large_custom_sections", "long_exported_function_names", "many_custom_sections", "many_exports", "many_functions", "many_globals", "valid_import"] + ++ map bad ["duplicate_custom_section", "invalid_canister_composite_query_cq_reta", "invalid_canister_composite_query_cq_retb", "invalid_canister_export", "invalid_canister_global_timer_reta", "invalid_canister_global_timer_retb", "invalid_canister_heartbeat_reta", "invalid_canister_heartbeat_retb", "invalid_canister_init_reta", "invalid_canister_init_retb", "invalid_canister_inspect_message_reta", "invalid_canister_inspect_message_retb", "invalid_canister_post_upgrade_reta", "invalid_canister_post_upgrade_retb", "invalid_canister_pre_upgrade_reta", "invalid_canister_pre_upgrade_retb", "invalid_canister_query_que_reta", "invalid_canister_query_que_retb", "invalid_canister_update_upd_reta", "invalid_canister_update_upd_retb", "invalid_custom_section", "invalid_empty_custom_section_name", "invalid_empty_query_name", "invalid_import", "name_clash_query_composite_query", "name_clash_update_composite_query", "name_clash_update_query", "too_large_custom_sections", "too_long_exported_function_names", "too_many_custom_sections", "too_many_exports", "too_many_functions", "too_many_globals"] + ++ [ testCase "(start) function" $ do + cid <- inst "start" + ctr <- read cid "read" + ctr @?= 4, -- (start) function was executed + testCase "no (start) function" $ do + cid <- inst "no_start" + ctr <- read cid "read" + ctr @?= 0, -- no (start) function was executed + testCase "empty query name" $ do + cid <- inst "empty_query_name" + void $ read cid "", + testCase "query name with spaces" $ do + cid <- inst "query_name_with_spaces" + void $ read cid "name with spaces", + testCase "empty custom section name" $ do + cid <- inst "empty_custom_section_name" + cert <- getStateCert otherUser cid [["canister", cid, "metadata", ""]] + lookupPath (cert_tree cert) ["canister", cid, "metadata", ""] @?= Found "a", + testCase "custom section name with spaces" $ do + cid <- inst "custom_section_name_with_spaces" + cert <- getStateCert otherUser cid [["canister", cid, "metadata", "name with spaces"]] + lookupPath (cert_tree cert) ["canister", cid, "metadata", "name with spaces"] @?= Found "a" + ], + testCaseSteps "management requests" $ \step -> do + step "Create (provisional)" + can_id <- create ecid + + step "Install" + ic_install ic00 (enum #install) can_id trivialWasmModule "" + + step "Install again fails" + ic_install'' defaultUser (enum #install) can_id trivialWasmModule "" + >>= isErrOrReject [3, 5] + + step "Reinstall" + ic_install ic00 (enum #reinstall) can_id trivialWasmModule "" + + step "Reinstall as wrong user" + ic_install'' otherUser (enum #reinstall) can_id trivialWasmModule "" + >>= isErrOrReject [3, 5] + + step "Upgrade" + ic_install ic00 (enumNothing #upgrade) can_id trivialWasmModule "" + + step "Upgrade as wrong user" + ic_install'' otherUser (enumNothing #upgrade) can_id trivialWasmModule "" + >>= isErrOrReject [3, 5] + + step "Change controller" + ic_set_controllers ic00 can_id [otherUser] + + step "Change controller (with wrong controller)" + ic_set_controllers'' defaultUser can_id [otherUser] + >>= isErrOrReject [3, 5] + + step "Reinstall as new controller" + ic_install (ic00as otherUser) (enum #reinstall) can_id trivialWasmModule "", + testCaseSteps "install (gzip compressed)" $ \step -> do + cid <- create ecid + let compressedModule = compress trivialWasmModule + + step "Install compressed module" + ic_install ic00 (enum #install) cid compressedModule "" + + cs <- ic_canister_status ic00 cid + cs .! #module_hash @?= Just (sha256 compressedModule) + + step "Reinstall compressed module" + ic_install ic00 (enum #reinstall) cid compressedModule "" + + cs <- ic_canister_status ic00 cid + cs .! #module_hash @?= Just (sha256 compressedModule) + + step "Install raw module" + ic_install ic00 (enum #reinstall) cid trivialWasmModule "" + + cs <- ic_canister_status ic00 cid + cs .! #module_hash @?= Just (sha256 trivialWasmModule) + + step "Upgrade to a compressed module" + ic_install ic00 (enumNothing #upgrade) cid compressedModule "" + + cs <- ic_canister_status ic00 cid + cs .! #module_hash @?= Just (sha256 compressedModule), + testCaseSteps "reinstall on empty" $ \step -> do + step "Create" + can_id <- create ecid + + step "Reinstall over empty canister" + ic_install ic00 (enum #reinstall) can_id trivialWasmModule "", + testCaseSteps "canister_status" $ \step -> do + step "Create empty" + cid <- create ecid + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #running + cs .! #settings .! #controllers @?= Vec.fromList [Principal defaultUser] + cs .! #module_hash @?= Nothing - step "Is running (local)?" - query cid (replyData (i2b getStatus)) >>= asWord32 >>= is 1 + step "Install" + ic_install ic00 (enum #install) cid trivialWasmModule "" + cs <- ic_canister_status ic00 cid + cs .! #module_hash @?= Just (sha256 trivialWasmModule), + testCaseSteps "canister lifecycle" $ \step -> do + cid <- + install ecid $ + onPreUpgrade $ + callback $ + ignore (stableGrow (int 1)) + >>> stableWrite (int 0) (i2b getStatus) - step "Stop" - ic_stop_canister ic00 cid + step "Is running (via management)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #running - step "Is stopped (via management)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #stopped + step "Is running (local)?" + query cid (replyData (i2b getStatus)) >>= asWord32 >>= is 1 - step "Stop is noop" - ic_stop_canister ic00 cid + step "Stop" + ic_stop_canister ic00 cid - step "Cannot call (update)?" - call' cid reply >>= isReject [5] + step "Is stopped (via management)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #stopped - step "Cannot call (query)?" - query' cid reply >>= isQueryReject ecid [5] + step "Stop is noop" + ic_stop_canister ic00 cid - step "Upgrade" - upgrade cid $ setGlobal (i2b getStatus) + step "Cannot call (update)?" + call' cid reply >>= isReject [5] - step "Start canister" - ic_start_canister ic00 cid + step "Cannot call (query)?" + query' cid reply >>= isQueryReject ecid [5] - step "Is running (via managemnet)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #running + step "Upgrade" + upgrade cid $ setGlobal (i2b getStatus) - step "Is running (local)?" - query cid (replyData (i2b getStatus)) >>= asWord32 >>= is 1 + step "Start canister" + ic_start_canister ic00 cid - step "Was stopped during pre-upgrade?" - query cid (replyData (stableRead (int 0) (int 4))) >>= asWord32 >>= is 3 + step "Is running (via managemnet)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #running - step "Was stopped during post-upgrade?" - query cid (replyData getGlobal) >>= asWord32 >>= is 3 + step "Is running (local)?" + query cid (replyData (i2b getStatus)) >>= asWord32 >>= is 1 - step "Can call (update)?" - call_ cid reply + step "Was stopped during pre-upgrade?" + query cid (replyData (stableRead (int 0) (int 4))) >>= asWord32 >>= is 3 - step "Can call (query)?" - query_ cid reply + step "Was stopped during post-upgrade?" + query cid (replyData getGlobal) >>= asWord32 >>= is 3 - step "Start is noop" - ic_start_canister ic00 cid, - testCaseSteps "canister stopping" $ \step -> do - cid <- install ecid noop + step "Can call (update)?" + call_ cid reply - step "Is running (via management)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #running + step "Can call (query)?" + query_ cid reply - step "Is running (local)?" - query cid (replyData (i2b getStatus)) >>= asWord32 >>= is 1 + step "Start is noop" + ic_start_canister ic00 cid, + testCaseSteps "canister stopping" $ \step -> do + cid <- install ecid noop - step "Create message hold" - (messageHold, release) <- createMessageHold ecid + step "Is running (via management)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #running - step "Create long-running call" - grs1 <- submitCall cid $ callRequest cid messageHold - awaitKnown grs1 >>= isPendingOrProcessing + step "Is running (local)?" + query cid (replyData (i2b getStatus)) >>= asWord32 >>= is 1 - step "Normal call (to sync)" - call_ cid reply + step "Create message hold" + (messageHold, release) <- createMessageHold ecid - step "Stop" - grs2 <- submitCall cid $ stopRequest cid - awaitKnown grs2 >>= isPendingOrProcessing + step "Create long-running call" + grs1 <- submitCall cid $ callRequest cid messageHold + awaitKnown grs1 >>= isPendingOrProcessing - step "Is stopping (via management)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #stopping + step "Normal call (to sync)" + call_ cid reply - step "Next stop waits, too" - grs3 <- submitCall cid $ stopRequest cid - awaitKnown grs3 >>= isPendingOrProcessing + step "Stop" + grs2 <- submitCall cid $ stopRequest cid + awaitKnown grs2 >>= isPendingOrProcessing - step "Cannot call (update)?" - call' cid reply >>= isReject [5] + step "Is stopping (via management)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #stopping - step "Cannot call (query)?" - query' cid reply >>= isQueryReject ecid [5] + step "Next stop waits, too" + grs3 <- submitCall cid $ stopRequest cid + awaitKnown grs3 >>= isPendingOrProcessing - step "Release the held message" - release + step "Cannot call (update)?" + call' cid reply >>= isReject [5] - step "Wait for calls to complete" - awaitStatus grs1 >>= isReply >>= is "" - awaitStatus grs2 >>= isReply >>= is (Candid.encode ()) - awaitStatus grs3 >>= isReply >>= is (Candid.encode ()) + step "Cannot call (query)?" + query' cid reply >>= isQueryReject ecid [5] - step "Is stopped (via management)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #stopped + step "Release the held message" + release - step "Cannot call (update)?" - call' cid reply >>= isReject [5] + step "Wait for calls to complete" + awaitStatus grs1 >>= isReply >>= is "" + awaitStatus grs2 >>= isReply >>= is (Candid.encode ()) + awaitStatus grs3 >>= isReply >>= is (Candid.encode ()) - step "Cannot call (query)?" - query' cid reply >>= isQueryReject ecid [5], - testCaseSteps "starting a stopping canister" $ \step -> do - cid <- install ecid noop + step "Is stopped (via management)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #stopped - step "Create message hold" - (messageHold, _) <- createMessageHold ecid + step "Cannot call (update)?" + call' cid reply >>= isReject [5] - step "Create long-running call" - grs1 <- submitCall cid $ callRequest cid messageHold - awaitKnown grs1 >>= isPendingOrProcessing + step "Cannot call (query)?" + query' cid reply >>= isQueryReject ecid [5], + testCaseSteps "starting a stopping canister" $ \step -> do + cid <- install ecid noop - step "Normal call (to sync)" - call_ cid reply + step "Create message hold" + (messageHold, _) <- createMessageHold ecid - step "Stop" - grs2 <- submitCall cid $ stopRequest cid - awaitKnown grs2 >>= isPendingOrProcessing + step "Create long-running call" + grs1 <- submitCall cid $ callRequest cid messageHold + awaitKnown grs1 >>= isPendingOrProcessing - step "Is stopping (via management)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #stopping + step "Normal call (to sync)" + call_ cid reply - step "Restart" - ic_start_canister ic00 cid + step "Stop" + grs2 <- submitCall cid $ stopRequest cid + awaitKnown grs2 >>= isPendingOrProcessing - step "Is running (via management)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #running, - testCaseSteps "canister deletion" $ \step -> do - cid <- install ecid noop + step "Is stopping (via management)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #stopping - step "Deletion fails" - ic_delete_canister' ic00 cid >>= isReject [5] + step "Restart" + ic_start_canister ic00 cid - step "Create message hold" - (messageHold, release) <- createMessageHold ecid + step "Is running (via management)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #running, + testCaseSteps "canister deletion" $ \step -> do + cid <- install ecid noop - step "Create long-running call" - grs1 <- submitCall cid $ callRequest cid messageHold - awaitKnown grs1 >>= isPendingOrProcessing + step "Deletion fails" + ic_delete_canister' ic00 cid >>= isReject [5] - step "Start stopping" - grs2 <- submitCall cid $ stopRequest cid - awaitKnown grs2 >>= isPendingOrProcessing - - step "Is stopping?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #stopping - - step "Deletion fails" - ic_delete_canister' ic00 cid >>= isReject [5] - - step "Let canister stop" - release - awaitStatus grs1 >>= isReply >>= is "" - awaitStatus grs2 >>= isReply >>= is (Candid.encode ()) - - step "Is stopped?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #stopped - - step "Deletion succeeds" - ic_delete_canister ic00 cid - - -- Disabled; such a call gets accepted (200) but - -- then the status never shows up, which causes a timeout - -- - -- step "Cannot call (update)?" - -- call' cid reply >>= isReject [3] - - step "Cannot call (inter-canister)?" - cid2 <- install ecid noop - do call cid2 $ inter_update cid defArgs - >>= isRelay - >>= isReject [3] - - step "Cannot call (query)?" - query' cid reply >>= isQueryReject ecid [3] - - step "Cannot query canister_status" - ic_canister_status'' defaultUser cid >>= isErrOrReject [3, 5] - - step "Deletion fails" - ic_delete_canister'' defaultUser cid >>= isErrOrReject [3, 5], - testCaseSteps "canister lifecycle (wrong controller)" $ \step -> do - cid <- install ecid noop - - step "Start as wrong user" - ic_start_canister'' otherUser cid >>= isErrOrReject [3, 5] - step "Stop as wrong user" - ic_stop_canister'' otherUser cid >>= isErrOrReject [3, 5] - step "Canister Status as wrong user" - ic_canister_status'' otherUser cid >>= isErrOrReject [3, 5] - step "Delete as wrong user" - ic_delete_canister'' otherUser cid >>= isErrOrReject [3, 5], - testCaseSteps "aaaaa-aa (inter-canister)" $ \step -> do - -- install universal canisters to proxy the requests - cid <- install ecid noop - cid2 <- install ecid noop - - step "Create" - can_id <- ic_provisional_create (ic00via cid) ecid Nothing Nothing Nothing - - step "Install" - ic_install (ic00via cid) (enum #install) can_id trivialWasmModule "" - - step "Install again fails" - ic_install' (ic00via cid) (enum #install) can_id trivialWasmModule "" - >>= isReject [3, 5] - - step "Reinstall" - ic_install (ic00via cid) (enum #reinstall) can_id trivialWasmModule "" - - step "Reinstall (gzip compressed)" - ic_install (ic00via cid) (enum #reinstall) can_id (compress trivialWasmModule) "" - - step "Reinstall as wrong user" - ic_install' (ic00via cid2) (enum #reinstall) can_id trivialWasmModule "" - >>= isReject [3, 5] - - step "Upgrade" - ic_install (ic00via cid) (enumNothing #upgrade) can_id trivialWasmModule "" - - step "Change controller" - ic_set_controllers (ic00via cid) can_id [cid2] - - step "Change controller (with wrong controller)" - ic_set_controllers' (ic00via cid) can_id [cid2] - >>= isReject [3, 5] - - step "Reinstall as new controller" - ic_install (ic00via cid2) (enum #reinstall) can_id trivialWasmModule "" - - step "Create" - can_id2 <- ic_provisional_create (ic00via cid) ecid Nothing Nothing Nothing - - step "Reinstall on empty" - ic_install (ic00via cid) (enum #reinstall) can_id2 trivialWasmModule "", - simpleTestCase "aaaaa-aa (inter-canister, large)" ecid $ \cid -> do - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - can_id <- ic_provisional_create (ic00via cid) ecid Nothing Nothing Nothing - ic_install (ic00via cid) (enum #install) can_id universal_wasm "" - do call can_id $ replyData "Hi" - >>= is "Hi", - simpleTestCase "randomness" ecid $ \cid -> do - r1 <- ic_raw_rand (ic00via cid) ecid - r2 <- ic_raw_rand (ic00via cid) ecid - BS.length r1 @?= 32 - BS.length r2 @?= 32 - assertBool "random blobs are different" $ r1 /= r2, - testGroup "canister http outcalls" $ canister_http_calls my_sub httpbin_proto, - testGroup - "large calls" - $ let arg n = BS.pack $ take n $ repeat 0 - in let prog n = ignore (stableGrow (int 666)) >>> stableWrite (int 0) (bytes $ arg n) >>> replyData "ok" - in let callRec cid n = - rec - [ "request_type" =: GText "call", - "canister_id" =: GBlob cid, - "sender" =: GBlob anonymousUser, - "method_name" =: GText "update", - "arg" =: GBlob (run $ prog n) - ] - in let queryRec cid n = - rec - [ "request_type" =: GText "query", - "canister_id" =: GBlob cid, - "sender" =: GBlob anonymousUser, - "method_name" =: GText "query", - "arg" =: GBlob (run $ prog n) + step "Create message hold" + (messageHold, release) <- createMessageHold ecid + + step "Create long-running call" + grs1 <- submitCall cid $ callRequest cid messageHold + awaitKnown grs1 >>= isPendingOrProcessing + + step "Start stopping" + grs2 <- submitCall cid $ stopRequest cid + awaitKnown grs2 >>= isPendingOrProcessing + + step "Is stopping?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #stopping + + step "Deletion fails" + ic_delete_canister' ic00 cid >>= isReject [5] + + step "Let canister stop" + release + awaitStatus grs1 >>= isReply >>= is "" + awaitStatus grs2 >>= isReply >>= is (Candid.encode ()) + + step "Is stopped?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #stopped + + step "Deletion succeeds" + ic_delete_canister ic00 cid + + -- Disabled; such a call gets accepted (200) but + -- then the status never shows up, which causes a timeout + -- + -- step "Cannot call (update)?" + -- call' cid reply >>= isReject [3] + + step "Cannot call (inter-canister)?" + cid2 <- install ecid noop + do call cid2 $ inter_update cid defArgs + >>= isRelay + >>= isReject [3] + + step "Cannot call (query)?" + query' cid reply >>= isQueryReject ecid [3] + + step "Cannot query canister_status" + ic_canister_status'' defaultUser cid >>= isErrOrReject [3, 5] + + step "Deletion fails" + ic_delete_canister'' defaultUser cid >>= isErrOrReject [3, 5], + testCaseSteps "canister lifecycle (wrong controller)" $ \step -> do + cid <- install ecid noop + + step "Start as wrong user" + ic_start_canister'' otherUser cid >>= isErrOrReject [3, 5] + step "Stop as wrong user" + ic_stop_canister'' otherUser cid >>= isErrOrReject [3, 5] + step "Canister Status as wrong user" + ic_canister_status'' otherUser cid >>= isErrOrReject [3, 5] + step "Delete as wrong user" + ic_delete_canister'' otherUser cid >>= isErrOrReject [3, 5], + testCaseSteps "aaaaa-aa (inter-canister)" $ \step -> do + -- install universal canisters to proxy the requests + cid <- install ecid noop + cid2 <- install ecid noop + + step "Create" + can_id <- ic_provisional_create (ic00via cid) ecid Nothing Nothing Nothing + + step "Install" + ic_install (ic00via cid) (enum #install) can_id trivialWasmModule "" + + step "Install again fails" + ic_install' (ic00via cid) (enum #install) can_id trivialWasmModule "" + >>= isReject [3, 5] + + step "Reinstall" + ic_install (ic00via cid) (enum #reinstall) can_id trivialWasmModule "" + + step "Reinstall (gzip compressed)" + ic_install (ic00via cid) (enum #reinstall) can_id (compress trivialWasmModule) "" + + step "Reinstall as wrong user" + ic_install' (ic00via cid2) (enum #reinstall) can_id trivialWasmModule "" + >>= isReject [3, 5] + + step "Upgrade" + ic_install (ic00via cid) (enumNothing #upgrade) can_id trivialWasmModule "" + + step "Change controller" + ic_set_controllers (ic00via cid) can_id [cid2] + + step "Change controller (with wrong controller)" + ic_set_controllers' (ic00via cid) can_id [cid2] + >>= isReject [3, 5] + + step "Reinstall as new controller" + ic_install (ic00via cid2) (enum #reinstall) can_id trivialWasmModule "" + + step "Create" + can_id2 <- ic_provisional_create (ic00via cid) ecid Nothing Nothing Nothing + + step "Reinstall on empty" + ic_install (ic00via cid) (enum #reinstall) can_id2 trivialWasmModule "", + simpleTestCase "aaaaa-aa (inter-canister, large)" ecid $ \cid -> do + can_id <- ic_provisional_create (ic00via cid) ecid Nothing Nothing Nothing + ic_set_controllers (ic00via cid) can_id [store_canister_id, cid] + ic_install_single_chunk (ic00via store_canister_id) (enum #install) can_id store_canister_id ucan_chunk_hash "" + do call can_id $ replyData "Hi" + >>= is "Hi", + simpleTestCase "randomness" ecid $ \cid -> do + r1 <- ic_raw_rand (ic00via cid) ecid + r2 <- ic_raw_rand (ic00via cid) ecid + BS.length r1 @?= 32 + BS.length r2 @?= 32 + assertBool "random blobs are different" $ r1 /= r2, + testGroup "canister http outcalls" $ canister_http_calls my_sub httpbin_proto, + testGroup + "large calls" + $ let arg n = BS.pack $ take n $ repeat 0 + in let prog n = ignore (stableGrow (int 666)) >>> stableWrite (int 0) (bytes $ arg n) >>> replyData "ok" + in let callRec cid n = + rec + [ "request_type" =: GText "call", + "canister_id" =: GBlob cid, + "sender" =: GBlob anonymousUser, + "method_name" =: GText "update", + "arg" =: GBlob (run $ prog n) + ] + in let queryRec cid n = + rec + [ "request_type" =: GText "query", + "canister_id" =: GBlob cid, + "sender" =: GBlob anonymousUser, + "method_name" =: GText "query", + "arg" =: GBlob (run $ prog n) + ] + in [ simpleTestCase "Large update call" ecid $ \cid -> + do + let size = case my_type of + System -> 3600000 -- registry setting for system subnets: 3.5MiB + _ -> 2000000 -- registry setting for app subnets: 2MiB + addNonceExpiryEnv (callRec cid size) + >>= postCallCBOR cid + >>= code202 + call cid (prog size) >>= is "ok", + simpleTestCase "Too large update call" ecid $ \cid -> + do + let size = case my_type of + System -> 3700000 + _ -> 2100000 + addNonceExpiryEnv (callRec cid size) + >>= postCallCBOR cid + >>= code4xx, + simpleTestCase "Large query call" ecid $ \cid -> do + let size = 4100000 -- BN limits all requests to 4MiB + addNonceExpiryEnv (queryRec cid size) + >>= postQueryCBOR cid + >>= code2xx + query cid (prog size) >>= is "ok", + simpleTestCase "Too large query call" ecid $ \cid -> + addNonceExpiryEnv (queryRec cid 4200000) + >>= postQueryCBOR cid + >>= code4xx + ], + testGroup + "simple calls" + [ simpleTestCase "Call" ecid $ \cid -> + call cid (replyData "ABCD") >>= is "ABCD", + simpleTestCase "Call (query)" ecid $ \cid -> do + query cid (replyData "ABCD") >>= is "ABCD", + simpleTestCase "Call no non-existent update method" ecid $ \cid -> + do + awaitCall' cid $ + rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "no_such_update", + "arg" =: GBlob "" + ] + >>= isErrOrReject [5], + simpleTestCase "Call no non-existent query method" ecid $ \cid -> + do + let cbor = + rec + [ "request_type" =: GText "query", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "no_such_update", + "arg" =: GBlob "" + ] + (rid, res) <- queryCBOR cid cbor + res <- queryResponse res + isQueryReject ecid [5] (rid, res), + simpleTestCase "reject" ecid $ \cid -> + call' cid (reject "ABCD") >>= isReject [4], + simpleTestCase "reject (query)" ecid $ \cid -> + query' cid (reject "ABCD") >>= isQueryReject ecid [4], + simpleTestCase "No response" ecid $ \cid -> + call' cid noop >>= isReject [5], + simpleTestCase "No response does not rollback" ecid $ \cid -> do + call'' cid (setGlobal "FOO") >>= isErrOrReject [5] + query cid (replyData getGlobal) >>= is "FOO", + simpleTestCase "No response (query)" ecid $ \cid -> + query' cid noop >>= isQueryReject ecid [5], + simpleTestCase "Double reply" ecid $ \cid -> + call' cid (reply >>> reply) >>= isReject [5], + simpleTestCase "Double reply (query)" ecid $ \cid -> + query' cid (reply >>> reply) >>= isQueryReject ecid [5], + simpleTestCase "Reply data append after reply" ecid $ \cid -> + call' cid (reply >>> replyDataAppend "foo") >>= isReject [5], + simpleTestCase "Reply data append after reject" ecid $ \cid -> + call' cid (reject "bar" >>> replyDataAppend "foo") >>= isReject [5], + simpleTestCase "Caller" ecid $ \cid -> + call cid (replyData caller) >>= is defaultUser, + simpleTestCase "Caller (query)" ecid $ \cid -> + query cid (replyData caller) >>= is defaultUser + ], + testGroup + "Settings" + [ testGroup + "Controllers" + $ [ testCase "A canister can request its own status if it does not control itself" $ do + let controllers = [defaultUser, otherUser] + cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing + ic_set_controllers ic00 cid controllers + ic_install_single_chunk ic00 (enum #install) cid store_canister_id ucan_chunk_hash "" + + cs <- ic_canister_status (ic00via cid) cid + assertBool "canister should not control itself in this test" $ not $ elem cid controllers + Vec.toList (cs .! #settings .! #controllers) `isSet` map Principal controllers, + testCase "Changing controllers" $ do + let controllers = [defaultUser, otherUser] + cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing + ic_set_controllers ic00 cid controllers + ic_install_single_chunk ic00 (enum #install) cid store_canister_id ucan_chunk_hash "" + + -- Set new controller + ic_set_controllers (ic00as defaultUser) cid [ecdsaUser] + + -- Only that controller can get canister status + ic_canister_status'' defaultUser cid >>= isErrOrReject [3, 5] + ic_canister_status'' otherUser cid >>= isErrOrReject [3, 5] + ic_canister_status'' anonymousUser cid >>= isErrOrReject [3, 5] + ic_canister_status'' secp256k1User cid >>= isErrOrReject [3, 5] + cs <- ic_canister_status (ic00as ecdsaUser) cid + cs .! #settings .! #controllers @?= Vec.fromList [Principal ecdsaUser], + simpleTestCase "Multiple controllers (aaaaa-aa)" ecid $ \cid -> do + let controllers = [cid, otherUser] + cid2 <- ic_create_with_controllers (ic00viaWithCycles cid 20_000_000_000_000) ecid controllers + ic_set_controllers (ic00via cid) cid2 (controllers ++ [store_canister_id]) + ic_install_single_chunk (ic00via store_canister_id) (enum #install) cid2 store_canister_id ucan_chunk_hash "" + ic_set_controllers (ic00via cid) cid2 controllers + + -- Controllers should be able to fetch the canister status. + cs <- ic_canister_status (ic00via cid) cid2 + Vec.toList (cs .! #settings .! #controllers) `isSet` map Principal controllers + cs <- ic_canister_status (ic00as otherUser) cid2 + Vec.toList (cs .! #settings .! #controllers) `isSet` map Principal controllers + + -- Non-controllers cannot fetch the canister status + ic_canister_status'' ecdsaUser cid >>= isErrOrReject [3, 5] + ic_canister_status'' anonymousUser cid >>= isErrOrReject [3, 5] + ic_canister_status'' secp256k1User cid >>= isErrOrReject [3, 5], + simpleTestCase "> 10 controllers" ecid $ \cid -> do + ic_create_with_controllers' (ic00viaWithCycles cid 20_000_000_000_000) ecid (replicate 11 cid) >>= isReject [3, 5] + ic_set_controllers' ic00 cid (replicate 11 cid) >>= isReject [4], + simpleTestCase "No controller" ecid $ \cid -> do + cid2 <- ic_create_with_controllers (ic00viaWithCycles cid 20_000_000_000_000) ecid [] + ic_canister_status'' defaultUser cid2 >>= isErrOrReject [3, 5] + ic_canister_status'' otherUser cid2 >>= isErrOrReject [3, 5], + testCase "Controller is self" $ do + cid <- install ecid noop + ic_set_controllers ic00 cid [cid] -- Set controller of cid to be itself + + -- cid can now request its own status + cs <- ic_canister_status (ic00via cid) cid + cs .! #settings .! #controllers @?= Vec.fromList [Principal cid], + testCase "Duplicate controllers" $ do + let controllers = [defaultUser, defaultUser, otherUser] + cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing + ic_set_controllers ic00 cid controllers + cs <- ic_canister_status (ic00as defaultUser) cid + Vec.toList (cs .! #settings .! #controllers) `isSet` map Principal controllers ] - in [ simpleTestCase "Large update call" ecid $ \cid -> - do - let size = case my_type of - System -> 3600000 -- registry setting for system subnets: 3.5MiB - _ -> 2000000 -- registry setting for app subnets: 2MiB - addNonceExpiryEnv (callRec cid size) - >>= postCallCBOR cid - >>= code202 - call cid (prog size) >>= is "ok", - simpleTestCase "Too large update call" ecid $ \cid -> - do - let size = case my_type of - System -> 3700000 - _ -> 2100000 - addNonceExpiryEnv (callRec cid size) - >>= postCallCBOR cid - >>= code4xx, - simpleTestCase "Large query call" ecid $ \cid -> do - let size = 4100000 -- BN limits all requests to 4MiB - addNonceExpiryEnv (queryRec cid size) - >>= postQueryCBOR cid - >>= code2xx - query cid (prog size) >>= is "ok", - simpleTestCase "Too large query call" ecid $ \cid -> - addNonceExpiryEnv (queryRec cid 4200000) - >>= postQueryCBOR cid - >>= code4xx - ], - testGroup - "simple calls" - [ simpleTestCase "Call" ecid $ \cid -> - call cid (replyData "ABCD") >>= is "ABCD", - simpleTestCase "Call (query)" ecid $ \cid -> do - query cid (replyData "ABCD") >>= is "ABCD", - simpleTestCase "Call no non-existent update method" ecid $ \cid -> - do - awaitCall' cid $ - rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "no_such_update", - "arg" =: GBlob "" - ] - >>= isErrOrReject [5], - simpleTestCase "Call no non-existent query method" ecid $ \cid -> - do - let cbor = - rec - [ "request_type" =: GText "query", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "no_such_update", - "arg" =: GBlob "" - ] - (rid, res) <- queryCBOR cid cbor - res <- queryResponse res - isQueryReject ecid [5] (rid, res), - simpleTestCase "reject" ecid $ \cid -> - call' cid (reject "ABCD") >>= isReject [4], - simpleTestCase "reject (query)" ecid $ \cid -> - query' cid (reject "ABCD") >>= isQueryReject ecid [4], - simpleTestCase "No response" ecid $ \cid -> - call' cid noop >>= isReject [5], - simpleTestCase "No response does not rollback" ecid $ \cid -> do - call'' cid (setGlobal "FOO") >>= isErrOrReject [5] - query cid (replyData getGlobal) >>= is "FOO", - simpleTestCase "No response (query)" ecid $ \cid -> - query' cid noop >>= isQueryReject ecid [5], - simpleTestCase "Double reply" ecid $ \cid -> - call' cid (reply >>> reply) >>= isReject [5], - simpleTestCase "Double reply (query)" ecid $ \cid -> - query' cid (reply >>> reply) >>= isQueryReject ecid [5], - simpleTestCase "Reply data append after reply" ecid $ \cid -> - call' cid (reply >>> replyDataAppend "foo") >>= isReject [5], - simpleTestCase "Reply data append after reject" ecid $ \cid -> - call' cid (reject "bar" >>> replyDataAppend "foo") >>= isReject [5], - simpleTestCase "Caller" ecid $ \cid -> - call cid (replyData caller) >>= is defaultUser, - simpleTestCase "Caller (query)" ecid $ \cid -> - query cid (replyData caller) >>= is defaultUser - ], - testGroup - "Settings" - [ testGroup - "Controllers" - $ [ testCase "A canister can request its own status if it does not control itself" $ do - let controllers = [defaultUser, otherUser] - cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing - ic_set_controllers ic00 cid controllers - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install ic00 (enum #install) cid universal_wasm "" - - cs <- ic_canister_status (ic00via cid) cid - assertBool "canister should not control itself in this test" $ not $ elem cid controllers - Vec.toList (cs .! #settings .! #controllers) `isSet` map Principal controllers, - testCase "Changing controllers" $ do - let controllers = [defaultUser, otherUser] - cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing - ic_set_controllers ic00 cid controllers - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install ic00 (enum #install) cid universal_wasm "" - - -- Set new controller - ic_set_controllers (ic00as defaultUser) cid [ecdsaUser] - - -- Only that controller can get canister status - ic_canister_status'' defaultUser cid >>= isErrOrReject [3, 5] - ic_canister_status'' otherUser cid >>= isErrOrReject [3, 5] - ic_canister_status'' anonymousUser cid >>= isErrOrReject [3, 5] - ic_canister_status'' secp256k1User cid >>= isErrOrReject [3, 5] - cs <- ic_canister_status (ic00as ecdsaUser) cid - cs .! #settings .! #controllers @?= Vec.fromList [Principal ecdsaUser], - simpleTestCase "Multiple controllers (aaaaa-aa)" ecid $ \cid -> do - let controllers = [cid, otherUser] - cid2 <- ic_create_with_controllers (ic00viaWithCycles cid 20_000_000_000_000) ecid controllers - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install (ic00via cid) (enum #install) cid2 universal_wasm "" - - -- Controllers should be able to fetch the canister status. - cs <- ic_canister_status (ic00via cid) cid2 - Vec.toList (cs .! #settings .! #controllers) `isSet` map Principal controllers - cs <- ic_canister_status (ic00as otherUser) cid2 - Vec.toList (cs .! #settings .! #controllers) `isSet` map Principal controllers - - -- Non-controllers cannot fetch the canister status - ic_canister_status'' ecdsaUser cid >>= isErrOrReject [3, 5] - ic_canister_status'' anonymousUser cid >>= isErrOrReject [3, 5] - ic_canister_status'' secp256k1User cid >>= isErrOrReject [3, 5], - simpleTestCase "> 10 controllers" ecid $ \cid -> do - ic_create_with_controllers' (ic00viaWithCycles cid 20_000_000_000_000) ecid (replicate 11 cid) >>= isReject [3, 5] - ic_set_controllers' ic00 cid (replicate 11 cid) >>= isReject [4], - simpleTestCase "No controller" ecid $ \cid -> do - cid2 <- ic_create_with_controllers (ic00viaWithCycles cid 20_000_000_000_000) ecid [] - ic_canister_status'' defaultUser cid2 >>= isErrOrReject [3, 5] - ic_canister_status'' otherUser cid2 >>= isErrOrReject [3, 5], - testCase "Controller is self" $ do - cid <- install ecid noop - ic_set_controllers ic00 cid [cid] -- Set controller of cid to be itself - - -- cid can now request its own status - cs <- ic_canister_status (ic00via cid) cid - cs .! #settings .! #controllers @?= Vec.fromList [Principal cid], - testCase "Duplicate controllers" $ do - let controllers = [defaultUser, defaultUser, otherUser] - cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing - ic_set_controllers ic00 cid controllers - cs <- ic_canister_status (ic00as defaultUser) cid - Vec.toList (cs .! #settings .! #controllers) `isSet` map Principal controllers - ] - ++ ( let invalid_compute_allocation :: CanisterSettings = - empty - .+ #controllers - .== Nothing - .+ #compute_allocation - .== Just 101 - .+ #memory_allocation - .== Nothing - .+ #freezing_threshold - .== Nothing - .+ #reserved_cycles_limit - .== Nothing - .+ #log_visibility - .== Nothing - .+ #wasm_memory_limit - .== Nothing - in let invalid_memory_allocation :: CanisterSettings = - empty - .+ #controllers - .== Nothing - .+ #compute_allocation - .== Nothing - .+ #memory_allocation - .== Just (2 ^ 48 + 1) - .+ #freezing_threshold - .== Nothing - .+ #reserved_cycles_limit - .== Nothing - .+ #log_visibility - .== Nothing - .+ #wasm_memory_limit - .== Nothing - in let invalid_freezing_threshold :: CanisterSettings = - empty - .+ #controllers - .== Nothing - .+ #compute_allocation - .== Nothing - .+ #memory_allocation - .== Nothing - .+ #freezing_threshold - .== Just (2 ^ 64) - .+ #reserved_cycles_limit - .== Nothing - .+ #log_visibility - .== Nothing - .+ #wasm_memory_limit - .== Nothing - in let invalid_settings = - [ ("Invalid compute allocation (101)", invalid_compute_allocation), - ("Invalid memory allocation (2^48+1)", invalid_memory_allocation), - ("Invalid freezing threshold (2^64)", invalid_freezing_threshold) - ] - in let test_modes = - [ ( "via provisional_create_canister_with_cycles:", - \(desc, settings) -> testCase desc $ do - ic_provisional_create' ic00 ecid Nothing Nothing (Just settings) >>= isReject [5] - ), - ( "via create_canister:", - \(desc, settings) -> simpleTestCase desc ecid $ \cid -> do - ic_create' (ic00via cid) ecid (Just settings) >>= isReject [5] - ), - ( "via update_settings", - \(desc, settings) -> simpleTestCase desc ecid $ \cid -> do - ic_update_settings' ic00 cid settings >>= isReject [5] - ) - ] - in map (\(desc, test) -> testGroup desc (map test invalid_settings)) test_modes - ), - simpleTestCase "Valid allocations" ecid $ \cid -> do - let settings :: CanisterSettings = - empty - .+ #controllers - .== Nothing - .+ #compute_allocation - .== Just 1 - .+ #memory_allocation - .== Just (1024 * 1024) - .+ #freezing_threshold - .== Just 1000_000 - .+ #reserved_cycles_limit - .== Nothing - .+ #log_visibility - .== Nothing - .+ #wasm_memory_limit - .== Nothing - cid2 <- ic_create (ic00viaWithCycles cid 20_000_000_000_000) ecid (Just settings) - cs <- ic_canister_status (ic00via cid) cid2 - cs .! #settings .! #compute_allocation @?= 1 - cs .! #settings .! #memory_allocation @?= 1024 * 1024 - cs .! #settings .! #freezing_threshold @?= 1000_000 - ], - testGroup - "anonymous user" - [ simpleTestCase "update, sender absent fails" ecid $ \cid -> - do - envelopeFor anonymousUser $ - rec - [ "request_type" =: GText "call", - "canister_id" =: GBlob cid, - "method_name" =: GText "update", - "arg" =: GBlob (run (replyData caller)) - ] - >>= postCallCBOR cid - >>= code4xx, - simpleTestCase "query, sender absent fails" ecid $ \cid -> - do - envelopeFor anonymousUser $ - rec - [ "request_type" =: GText "query", - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run (replyData caller)) - ] - >>= postQueryCBOR cid - >>= code4xx, - simpleTestCase "update, sender explicit" ecid $ \cid -> - do - awaitCall cid $ - rec - [ "request_type" =: GText "call", - "canister_id" =: GBlob cid, - "sender" =: GBlob anonymousUser, - "method_name" =: GText "update", - "arg" =: GBlob (run (replyData caller)) - ] - >>= isReply - >>= is anonymousUser, - simpleTestCase "query, sender explicit" ecid $ \cid -> - do - let cbor = - rec - [ "request_type" =: GText "query", - "canister_id" =: GBlob cid, - "sender" =: GBlob anonymousUser, - "method_name" =: GText "query", - "arg" =: GBlob (run (replyData caller)) - ] - (rid, res) <- queryCBOR cid cbor - res <- queryResponse res - isQueryReply ecid (rid, res) >>= is anonymousUser - ], - testGroup - "state" - [ simpleTestCase "set/get" ecid $ \cid -> do - call_ cid $ setGlobal "FOO" >>> reply - query cid (replyData getGlobal) >>= is "FOO", - simpleTestCase "set/set/get" ecid $ \cid -> do - call_ cid $ setGlobal "FOO" >>> reply - call_ cid $ setGlobal "BAR" >>> reply - query cid (replyData getGlobal) >>= is "BAR", - simpleTestCase "resubmission" ecid $ \cid -> do - -- Submits the same request (same nonce) twice, checks that - -- the IC does not act twice. - -- (Using growing stable memory as non-idempotent action) - callTwice' cid (ignore (stableGrow (int 1)) >>> reply) >>= isReply >>= is "" - query cid (replyData (i2b stableSize)) >>= is "\1\0\0\0" - ], - simpleTestCase "self" ecid $ \cid -> - query cid (replyData self) >>= is cid, - testGroup - "wrong url path" - [ simpleTestCase "call request to query" ecid $ \cid -> do - let req = - rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "update", - "arg" =: GBlob (run reply) - ] - addNonceExpiryEnv req >>= postQueryCBOR cid >>= code4xx, - simpleTestCase "query request to call" ecid $ \cid -> do - let req = - rec - [ "request_type" =: GText "query", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run reply) - ] - addNonceExpiryEnv req >>= postCallCBOR cid >>= code4xx, - simpleTestCase "query request to read_state" ecid $ \cid -> do - let req = - rec - [ "request_type" =: GText "query", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run reply) - ] - addNonceExpiryEnv req >>= postReadStateCBOR cid >>= code4xx, - simpleTestCase "read_state request to query" ecid $ \cid -> do - addNonceExpiryEnv readStateEmpty >>= postQueryCBOR cid >>= code4xx - ], - testGroup - "wrong effective canister id" - [ simpleTestCase "in call" ecid $ \cid1 -> do - cid2 <- create ecid - let req = - rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid1, - "method_name" =: GText "update", - "arg" =: GBlob (run reply) - ] - addNonceExpiryEnv req >>= postCallCBOR cid2 >>= code4xx, - simpleTestCase "in query" ecid $ \cid1 -> do - cid2 <- create ecid - let req = - rec - [ "request_type" =: GText "query", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid1, - "method_name" =: GText "query", - "arg" =: GBlob (run reply) - ] - addNonceExpiryEnv req >>= postQueryCBOR cid2 >>= code4xx, - simpleTestCase "in read_state" ecid $ \cid -> do - cid2 <- install ecid noop - getStateCert' defaultUser cid2 [["canisters", cid, "controllers"]] >>= isErr4xx, - -- read_state tested in read_state group - -- - simpleTestCase "in management call" ecid $ \cid1 -> do - cid2 <- create ecid - let req = - rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob "", - "method_name" =: GText "canister_status", - "arg" =: GBlob (Candid.encode (#canister_id .== Principal cid1)) - ] - addNonceExpiryEnv req >>= postCallCBOR cid2 >>= code4xx, - simpleTestCase "non-existing (and likely invalid)" ecid $ \cid1 -> do - let req = - rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid1, - "method_name" =: GText "update", - "arg" =: GBlob (run reply) - ] - addNonceExpiryEnv req >>= postCallCBOR "foobar" >>= code4xx, - simpleTestCase "invalid textual representation" ecid $ \cid1 -> do - let req = - rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid1, - "method_name" =: GText "update", - "arg" =: GBlob (run reply) - ] - let path = "/api/v2/canister/" ++ filter (/= '-') (textual cid1) ++ "/call" - addNonceExpiryEnv req >>= postCBOR path >>= code4xx, - testCase "using management canister as effective canister id in update" $ do - let req = - rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob "", - "method_name" =: GText "raw_rand", - "arg" =: GBlob (Candid.encode ()) - ] - addNonceExpiryEnv req >>= postCallCBOR "" >>= code4xx, - testCase "using management canister as effective canister id in query" $ do - let req = - rec - [ "request_type" =: GText "query", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob "", - "method_name" =: GText "raw_rand", - "arg" =: GBlob (Candid.encode ()) - ] - addNonceExpiryEnv req >>= postQueryCBOR "" >>= code4xx, - testCase "using management canister as effective canister id in read_state" $ do - let req = - rec - [ "request_type" =: GText "read_state", - "sender" =: GBlob defaultUser, - "paths" =: GList [GList [GBlob "time"]] - ] - addNonceExpiryEnv req >>= postReadStateCBOR "" >>= code4xx - ], - testGroup - "inter-canister calls" - [ testGroup - "builder interface" - [ testGroup - "traps without call_new" - [ simpleTestCase "call_data_append" ecid $ \cid -> - call' cid (callDataAppend "Foo" >>> reply) >>= isReject [5], - simpleTestCase "call_on_cleanup" ecid $ \cid -> - call' cid (callOnCleanup (callback noop) >>> reply) >>= isReject [5], - simpleTestCase "call_cycles_add" ecid $ \cid -> - call' cid (callCyclesAdd (int64 0) >>> reply) >>= isReject [5], - simpleTestCase "call_perform" ecid $ \cid -> - call' cid (callPerform >>> reply) >>= isReject [5] - ], - simpleTestCase "call_new clears pending call" ecid $ \cid -> do - do - call cid $ - callNew "foo" "bar" "baz" "quux" - >>> callDataAppend "hey" - >>> inter_query cid defArgs - >>= isRelay - >>= isReply - >>= is ("Hello " <> cid <> " this is " <> cid), - simpleTestCase "call_data_append really appends" ecid $ \cid -> do - do - call cid $ - callNew - (bytes cid) - (bytes "query") - (callback relayReply) - (callback relayReject) - >>> callDataAppend (bytes (BS.take 3 (run defaultOtherSide))) - >>> callDataAppend (bytes (BS.drop 3 (run defaultOtherSide))) - >>> callPerform - >>= isRelay - >>= isReply - >>= is ("Hello " <> cid <> " this is " <> cid), - simpleTestCase "call_on_cleanup traps if called twice" ecid $ \cid -> - do - call' cid $ - callNew - (bytes cid) - (bytes "query") - (callback relayReply) - (callback relayReject) - >>> callOnCleanup (callback noop) - >>> callOnCleanup (callback noop) - >>> reply - >>= isReject [5] - ], - simpleTestCase "to nonexistent canister" ecid $ \cid -> - call cid (inter_call "foo" "bar" defArgs) >>= isRelay >>= isReject [3], - simpleTestCase "to nonexistent canister (user id)" ecid $ \cid -> - call cid (inter_call defaultUser "bar" defArgs) >>= isRelay >>= isReject [3], - simpleTestCase "to nonexistent method" ecid $ \cid -> - call cid (inter_call cid "bar" defArgs) >>= isRelay >>= isReject [5], - simpleTestCase "Call from query method traps (in update call)" ecid $ \cid -> - callToQuery'' cid (inter_query cid defArgs) >>= is2xx >>= isReject [5], - simpleTestCase "Call from query method traps (in query call)" ecid $ \cid -> - query' cid (inter_query cid defArgs) >>= isQueryReject ecid [5], - simpleTestCase "Call from query method traps (in inter-canister-call)" ecid $ \cid -> - do - call cid $ - inter_call - cid - "query" - defArgs - { other_side = inter_query cid defArgs - } - >>= isRelay - >>= isReject [5], - simpleTestCase "Self-call (to update)" ecid $ \cid -> - call cid (inter_update cid defArgs) - >>= isRelay - >>= isReply - >>= is ("Hello " <> cid <> " this is " <> cid), - simpleTestCase "Self-call (to query)" ecid $ \cid -> do - call cid (inter_query cid defArgs) - >>= isRelay - >>= isReply - >>= is ("Hello " <> cid <> " this is " <> cid), - simpleTestCase "update commits" ecid $ \cid -> do - do - call cid $ - setGlobal "FOO" - >>> inter_update cid defArgs {other_side = setGlobal "BAR" >>> reply} - >>= isRelay - >>= isReply - >>= is "" - - query cid (replyData getGlobal) >>= is "BAR", - simpleTestCase "query does not commit" ecid $ \cid -> do - do - call cid $ - setGlobal "FOO" - >>> inter_query cid defArgs {other_side = setGlobal "BAR" >>> reply} - >>= isRelay - >>= isReply - >>= is "" - - do query cid $ replyData getGlobal - >>= is "FOO", - simpleTestCase "query no response" ecid $ \cid -> - do call cid $ inter_query cid defArgs {other_side = noop} - >>= isRelay - >>= isReject [5], - simpleTestCase "query double reply" ecid $ \cid -> - do call cid $ inter_query cid defArgs {other_side = reply >>> reply} - >>= isRelay - >>= isReject [5], - simpleTestCase "Reject code is 0 in reply" ecid $ \cid -> - do call cid $ inter_query cid defArgs {on_reply = replyData (i2b reject_code)} - >>= asWord32 - >>= is 0, - simpleTestCase "Second reply in callback" ecid $ \cid -> do - do - call cid $ - setGlobal "FOO" - >>> replyData "First reply" - >>> inter_query - cid - defArgs - { on_reply = setGlobal "BAR" >>> replyData "Second reply", - on_reject = setGlobal "BAZ" >>> relayReject - } - >>= is "First reply" - - -- now check that the callback trapped and did not actual change the global - -- to make this test reliable, stop and start the canister, this will - -- ensure all outstanding callbacks are handled - barrier [cid] - - query cid (replyData getGlobal) >>= is "FOO", - simpleTestCase "partial reply" ecid $ \cid -> - do - call cid $ - replyDataAppend "FOO" - >>> inter_query cid defArgs {on_reply = replyDataAppend "BAR" >>> reply} - >>= is "BAR", -- check that the FOO is silently dropped - simpleTestCase "cleanup not executed when reply callback does not trap" ecid $ \cid -> do - call_ cid $ - inter_query - cid - defArgs - { on_reply = reply, - on_cleanup = Just (setGlobal "BAD") - } - query cid (replyData getGlobal) >>= is "", - simpleTestCase "cleanup not executed when reject callback does not trap" ecid $ \cid -> do - call_ cid $ - inter_query - cid - defArgs - { other_side = reject "meh", - on_reject = reply, - on_cleanup = Just (setGlobal "BAD") - } - query cid (replyData getGlobal) >>= is "", - testGroup - "two callbacks" - [ simpleTestCase "reply after trap" ecid $ \cid -> - do - call cid $ - inter_query cid defArgs {on_reply = trap "first callback traps"} - >>> inter_query cid defArgs {on_reply = replyData "good"} - >>= is "good", - simpleTestCase "trap after reply" ecid $ \cid -> - do - call cid $ - inter_query cid defArgs {on_reply = replyData "good"} - >>> inter_query cid defArgs {on_reply = trap "second callback traps"} - >>= is "good", - simpleTestCase "both trap" ecid $ \cid -> - do - call' cid $ - inter_query cid defArgs {on_reply = trap "first callback traps"} - >>> inter_query cid defArgs {on_reply = trap "second callback traps"} - >>= isReject [5] - ], - simpleTestCase "Call to other canister (to update)" ecid $ \cid -> do - cid2 <- install ecid noop - do call cid $ inter_update cid2 defArgs - >>= isRelay - >>= isReply - >>= is ("Hello " <> cid <> " this is " <> cid2), - simpleTestCase "Call to other canister (to query)" ecid $ \cid -> do - cid2 <- install ecid noop - do call cid $ inter_query cid2 defArgs - >>= isRelay - >>= isReply - >>= is ("Hello " <> cid <> " this is " <> cid2) - ], - testCaseSteps "stable memory" $ \step -> do - cid <- install ecid noop - - step "Stable mem size is zero" - query cid (replyData (i2b stableSize)) >>= is "\x0\x0\x0\x0" - - step "Writing stable memory (failing)" - call' cid (stableWrite (int 0) "FOO") >>= isReject [5] - step "Set stable mem (failing, query)" - query' cid (stableWrite (int 0) "FOO") >>= isQueryReject ecid [5] - - step "Growing stable memory" - call cid (replyData (i2b (stableGrow (int 1)))) >>= is "\x0\x0\x0\x0" - - step "Growing stable memory again" - call cid (replyData (i2b (stableGrow (int 1)))) >>= is "\x1\x0\x0\x0" - - step "Growing stable memory in query" - query cid (replyData (i2b (stableGrow (int 1)))) >>= is "\x2\x0\x0\x0" - - step "Stable mem size is two" - query cid (replyData (i2b stableSize)) >>= is "\x2\x0\x0\x0" - - step "Try growing stable memory beyond 4GiB" - call cid (replyData (i2b (stableGrow (int 65535)))) >>= is "\xff\xff\xff\xff" - - step "Writing stable memory" - call_ cid $ stableWrite (int 0) "FOO" >>> reply - - step "Writing stable memory (query)" - query_ cid $ stableWrite (int 0) "BAR" >>> reply - - step "Reading stable memory" - call cid (replyData (stableRead (int 0) (int 3))) >>= is "FOO", - testCaseSteps "64 bit stable memory" $ \step -> do - cid <- install ecid noop - - step "Stable mem size is zero" - query cid (replyData (i64tob stable64Size)) >>= is "\x0\x0\x0\x0\x0\x0\x0\x0" - - step "Writing stable memory (failing)" - call' cid (stable64Write (int64 0) "FOO") >>= isReject [5] - - step "Set stable mem (failing, query)" - query' cid (stable64Write (int64 0) "FOO") >>= isQueryReject ecid [5] - - step "Growing stable memory" - call cid (replyData (i64tob (stable64Grow (int64 1)))) >>= is "\x0\x0\x0\x0\x0\x0\x0\x0" - - step "Growing stable memory again" - call cid (replyData (i64tob (stable64Grow (int64 1)))) >>= is "\x1\x0\x0\x0\x0\x0\x0\x0" - - step "Growing stable memory in query" - query cid (replyData (i64tob (stable64Grow (int64 1)))) >>= is "\x2\x0\x0\x0\x0\x0\x0\x0" - - step "Stable mem size is two" - query cid (replyData (i2b stableSize)) >>= is "\x2\x0\x0\x0" - query cid (replyData (i64tob stable64Size)) >>= is "\x2\x0\x0\x0\x0\x0\x0\x0" - - step "Writing stable memory" - call_ cid $ stable64Write (int64 0) "FOO" >>> reply - - step "Writing stable memory (query)" - query_ cid $ stable64Write (int64 0) "BAR" >>> reply - - step "Reading stable memory" - call cid (replyData (stable64Read (int64 0) (int64 3))) >>= is "FOO" - call cid (replyData (stableRead (int 0) (int 3))) >>= is "FOO" - - step "Writing in 32 bit mode" - call_ cid $ stableWrite (int 0) "BAR" >>> reply - - step "Reading back in 64 bit mode" - call cid (replyData (stable64Read (int64 0) (int64 3))) >>= is "BAR" - - step "Growing stable memory beyond 4GiB" - call cid (replyData (i64tob (stable64Grow (int64 65535)))) >>= is "\x2\x0\x0\x0\x0\x0\x0\x0" - query cid (replyData (i64tob stable64Size)) >>= is "\x01\x00\x01\x00\x0\x0\x0\x0" - - step "Using 32 bit API with large stable memory" - query' cid (ignore stableSize) >>= isQueryReject ecid [5] - query' cid (ignore $ stableGrow (int 1)) >>= isQueryReject ecid [5] - query' cid (stableWrite (int 0) "BAZ") >>= isQueryReject ecid [5] - query' cid (ignore $ stableRead (int 0) (int 3)) >>= isQueryReject ecid [5] - - step "Using 64 bit API with large stable memory" - call cid (replyData (i64tob (stable64Grow (int64 1)))) >>= is "\x01\x00\x01\x00\x0\x0\x0\x0" - query cid (replyData (i64tob stable64Size)) >>= is "\x02\x00\x01\x00\x0\x0\x0\x0" - call cid (replyData (stable64Read (int64 0) (int64 3))) >>= is "BAR" - call_ cid $ stable64Write (int64 0) "BAZ" >>> reply - call cid (replyData (stable64Read (int64 0) (int64 3))) >>= is "BAZ", - testGroup "time" $ - let getTimeTwice = cat (i64tob getTime) (i64tob getTime) - in [ simpleTestCase "in query" ecid $ \cid -> - query cid (replyData getTimeTwice) >>= as2Word64 >>= bothSame, - simpleTestCase "in update" ecid $ \cid -> - call cid (replyData getTimeTwice) >>= as2Word64 >>= bothSame, - testCase "in install" $ do - cid <- install ecid $ setGlobal getTimeTwice - query cid (replyData getGlobal) >>= as2Word64 >>= bothSame, - testCase "in pre_upgrade" $ do - cid <- - install ecid $ - ignore (stableGrow (int 1)) - >>> onPreUpgrade (callback $ stableWrite (int 0) getTimeTwice) - upgrade cid noop - query cid (replyData (stableRead (int 0) (int (2 * 8)))) >>= as2Word64 >>= bothSame, - simpleTestCase "in post_upgrade" ecid $ \cid -> do - upgrade cid $ setGlobal getTimeTwice - query cid (replyData getGlobal) >>= as2Word64 >>= bothSame - ], - testGroup "canister global timer" $ canister_timer_tests ecid, - testGroup "canister version" $ canister_version_tests ecid, - testGroup "canister history" $ canister_history_tests ecid, - testGroup "is_controller system API" $ - [ simpleTestCase "argument is controller" ecid $ \cid -> do - res <- query cid (replyData $ i2b $ isController (bytes defaultUser)) >>= asWord32 - res @?= 1, - simpleTestCase "argument is not controller" ecid $ \cid -> do - res <- query cid (replyData $ i2b $ isController (bytes "")) >>= asWord32 - res @?= 0, - simpleTestCase "argument is a valid principal" ecid $ \cid -> do - res <- query cid (replyData $ i2b $ isController (bytes $ BS.replicate 29 0)) >>= asWord32 - res @?= 0, - simpleTestCase "argument is not a valid principal" ecid $ \cid -> do - query' cid (replyData $ i2b $ isController (bytes $ BS.replicate 30 0)) >>= isQueryReject ecid [5] - ], - testGroup "upgrades" $ - let installForUpgrade on_pre_upgrade = - install ecid $ - setGlobal "FOO" - >>> ignore (stableGrow (int 1)) - >>> stableWrite (int 0) "BAR______" - >>> onPreUpgrade (callback on_pre_upgrade) - - checkNoUpgrade cid = do - query cid (replyData getGlobal) >>= is "FOO" - query cid (replyData (stableRead (int 0) (int 9))) >>= is "BAR______" - in [ testCase "succeeding" $ do - -- check that the pre-upgrade hook has access to the old state - cid <- installForUpgrade $ stableWrite (int 3) getGlobal - checkNoUpgrade cid - - upgrade cid $ stableWrite (int 6) (stableRead (int 0) (int 3)) - - query cid (replyData getGlobal) >>= is "" - query cid (replyData (stableRead (int 0) (int 9))) >>= is "BARFOOBAR", - testCase "trapping in pre-upgrade" $ do - cid <- installForUpgrade $ trap "trap in pre-upgrade" - checkNoUpgrade cid - - upgrade' cid noop >>= isReject [5] - checkNoUpgrade cid, - testCase "trapping in pre-upgrade (by calling)" $ do - cid <- installForUpgrade $ trap "trap in pre-upgrade" - call_ cid $ - reply - >>> onPreUpgrade - ( callback - ( inter_query cid defArgs {other_side = noop} - ) - ) - checkNoUpgrade cid - - upgrade' cid noop >>= isReject [5] - checkNoUpgrade cid, - testCase "trapping in pre-upgrade (by accessing arg)" $ do - cid <- installForUpgrade $ ignore argData - checkNoUpgrade cid - - upgrade' cid noop >>= isReject [5] - checkNoUpgrade cid, - testCase "trapping in post-upgrade" $ do - cid <- installForUpgrade $ stableWrite (int 3) getGlobal - checkNoUpgrade cid - - upgrade' cid (trap "trap in post-upgrade") >>= isReject [5] - checkNoUpgrade cid, - testCase "trapping in post-upgrade (by calling)" $ do - cid <- installForUpgrade $ stableWrite (int 3) getGlobal - checkNoUpgrade cid - - do upgrade' cid $ inter_query cid defArgs {other_side = noop} - >>= isReject [5] - checkNoUpgrade cid - ], - testGroup - "heartbeat" - [ testCase "called once for all canisters" $ do - cid <- install ecid $ onHeartbeat $ callback $ ignore (stableGrow (int 1)) >>> stableWrite (int 0) "FOO" - cid2 <- install ecid $ onHeartbeat $ callback $ ignore (stableGrow (int 1)) >>> stableWrite (int 0) "BAR" - -- Heartbeat cannot respond. Should be trapped. - cid3 <- install ecid $ onHeartbeat $ callback $ setGlobal "FIZZ" >>> replyData "FIZZ" - - -- The spec currently gives no guarantee about when or how frequent heartbeats are executed. - -- But all implementations have the property: if update call B is submitted after call A is completed, - -- then a heartbeat runs before the execution of B. - -- We use this here to make sure that heartbeats have been attempted: - call_ cid reply - call_ cid reply - - query cid (replyData (stableRead (int 0) (int 3))) >>= is "FOO" - query cid2 (replyData (stableRead (int 0) (int 3))) >>= is "BAR" - query cid3 (replyData getGlobal) >>= is "" - ], - testGroup - "reinstall" - [ testCase "succeeding" $ do - cid <- - install ecid $ - setGlobal "FOO" - >>> ignore (stableGrow (int 1)) - >>> stableWrite (int 0) "FOO______" - query cid (replyData getGlobal) >>= is "FOO" - query cid (replyData (stableRead (int 0) (int 9))) >>= is "FOO______" - query cid (replyData (i2b stableSize)) >>= asWord32 >>= is 1 - - reinstall cid $ - setGlobal "BAR" - >>> ignore (stableGrow (int 2)) - >>> stableWrite (int 0) "BAR______" - - query cid (replyData getGlobal) >>= is "BAR" - query cid (replyData (stableRead (int 0) (int 9))) >>= is "BAR______" - query cid (replyData (i2b stableSize)) >>= asWord32 >>= is 2 - - reinstall cid noop - - query cid (replyData getGlobal) >>= is "" - query cid (replyData (i2b stableSize)) >>= asWord32 >>= is 0, - testCase "trapping" $ do - cid <- - install ecid $ - setGlobal "FOO" - >>> ignore (stableGrow (int 1)) - >>> stableWrite (int 0) "FOO______" - query cid (replyData getGlobal) >>= is "FOO" - query cid (replyData (stableRead (int 0) (int 9))) >>= is "FOO______" - query cid (replyData (i2b stableSize)) >>= is "\1\0\0\0" - - reinstall' cid (trap "Trapping the reinstallation") >>= isReject [5] - - query cid (replyData getGlobal) >>= is "FOO" - query cid (replyData (stableRead (int 0) (int 9))) >>= is "FOO______" - query cid (replyData (i2b stableSize)) >>= is "\1\0\0\0" - ], - testGroup - "uninstall" - [ testCase "uninstall empty canister" $ do - cid <- create ecid - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #running - cs .! #settings .! #controllers @?= Vec.fromList [Principal defaultUser] - cs .! #module_hash @?= Nothing - ic_uninstall ic00 cid - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #running - cs .! #settings .! #controllers @?= Vec.fromList [Principal defaultUser] - cs .! #module_hash @?= Nothing, - testCase "uninstall as wrong user" $ do - cid <- create ecid - ic_uninstall'' otherUser cid >>= isErrOrReject [3, 5], - testCase "uninstall and reinstall wipes state" $ do - cid <- install ecid (setGlobal "FOO") - ic_uninstall ic00 cid - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install ic00 (enum #install) cid universal_wasm (run (setGlobal "BAR")) - query cid (replyData getGlobal) >>= is "BAR", - testCase "uninstall and reinstall wipes stable memory" $ do - cid <- install ecid (ignore (stableGrow (int 1)) >>> stableWrite (int 0) "FOO") - ic_uninstall ic00 cid - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install ic00 (enum #install) cid universal_wasm (run (setGlobal "BAR")) - query cid (replyData (i2b stableSize)) >>= asWord32 >>= is 0 - do - query cid $ - ignore (stableGrow (int 1)) - >>> replyData (stableRead (int 0) (int 3)) - >>= is "\0\0\0" - do - call cid $ - ignore (stableGrow (int 1)) - >>> replyData (stableRead (int 0) (int 3)) - >>= is "\0\0\0", - testCase "uninstall and reinstall wipes certified data" $ do - cid <- install ecid $ setCertifiedData "FOO" - query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO" - ic_uninstall ic00 cid - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install ic00 (enum #install) cid universal_wasm (run noop) - query cid (replyData getCertificate) >>= extractCertData cid >>= is "", - simpleTestCase "uninstalled rejects calls" ecid $ \cid -> do - call cid (replyData "Hi") >>= is "Hi" - query cid (replyData "Hi") >>= is "Hi" - ic_uninstall ic00 cid - -- should be http error, due to inspection - call'' cid (replyData "Hi") >>= isNoErrReject [5] - query' cid (replyData "Hi") >>= isQueryReject ecid [5], - testCaseSteps "open call contexts are rejected" $ \step -> do - cid <- install ecid noop - - step "Create message hold" - (messageHold, release) <- createMessageHold ecid - - step "Create long-running call" - grs1 <- submitCall cid $ callRequest cid messageHold - awaitKnown grs1 >>= isPendingOrProcessing - - step "Uninstall" - ic_uninstall ic00 cid - - step "Long-running call is rejected" - awaitStatus grs1 >>= isReject [4] - - step "Now release" - release - awaitStatus grs1 >>= isReject [4], -- still a reject - testCaseSteps "deleted call contexts prevent stopping" $ \step -> do - cid <- install ecid noop - - step "Create message hold" - (messageHold, release) <- createMessageHold ecid - - step "Create long-running call" - grs1 <- submitCall cid $ callRequest cid messageHold - awaitKnown grs1 >>= isPendingOrProcessing - - step "Uninstall" - ic_uninstall ic00 cid - - step "Long-running call is rejected" - awaitStatus grs1 >>= isReject [4] - - step "Stop" - grs2 <- submitCall cid $ stopRequest cid - awaitKnown grs2 >>= isPendingOrProcessing - - step "Is stopping (via management)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #stopping - - step "Next stop waits, too" - grs3 <- submitCall cid $ stopRequest cid - awaitKnown grs3 >>= isPendingOrProcessing - - step "Release the held message" - release - - step "Wait for calls to complete" - awaitStatus grs1 >>= isReject [4] -- still a reject - awaitStatus grs2 >>= isReply >>= is (Candid.encode ()) - awaitStatus grs3 >>= isReply >>= is (Candid.encode ()) - - step "Is stopped (via management)?" - cs <- ic_canister_status ic00 cid - cs .! #status @?= enum #stopped, - testCaseSteps "deleted call contexts are not delivered" $ \step -> do - -- This is a tricky one: We make one long-running call, - -- then uninstall (rejecting the call), then re-install fresh code, - -- make another long-running call, then release the first one. The system - -- should not confuse the two callbacks. - cid <- install ecid noop - helper <- install ecid noop - - step "Create message holds" - (messageHold1, release1) <- createMessageHold ecid - (messageHold2, release2) <- createMessageHold ecid - - step "Create first long-running call" - grs1 <- - submitCall cid $ - callRequest cid $ - inter_call - helper - "update" - defArgs - { other_side = messageHold1, - on_reply = replyData "First" - } - awaitKnown grs1 >>= isPendingOrProcessing - - step "Uninstall" - ic_uninstall ic00 cid - awaitStatus grs1 >>= isReject [4] - - step "Reinstall" - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install ic00 (enum #install) cid universal_wasm (run (setGlobal "BAR")) - - step "Create second long-running call" - grs2 <- - submitCall cid $ - callRequest cid $ - inter_call - helper - "update" - defArgs - { other_side = messageHold2, - on_reply = replyData "Second" - } - awaitStatus grs1 >>= isReject [4] - awaitKnown grs2 >>= isPendingOrProcessing - - step "Release first call" - release1 - awaitStatus grs1 >>= isReject [4] - awaitKnown grs2 >>= isPendingOrProcessing - - step "Release second call" - release2 - awaitStatus grs1 >>= isReject [4] - awaitStatus grs2 >>= isReply >>= is "Second" - ], - testGroup - "debug facilities" - [ simpleTestCase "Using debug_print" ecid $ \cid -> - call_ cid (debugPrint "ic-ref-test print" >>> reply), - simpleTestCase "Using debug_print (query)" ecid $ \cid -> - query_ cid $ debugPrint "ic-ref-test print" >>> reply, - simpleTestCase "Using debug_print with invalid bounds" ecid $ \cid -> - query_ cid $ badPrint >>> reply, - simpleTestCase "Explicit trap" ecid $ \cid -> - call' cid (trap "trapping") >>= isReject [5], - simpleTestCase "Explicit trap (query)" ecid $ \cid -> do - query' cid (trap "trapping") >>= isQueryReject ecid [5] - ], - testCase "caller (in init)" $ do - cid <- install ecid $ setGlobal caller - query cid (replyData getGlobal) >>= is defaultUser, - testCase "self (in init)" $ do - cid <- install ecid $ setGlobal self - query cid (replyData getGlobal) >>= is cid, - testGroup "trapping in init" $ - let failInInit pgm = do - cid <- create ecid - install' cid pgm >>= isReject [5] - -- canister does not exist - query' cid noop >>= isQueryReject ecid [5] - in [ testCase "explicit trap" $ failInInit $ trap "trapping in install", - testCase "call" $ failInInit $ inter_query "dummy" defArgs, - testCase "reply" $ failInInit reply, - testCase "reject" $ failInInit $ reject "rejecting in init" - ], - testGroup - "query" - [ testGroup "required fields" $ do - -- TODO: Begin with a succeeding request to a real canister, to rule - -- out other causes of failure than missing fields - omitFields queryToNonExistent $ \req -> do - cid <- create ecid - addExpiry req >>= envelope defaultSK >>= postQueryCBOR cid >>= code4xx, - simpleTestCase "non-existing (deleted) canister" ecid $ \cid -> do - ic_stop_canister ic00 cid - ic_delete_canister ic00 cid - query' cid reply >>= isQueryReject ecid [3], - simpleTestCase "does not commit" ecid $ \cid -> do - call_ cid (setGlobal "FOO" >>> reply) - query cid (setGlobal "BAR" >>> replyData getGlobal) >>= is "BAR" - query cid (replyData getGlobal) >>= is "FOO" - ], - testGroup "read state" $ - let ensure_request_exists cid user = do - req <- - addNonce >=> addExpiry $ - rec - [ "request_type" =: GText "call", - "sender" =: GBlob user, - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run (replyData "\xff\xff")) - ] - awaitCall cid req >>= isReply >>= is "\xff\xff" - - -- check that the request is there - getRequestStatus user cid (requestId req) >>= is (Responded (Reply "\xff\xff")) - - return (requestId req) - ensure_provisional_create_canister_request_exists ecid user = do - let arg :: ProvisionalCreateCanisterArgs = - empty - .+ #amount - .== Just initial_cycles - .+ #settings - .== Nothing - .+ #specified_id - .== Nothing - .+ #sender_canister_version - .== Nothing - req <- - addNonce >=> addExpiry $ - rec - [ "request_type" =: GText "call", - "sender" =: GBlob user, - "canister_id" =: GBlob "", - "method_name" =: GText "provisional_create_canister_with_cycles", - "arg" =: GBlob (Candid.encode arg) - ] - _ <- awaitCall ecid req >>= isReply - - -- check that the request is there - getRequestStatus user ecid (requestId req) >>= isResponded - - return (requestId req) - in [ testGroup "required fields" $ - omitFields readStateEmpty $ \req -> do - cid <- create ecid - addExpiry req >>= envelope defaultSK >>= postReadStateCBOR cid >>= code4xx, - simpleTestCase "certificate validates" ecid $ \cid -> do - cert <- getStateCert defaultUser cid [] - validateStateCert cid cert, - simpleTestCase "certificate does not validate if canister range check fails" ecid $ \cid -> do - unless my_is_root $ do - cert <- getStateCert defaultUser cid [] - result <- try (validateStateCert other_ecid cert) :: IO (Either DelegationCanisterRangeCheck ()) - assertBool "certificate should not validate" $ isLeft result, - testCaseSteps "time is present" $ \step -> do - cid <- create ecid - cert <- getStateCert defaultUser cid [] - time <- certValue @Natural cert ["time"] - step $ "Reported time: " ++ show time, - testCase "time can be asked for" $ do - cid <- create ecid - cert <- getStateCert defaultUser cid [["time"]] - void $ certValue @Natural cert ["time"], - testCase "can ask for /subnet" $ do - cert <- getStateCert defaultUser ecid [["subnet"]] - void $ certValue @Blob cert ["subnet", my_subnet_id, "public_key"] - void $ certValue @Blob cert ["subnet", my_subnet_id, "canister_ranges"] - void $ certValue @Blob cert ["subnet", other_subnet_id, "public_key"] - void $ certValue @Blob cert ["subnet", other_subnet_id, "canister_ranges"] - void $ forM my_nodes $ \nid -> do - void $ certValue @Blob cert ["subnet", my_subnet_id, "node", rawEntityId nid, "public_key"] - void $ forM other_nodes $ \nid -> do - certValueAbsent cert ["subnet", other_subnet_id, "node", rawEntityId nid, "public_key"], - testCase "controller of empty canister" $ do - cid <- create ecid - cert <- getStateCert defaultUser cid [["canister", cid, "controllers"]] - certValue @Blob cert ["canister", cid, "controllers"] >>= asCBORBlobList >>= isSet [defaultUser], - testCase "module_hash of empty canister" $ do - cid <- create ecid - cert <- getStateCert defaultUser cid [["canister", cid, "module_hash"]] - lookupPath (cert_tree cert) ["canister", cid, "module_hash"] @?= Absent, - testCase "single controller of installed canister" $ do - cid <- install ecid noop - -- also vary user, just for good measure - cert <- getStateCert anonymousUser cid [["canister", cid, "controllers"]] - certValue @Blob cert ["canister", cid, "controllers"] >>= asCBORBlobList >>= isSet [defaultUser], - testCase "multiple controllers of installed canister" $ do - cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing - ic_set_controllers ic00 cid [defaultUser, otherUser] - cert <- getStateCert defaultUser cid [["canister", cid, "controllers"]] - certValue @Blob cert ["canister", cid, "controllers"] >>= asCBORBlobList >>= isSet [defaultUser, otherUser], - testCase "zero controllers of installed canister" $ do - cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing - ic_set_controllers ic00 cid [] - cert <- getStateCert defaultUser cid [["canister", cid, "controllers"]] - certValue @Blob cert ["canister", cid, "controllers"] >>= asCBORBlobList >>= isSet [], - testCase "module_hash of universal canister" $ do - cid <- install ecid noop - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - cert <- getStateCert anonymousUser cid [["canister", cid, "module_hash"]] - certValue @Blob cert ["canister", cid, "module_hash"] >>= is (sha256 universal_wasm), - testGroup - "malformed request id" - [ simpleTestCase ("rid \"" ++ shorten 8 (asHex rid) ++ "\"") ecid $ \cid -> do - getStateCert' defaultUser cid [["request_status", rid]] >>= isErr4xx - | rid <- ["", "foo"] - ], - testGroup - "non-existence proofs for non-existing request id" - [ simpleTestCase ("rid \"" ++ shorten 8 (asHex rid) ++ "\"") ecid $ \cid -> do - cert <- getStateCert defaultUser cid [["request_status", rid]] - certValueAbsent cert ["request_status", rid, "status"] - | rid <- [BS.replicate 32 0, BS.replicate 32 8, BS.replicate 32 255] - ], - simpleTestCase "can ask for portion of request status" ecid $ \cid -> do - rid <- ensure_request_exists cid defaultUser - cert <- getStateCert defaultUser cid [["request_status", rid, "status"], ["request_status", rid, "reply"]] - void $ certValue @T.Text cert ["request_status", rid, "status"] - void $ certValue @Blob cert ["request_status", rid, "reply"], - simpleTestCase "access denied for other users request" ecid $ \cid -> do - rid <- ensure_request_exists cid defaultUser - getStateCert' otherUser cid [["request_status", rid]] >>= isErr4xx, - simpleTestCase "reading two statuses to same canister in one go" ecid $ \cid -> do - rid1 <- ensure_request_exists cid defaultUser - rid2 <- ensure_request_exists cid defaultUser - getStateCert' defaultUser cid [["request_status", rid1], ["request_status", rid2]] >>= isErr4xx, - simpleTestCase "access denied for other users request (mixed request)" ecid $ \cid -> do - rid1 <- ensure_request_exists cid defaultUser - rid2 <- ensure_request_exists cid otherUser - getStateCert' defaultUser cid [["request_status", rid1], ["request_status", rid2]] >>= isErr4xx, - simpleTestCase "access denied for two statuses to different canisters" ecid $ \cid -> do - cid2 <- install ecid noop - rid1 <- ensure_request_exists cid defaultUser - rid2 <- ensure_request_exists cid2 defaultUser - getStateCert' defaultUser cid [["request_status", rid1], ["request_status", rid2]] >>= isErr4xx, - simpleTestCase "access denied with different effective canister id" ecid $ \cid -> do - cid2 <- install ecid noop - rid <- ensure_provisional_create_canister_request_exists cid defaultUser - getStateCert' defaultUser cid2 [["request_status", rid]] >>= isErr4xx, - simpleTestCase "access denied for bogus path" ecid $ \cid -> do - getStateCert' otherUser cid [["hello", "world"]] >>= isErr4xx, - simpleTestCase "access denied for fetching full state tree" ecid $ \cid -> do - getStateCert' otherUser cid [[]] >>= isErr4xx, - testGroup "metadata" $ - let withCustomSection mod (name, content) = mod <> BS.singleton 0 <> sized (sized name <> content) - where - sized x = BS.fromStrict (toLEB128 @Natural (fromIntegral (BS.length x))) <> x - withSections xs = foldl withCustomSection trivialWasmModule xs - in [ simpleTestCase "absent" ecid $ \cid -> do - cert <- getStateCert defaultUser cid [["canister", cid, "metadata", "foo"]] - lookupPath (cert_tree cert) ["canister", cid, "metadata", "foo"] @?= Absent, - testCase "public" $ do - let mod = withSections [("icp:public test", "bar")] - cid <- create ecid - ic_install ic00 (enum #install) cid mod "" - cert <- getStateCert otherUser cid [["canister", cid, "metadata", "test"]] - lookupPath (cert_tree cert) ["canister", cid, "metadata", "test"] @?= Found "bar" - cert <- getStateCert anonymousUser cid [["canister", cid, "metadata", "test"]] - lookupPath (cert_tree cert) ["canister", cid, "metadata", "test"] @?= Found "bar", - testCase "private" $ do - let mod = withSections [("icp:private test", "bar")] - cid <- create ecid - ic_install ic00 (enum #install) cid mod "" - getStateCert' otherUser cid [["canister", cid, "metadata", "test"]] >>= isErr4xx - getStateCert' anonymousUser cid [["canister", cid, "metadata", "test"]] >>= isErr4xx - cert <- getStateCert defaultUser cid [["canister", cid, "metadata", "test"]] - lookupPath (cert_tree cert) ["canister", cid, "metadata", "test"] @?= Found "bar", - testCase "duplicate public" $ do - let mod = withSections [("icp:public test", "bar"), ("icp:public test", "baz")] - cid <- create ecid - ic_install' ic00 (enum #install) cid mod "" >>= isReject [5], - testCase "duplicate private" $ do - let mod = withSections [("icp:private test", "bar"), ("icp:private test", "baz")] - cid <- create ecid - ic_install' ic00 (enum #install) cid mod "" >>= isReject [5], - testCase "duplicate mixed" $ do - let mod = withSections [("icp:private test", "bar"), ("icp:public test", "baz")] - cid <- create ecid - ic_install' ic00 (enum #install) cid mod "" >>= isReject [5], - testCase "invalid utf8 in module" $ do - let mod = withSections [("icp:public \xe2\x28\xa1", "baz")] - cid <- create ecid - ic_install' ic00 (enum #install) cid mod "" >>= isReject [5], - simpleTestCase "invalid utf8 in read_state" ecid $ \cid -> do - getStateCert' defaultUser cid [["canister", cid, "metadata", "\xe2\x28\xa1"]] >>= isErr4xx, - testCase "unicode metadata name" $ do - let mod = withSections [("icp:public ☃️", "bar")] - cid <- create ecid - ic_install ic00 (enum #install) cid mod "" - cert <- getStateCert anonymousUser cid [["canister", cid, "metadata", "☃️"]] - lookupPath (cert_tree cert) ["canister", cid, "metadata", "☃️"] @?= Found "bar", - testCase "zero-length metadata name" $ do - let mod = withSections [("icp:public ", "bar")] - cid <- create ecid - ic_install ic00 (enum #install) cid mod "" - cert <- getStateCert anonymousUser cid [["canister", cid, "metadata", ""]] - lookupPath (cert_tree cert) ["canister", cid, "metadata", ""] @?= Found "bar", - testCase "metadata section name with spaces" $ do - let mod = withSections [("icp:public metadata section name with spaces", "bar")] - cid <- create ecid - ic_install ic00 (enum #install) cid mod "" - cert <- getStateCert anonymousUser cid [["canister", cid, "metadata", "metadata section name with spaces"]] - lookupPath (cert_tree cert) ["canister", cid, "metadata", "metadata section name with spaces"] @?= Found "bar" - ] - ], - testGroup - "certified variables" - [ simpleTestCase "initially empty" ecid $ \cid -> do - query cid (replyData getCertificate) >>= extractCertData cid >>= is "", - simpleTestCase "validates" ecid $ \cid -> do - query cid (replyData getCertificate) - >>= decodeCert' - >>= validateStateCert cid, - simpleTestCase "present in query method (query call)" ecid $ \cid -> do - query cid (replyData (i2b getCertificatePresent)) - >>= is "\1\0\0\0", - simpleTestCase "not present in query method (update call)" ecid $ \cid -> do - callToQuery'' cid (replyData (i2b getCertificatePresent)) - >>= is2xx - >>= isReply - >>= is "\0\0\0\0", - simpleTestCase "not present in query method (inter-canister call)" ecid $ \cid -> do - do - call cid $ - inter_call - cid - "query" - defArgs - { other_side = replyData (i2b getCertificatePresent) - } - >>= isRelay - >>= isReply - >>= is "\0\0\0\0", - simpleTestCase "not present in update method" ecid $ \cid -> do - call cid (replyData (i2b getCertificatePresent)) - >>= is "\0\0\0\0", - simpleTestCase "set and get" ecid $ \cid -> do - call_ cid $ setCertifiedData "FOO" >>> reply - query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", - simpleTestCase "set twice" ecid $ \cid -> do - call_ cid $ setCertifiedData "FOO" >>> setCertifiedData "BAR" >>> reply - query cid (replyData getCertificate) >>= extractCertData cid >>= is "BAR", - simpleTestCase "set then trap" ecid $ \cid -> do - call_ cid $ setCertifiedData "FOO" >>> reply - call' cid (setCertifiedData "BAR" >>> trap "Trapped") >>= isReject [5] - query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", - simpleTestCase "too large traps, old value retained" ecid $ \cid -> do - call_ cid $ setCertifiedData "FOO" >>> reply - call' cid (setCertifiedData (bytes (BS.replicate 33 0x42)) >>> reply) - >>= isReject [5] - query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", - testCase "set in init" $ do - cid <- install ecid $ setCertifiedData "FOO" - query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", - testCase "set in pre-upgrade" $ do - cid <- install ecid $ onPreUpgrade (callback $ setCertifiedData "FOO") - upgrade cid noop - query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", - simpleTestCase "set in post-upgrade" ecid $ \cid -> do - upgrade cid $ setCertifiedData "FOO" - query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", - simpleTestCase "cleared in reinstall" ecid $ \cid -> do - call_ cid $ setCertifiedData "FOO" >>> reply - query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO" - reinstall cid noop - query cid (replyData getCertificate) >>= extractCertData cid >>= is "", - simpleTestCase "cleared in uninstall" ecid $ \cid -> do - call_ cid $ setCertifiedData "FOO" >>> reply - query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO" - ic_uninstall ic00 cid - installAt cid noop - query cid (replyData getCertificate) >>= extractCertData cid >>= is "" - ], - testGroup "cycles" $ - let replyBalance = replyData (i64tob getBalance) - replyBalance128 = replyData getBalance128 - replyBalanceBalance128 = replyDataAppend (i64tob getBalance) >>> replyDataAppend getBalance128 >>> reply - rememberBalance = - ignore (stableGrow (int 1)) - >>> stableWrite (int 0) (i64tob getBalance) - recallBalance = replyData (stableRead (int 0) (int 8)) - acceptAll = ignore (acceptCycles getAvailableCycles) - queryBalance cid = query cid replyBalance >>= asWord64 - queryBalance128 cid = query cid replyBalance128 >>= asWord128 - queryBalanceBalance128 cid = query cid replyBalanceBalance128 >>= asWord64Word128 - - -- At the time of writing, creating a canister needs at least 1T - -- and the freezing limit is 5T - -- (At some point, the max was 100T, but that is no longer the case) - -- So lets try to stay away from these limits. - -- The lowest denomination we deal with below is def_cycles`div`4 - def_cycles = 80_000_000_000_000 :: Word64 - - -- The system burns cycles at unspecified rates. To cater for such behaviour, - -- we make the assumption that no test burns more than the following epsilon. - -- - -- The biggest fee we currently deal with is the system deducing 1T - -- upon canister creation. So our epsilon needs to allow that and then - -- some more. - eps = 3_000_000_000_000 :: Integer - - isRoughly :: (HasCallStack, Show a, Num a, Integral a, Show b, Num b, Integral b) => a -> b -> Assertion - isRoughly exp act = - assertBool - (show act ++ " not near " ++ show exp) - (abs (fromIntegral exp - fromIntegral act) < eps) - - create prog = do - cid <- ic_provisional_create ic00 ecid Nothing (Just (fromIntegral def_cycles)) Nothing - installAt cid prog - return cid - create_via cid initial_cycles = do - cid2 <- ic_create (ic00viaWithCycles cid initial_cycles) ecid Nothing - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install (ic00via cid) (enum #install) cid2 universal_wasm (run noop) - return cid2 - in [ testGroup "cycles API - backward compatibility" $ - [ simpleTestCase "canister_cycle_balance = canister_cycle_balance128 for numbers fitting in 64 bits" ecid $ \cid -> do - (a, b) <- queryBalanceBalance128 cid - bothSame (a, fromIntegral b), - testCase "legacy API traps when a result is too big" $ do - cid <- create noop - let large = 2 ^ (65 :: Int) - ic_top_up ic00 cid large - query' cid replyBalance >>= isQueryReject ecid [5] - queryBalance128 cid >>= isRoughly (large + fromIntegral def_cycles) - ], - testGroup "can use balance API" $ - let getBalanceTwice = join cat (i64tob getBalance) - test = replyData getBalanceTwice - in [ simpleTestCase "in query" ecid $ \cid -> - query cid test >>= as2Word64 >>= bothSame, - simpleTestCase "in update" ecid $ \cid -> - call cid test >>= as2Word64 >>= bothSame, - testCase "in init" $ do - cid <- install ecid (setGlobal getBalanceTwice) - query cid (replyData getGlobal) >>= as2Word64 >>= bothSame, - simpleTestCase "in callback" ecid $ \cid -> - call cid (inter_query cid defArgs {on_reply = test}) >>= as2Word64 >>= bothSame - ], - testGroup "can use available cycles API" $ - let getAvailableCyclesTwice = join cat (i64tob getAvailableCycles) - test = replyData getAvailableCyclesTwice - in [ simpleTestCase "in update" ecid $ \cid -> - call cid test >>= as2Word64 >>= bothSame, - simpleTestCase "in callback" ecid $ \cid -> - call cid (inter_query cid defArgs {on_reply = test}) >>= as2Word64 >>= bothSame - ], - simpleTestCase "can accept zero cycles" ecid $ \cid -> - call cid (replyData (i64tob (acceptCycles (int64 0)))) >>= asWord64 >>= is 0, - simpleTestCase "can accept more than available cycles" ecid $ \cid -> - call cid (replyData (i64tob (acceptCycles (int64 1)))) >>= asWord64 >>= is 0, - simpleTestCase "can accept absurd amount of cycles" ecid $ \cid -> - call cid (replyData (acceptCycles128 (int64 maxBound) (int64 maxBound))) >>= asWord128 >>= is 0, - testGroup - "provisional_create_canister_with_cycles" - [ testCase "balance as expected" $ do - cid <- create noop - queryBalance cid >>= isRoughly def_cycles, - testCaseSteps "default (i.e. max) balance" $ \step -> do - cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing - installAt cid noop - cycles <- queryBalance128 cid - step $ "Cycle balance now at " ++ show cycles, - testCaseSteps "> 2^128 succeeds" $ \step -> do - cid <- ic_provisional_create ic00 ecid Nothing (Just (10 * 2 ^ (128 :: Int))) Nothing - installAt cid noop - cycles <- queryBalance128 cid - step $ "Cycle balance now at " ++ show cycles - ], - testCase "cycles in canister_status" $ do - cid <- create noop - cs <- ic_canister_status ic00 cid - isRoughly def_cycles (cs .! #cycles), - testGroup - "cycle balance" - [ testCase "install" $ do - cid <- create rememberBalance - query cid recallBalance >>= asWord64 >>= isRoughly def_cycles, - testCase "update" $ do - cid <- create noop - call cid replyBalance >>= asWord64 >>= isRoughly def_cycles, - testCase "query" $ do - cid <- create noop - query cid replyBalance >>= asWord64 >>= isRoughly def_cycles, - testCase "in pre_upgrade" $ do - cid <- create $ onPreUpgrade (callback rememberBalance) - upgrade cid noop - query cid recallBalance >>= asWord64 >>= isRoughly def_cycles, - testCase "in post_upgrade" $ do - cid <- create noop - upgrade cid rememberBalance - query cid recallBalance >>= asWord64 >>= isRoughly def_cycles - queryBalance cid >>= isRoughly def_cycles - ], - testCase "can send cycles" $ do - cid1 <- create noop - cid2 <- create noop - do - call cid1 $ - inter_call - cid2 - "update" - defArgs - { other_side = - replyDataAppend (i64tob getAvailableCycles) - >>> acceptAll - >>> reply, - cycles = def_cycles `div` 4 - } - >>= isRelay - >>= isReply - >>= asWord64 - >>= isRoughly (def_cycles `div` 4) - queryBalance cid1 >>= isRoughly (def_cycles - def_cycles `div` 4) - queryBalance cid2 >>= isRoughly (def_cycles + def_cycles `div` 4), - testCase "sending more cycles than in balance traps" $ do - cid <- create noop - cycles <- queryBalance cid - call' cid (inter_call cid cid defArgs {cycles = cycles + 1000_000}) - >>= isReject [5], - testCase "relay cycles before accept traps" $ do - cid1 <- create noop - cid2 <- create noop - cid3 <- create noop - do - call cid1 $ - inter_call - cid2 - "update" - defArgs - { cycles = def_cycles `div` 2, - other_side = - inter_call - cid3 - "update" - defArgs - { other_side = acceptAll >>> reply, - cycles = def_cycles + def_cycles `div` 4, - on_reply = noop -- must not double reply - } - >>> acceptAll - >>> reply, - on_reply = trap "unexpected reply", - on_reject = replyData (i64tob getRefund) - } - >>= asWord64 - >>= isRoughly (def_cycles `div` 2) - queryBalance cid1 >>= isRoughly def_cycles - queryBalance cid2 >>= isRoughly def_cycles - queryBalance cid3 >>= isRoughly def_cycles, - testCase "relay cycles after accept works" $ do - cid1 <- create noop - cid2 <- create noop - cid3 <- create noop - do - call cid1 $ - inter_call - cid2 - "update" - defArgs - { cycles = def_cycles `div` 2, - other_side = - acceptAll - >>> inter_call - cid3 - "update" + ++ ( let invalid_compute_allocation :: CanisterSettings = + empty + .+ #controllers + .== Nothing + .+ #compute_allocation + .== Just 101 + .+ #memory_allocation + .== Nothing + .+ #freezing_threshold + .== Nothing + .+ #reserved_cycles_limit + .== Nothing + .+ #log_visibility + .== Nothing + .+ #wasm_memory_limit + .== Nothing + in let invalid_memory_allocation :: CanisterSettings = + empty + .+ #controllers + .== Nothing + .+ #compute_allocation + .== Nothing + .+ #memory_allocation + .== Just (2 ^ 48 + 1) + .+ #freezing_threshold + .== Nothing + .+ #reserved_cycles_limit + .== Nothing + .+ #log_visibility + .== Nothing + .+ #wasm_memory_limit + .== Nothing + in let invalid_freezing_threshold :: CanisterSettings = + empty + .+ #controllers + .== Nothing + .+ #compute_allocation + .== Nothing + .+ #memory_allocation + .== Nothing + .+ #freezing_threshold + .== Just (2 ^ 64) + .+ #reserved_cycles_limit + .== Nothing + .+ #log_visibility + .== Nothing + .+ #wasm_memory_limit + .== Nothing + in let invalid_settings = + [ ("Invalid compute allocation (101)", invalid_compute_allocation), + ("Invalid memory allocation (2^48+1)", invalid_memory_allocation), + ("Invalid freezing threshold (2^64)", invalid_freezing_threshold) + ] + in let test_modes = + [ ( "via provisional_create_canister_with_cycles:", + \(desc, settings) -> testCase desc $ do + ic_provisional_create' ic00 ecid Nothing Nothing (Just settings) >>= isReject [5] + ), + ( "via create_canister:", + \(desc, settings) -> simpleTestCase desc ecid $ \cid -> do + ic_create' (ic00via cid) ecid (Just settings) >>= isReject [5] + ), + ( "via update_settings", + \(desc, settings) -> simpleTestCase desc ecid $ \cid -> do + ic_update_settings' ic00 cid settings >>= isReject [5] + ) + ] + in map (\(desc, test) -> testGroup desc (map test invalid_settings)) test_modes + ), + simpleTestCase "Valid allocations" ecid $ \cid -> do + let settings :: CanisterSettings = + empty + .+ #controllers + .== Nothing + .+ #compute_allocation + .== Just 1 + .+ #memory_allocation + .== Just (1024 * 1024) + .+ #freezing_threshold + .== Just 1000_000 + .+ #reserved_cycles_limit + .== Nothing + .+ #log_visibility + .== Nothing + .+ #wasm_memory_limit + .== Nothing + cid2 <- ic_create (ic00viaWithCycles cid 20_000_000_000_000) ecid (Just settings) + cs <- ic_canister_status (ic00via cid) cid2 + cs .! #settings .! #compute_allocation @?= 1 + cs .! #settings .! #memory_allocation @?= 1024 * 1024 + cs .! #settings .! #freezing_threshold @?= 1000_000 + ], + testGroup + "anonymous user" + [ simpleTestCase "update, sender absent fails" ecid $ \cid -> + do + envelopeFor anonymousUser $ + rec + [ "request_type" =: GText "call", + "canister_id" =: GBlob cid, + "method_name" =: GText "update", + "arg" =: GBlob (run (replyData caller)) + ] + >>= postCallCBOR cid + >>= code4xx, + simpleTestCase "query, sender absent fails" ecid $ \cid -> + do + envelopeFor anonymousUser $ + rec + [ "request_type" =: GText "query", + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run (replyData caller)) + ] + >>= postQueryCBOR cid + >>= code4xx, + simpleTestCase "update, sender explicit" ecid $ \cid -> + do + awaitCall cid $ + rec + [ "request_type" =: GText "call", + "canister_id" =: GBlob cid, + "sender" =: GBlob anonymousUser, + "method_name" =: GText "update", + "arg" =: GBlob (run (replyData caller)) + ] + >>= isReply + >>= is anonymousUser, + simpleTestCase "query, sender explicit" ecid $ \cid -> + do + let cbor = + rec + [ "request_type" =: GText "query", + "canister_id" =: GBlob cid, + "sender" =: GBlob anonymousUser, + "method_name" =: GText "query", + "arg" =: GBlob (run (replyData caller)) + ] + (rid, res) <- queryCBOR cid cbor + res <- queryResponse res + isQueryReply ecid (rid, res) >>= is anonymousUser + ], + testGroup + "state" + [ simpleTestCase "set/get" ecid $ \cid -> do + call_ cid $ setGlobal "FOO" >>> reply + query cid (replyData getGlobal) >>= is "FOO", + simpleTestCase "set/set/get" ecid $ \cid -> do + call_ cid $ setGlobal "FOO" >>> reply + call_ cid $ setGlobal "BAR" >>> reply + query cid (replyData getGlobal) >>= is "BAR", + simpleTestCase "resubmission" ecid $ \cid -> do + -- Submits the same request (same nonce) twice, checks that + -- the IC does not act twice. + -- (Using growing stable memory as non-idempotent action) + callTwice' cid (ignore (stableGrow (int 1)) >>> reply) >>= isReply >>= is "" + query cid (replyData (i2b stableSize)) >>= is "\1\0\0\0" + ], + simpleTestCase "self" ecid $ \cid -> + query cid (replyData self) >>= is cid, + testGroup + "wrong url path" + [ simpleTestCase "call request to query" ecid $ \cid -> do + let req = + rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "update", + "arg" =: GBlob (run reply) + ] + addNonceExpiryEnv req >>= postQueryCBOR cid >>= code4xx, + simpleTestCase "query request to call" ecid $ \cid -> do + let req = + rec + [ "request_type" =: GText "query", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run reply) + ] + addNonceExpiryEnv req >>= postCallCBOR cid >>= code4xx, + simpleTestCase "query request to read_state" ecid $ \cid -> do + let req = + rec + [ "request_type" =: GText "query", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run reply) + ] + addNonceExpiryEnv req >>= postReadStateCBOR cid >>= code4xx, + simpleTestCase "read_state request to query" ecid $ \cid -> do + addNonceExpiryEnv readStateEmpty >>= postQueryCBOR cid >>= code4xx + ], + testGroup + "wrong effective canister id" + [ simpleTestCase "in call" ecid $ \cid1 -> do + cid2 <- create ecid + let req = + rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid1, + "method_name" =: GText "update", + "arg" =: GBlob (run reply) + ] + addNonceExpiryEnv req >>= postCallCBOR cid2 >>= code4xx, + simpleTestCase "in query" ecid $ \cid1 -> do + cid2 <- create ecid + let req = + rec + [ "request_type" =: GText "query", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid1, + "method_name" =: GText "query", + "arg" =: GBlob (run reply) + ] + addNonceExpiryEnv req >>= postQueryCBOR cid2 >>= code4xx, + simpleTestCase "in read_state" ecid $ \cid -> do + cid2 <- install ecid noop + getStateCert' defaultUser cid2 [["canisters", cid, "controllers"]] >>= isErr4xx, + -- read_state tested in read_state group + -- + simpleTestCase "in management call" ecid $ \cid1 -> do + cid2 <- create ecid + let req = + rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob "", + "method_name" =: GText "canister_status", + "arg" =: GBlob (Candid.encode (#canister_id .== Principal cid1)) + ] + addNonceExpiryEnv req >>= postCallCBOR cid2 >>= code4xx, + simpleTestCase "non-existing (and likely invalid)" ecid $ \cid1 -> do + let req = + rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid1, + "method_name" =: GText "update", + "arg" =: GBlob (run reply) + ] + addNonceExpiryEnv req >>= postCallCBOR "foobar" >>= code4xx, + simpleTestCase "invalid textual representation" ecid $ \cid1 -> do + let req = + rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid1, + "method_name" =: GText "update", + "arg" =: GBlob (run reply) + ] + let path = "/api/v2/canister/" ++ filter (/= '-') (textual cid1) ++ "/call" + addNonceExpiryEnv req >>= postCBOR path >>= code4xx, + testCase "using management canister as effective canister id in update" $ do + let req = + rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob "", + "method_name" =: GText "raw_rand", + "arg" =: GBlob (Candid.encode ()) + ] + addNonceExpiryEnv req >>= postCallCBOR "" >>= code4xx, + testCase "using management canister as effective canister id in query" $ do + let req = + rec + [ "request_type" =: GText "query", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob "", + "method_name" =: GText "raw_rand", + "arg" =: GBlob (Candid.encode ()) + ] + addNonceExpiryEnv req >>= postQueryCBOR "" >>= code4xx, + testCase "using management canister as effective canister id in read_state" $ do + let req = + rec + [ "request_type" =: GText "read_state", + "sender" =: GBlob defaultUser, + "paths" =: GList [GList [GBlob "time"]] + ] + addNonceExpiryEnv req >>= postReadStateCBOR "" >>= code4xx + ], + testGroup + "inter-canister calls" + [ testGroup + "builder interface" + [ testGroup + "traps without call_new" + [ simpleTestCase "call_data_append" ecid $ \cid -> + call' cid (callDataAppend "Foo" >>> reply) >>= isReject [5], + simpleTestCase "call_on_cleanup" ecid $ \cid -> + call' cid (callOnCleanup (callback noop) >>> reply) >>= isReject [5], + simpleTestCase "call_cycles_add" ecid $ \cid -> + call' cid (callCyclesAdd (int64 0) >>> reply) >>= isReject [5], + simpleTestCase "call_perform" ecid $ \cid -> + call' cid (callPerform >>> reply) >>= isReject [5] + ], + simpleTestCase "call_new clears pending call" ecid $ \cid -> do + do + call cid $ + callNew "foo" "bar" "baz" "quux" + >>> callDataAppend "hey" + >>> inter_query cid defArgs + >>= isRelay + >>= isReply + >>= is ("Hello " <> cid <> " this is " <> cid), + simpleTestCase "call_data_append really appends" ecid $ \cid -> do + do + call cid $ + callNew + (bytes cid) + (bytes "query") + (callback relayReply) + (callback relayReject) + >>> callDataAppend (bytes (BS.take 3 (run defaultOtherSide))) + >>> callDataAppend (bytes (BS.drop 3 (run defaultOtherSide))) + >>> callPerform + >>= isRelay + >>= isReply + >>= is ("Hello " <> cid <> " this is " <> cid), + simpleTestCase "call_on_cleanup traps if called twice" ecid $ \cid -> + do + call' cid $ + callNew + (bytes cid) + (bytes "query") + (callback relayReply) + (callback relayReject) + >>> callOnCleanup (callback noop) + >>> callOnCleanup (callback noop) + >>> reply + >>= isReject [5] + ], + simpleTestCase "to nonexistent canister" ecid $ \cid -> + call cid (inter_call "foo" "bar" defArgs) >>= isRelay >>= isReject [3], + simpleTestCase "to nonexistent canister (user id)" ecid $ \cid -> + call cid (inter_call defaultUser "bar" defArgs) >>= isRelay >>= isReject [3], + simpleTestCase "to nonexistent method" ecid $ \cid -> + call cid (inter_call cid "bar" defArgs) >>= isRelay >>= isReject [5], + simpleTestCase "Call from query method traps (in update call)" ecid $ \cid -> + callToQuery'' cid (inter_query cid defArgs) >>= is2xx >>= isReject [5], + simpleTestCase "Call from query method traps (in query call)" ecid $ \cid -> + query' cid (inter_query cid defArgs) >>= isQueryReject ecid [5], + simpleTestCase "Call from query method traps (in inter-canister-call)" ecid $ \cid -> + do + call cid $ + inter_call + cid + "query" + defArgs + { other_side = inter_query cid defArgs + } + >>= isRelay + >>= isReject [5], + simpleTestCase "Self-call (to update)" ecid $ \cid -> + call cid (inter_update cid defArgs) + >>= isRelay + >>= isReply + >>= is ("Hello " <> cid <> " this is " <> cid), + simpleTestCase "Self-call (to query)" ecid $ \cid -> do + call cid (inter_query cid defArgs) + >>= isRelay + >>= isReply + >>= is ("Hello " <> cid <> " this is " <> cid), + simpleTestCase "update commits" ecid $ \cid -> do + do + call cid $ + setGlobal "FOO" + >>> inter_update cid defArgs {other_side = setGlobal "BAR" >>> reply} + >>= isRelay + >>= isReply + >>= is "" + + query cid (replyData getGlobal) >>= is "BAR", + simpleTestCase "query does not commit" ecid $ \cid -> do + do + call cid $ + setGlobal "FOO" + >>> inter_query cid defArgs {other_side = setGlobal "BAR" >>> reply} + >>= isRelay + >>= isReply + >>= is "" + + do query cid $ replyData getGlobal + >>= is "FOO", + simpleTestCase "query no response" ecid $ \cid -> + do call cid $ inter_query cid defArgs {other_side = noop} + >>= isRelay + >>= isReject [5], + simpleTestCase "query double reply" ecid $ \cid -> + do call cid $ inter_query cid defArgs {other_side = reply >>> reply} + >>= isRelay + >>= isReject [5], + simpleTestCase "Reject code is 0 in reply" ecid $ \cid -> + do call cid $ inter_query cid defArgs {on_reply = replyData (i2b reject_code)} + >>= asWord32 + >>= is 0, + simpleTestCase "Second reply in callback" ecid $ \cid -> do + do + call cid $ + setGlobal "FOO" + >>> replyData "First reply" + >>> inter_query + cid + defArgs + { on_reply = setGlobal "BAR" >>> replyData "Second reply", + on_reject = setGlobal "BAZ" >>> relayReject + } + >>= is "First reply" + + -- now check that the callback trapped and did not actual change the global + -- to make this test reliable, stop and start the canister, this will + -- ensure all outstanding callbacks are handled + barrier [cid] + + query cid (replyData getGlobal) >>= is "FOO", + simpleTestCase "partial reply" ecid $ \cid -> + do + call cid $ + replyDataAppend "FOO" + >>> inter_query cid defArgs {on_reply = replyDataAppend "BAR" >>> reply} + >>= is "BAR", -- check that the FOO is silently dropped + simpleTestCase "cleanup not executed when reply callback does not trap" ecid $ \cid -> do + call_ cid $ + inter_query + cid defArgs - { other_side = acceptAll >>> reply, - cycles = def_cycles + def_cycles `div` 4 - }, - on_reply = replyData (i64tob getRefund), - on_reject = trap "unexpected reject" - } - >>= asWord64 - >>= isRoughly (0 :: Word64) - queryBalance cid1 >>= isRoughly (def_cycles `div` 2) - queryBalance cid2 >>= isRoughly (def_cycles `div` 4) - queryBalance cid3 >>= isRoughly (2 * def_cycles + def_cycles `div` 4), - testCase "aborting call resets balance" $ do - cid <- create noop - (a, b) <- - do - call cid $ - callNew "Foo" "Bar" "baz" "quux" - >>> callCyclesAdd (int64 (def_cycles `div` 2)) - >>> replyDataAppend (i64tob getBalance) - >>> callNew "Foo" "Bar" "baz" "quux" - >>> replyDataAppend (i64tob getBalance) - >>> reply - >>= as2Word64 - isRoughly (def_cycles `div` 2) a - isRoughly def_cycles b, - testCase "partial refund" $ do - cid1 <- create noop - cid2 <- create noop - do - call cid1 $ - inter_call - cid2 - "update" - defArgs - { cycles = def_cycles `div` 2, - other_side = ignore (acceptCycles (int64 (def_cycles `div` 4))) >>> reply, - on_reply = replyData (i64tob getRefund), - on_reject = trap "unexpected reject" - } - >>= asWord64 - >>= isRoughly (def_cycles `div` 4) - queryBalance cid1 >>= isRoughly (def_cycles - def_cycles `div` 4) - queryBalance cid2 >>= isRoughly (def_cycles + def_cycles `div` 4), - testCase "cycles not in balance while in transit" $ do - cid1 <- create noop - do - call cid1 $ - inter_call - cid1 - "update" - defArgs - { cycles = def_cycles `div` 4, - other_side = replyBalance, - on_reject = trap "unexpected reject" - } - >>= isRelay - >>= isReply - >>= asWord64 - >>= isRoughly (def_cycles - def_cycles `div` 4) - queryBalance cid1 >>= isRoughly def_cycles, - testCase "create and delete canister with cycles" $ do - cid1 <- create noop - cid2 <- create_via cid1 (def_cycles `div` 2) - queryBalance cid1 >>= isRoughly (def_cycles `div` 2) - queryBalance cid2 >>= isRoughly (def_cycles `div` 2) - ic_stop_canister (ic00via cid1) cid2 - -- We load some cycles on the deletion call, just to check that they are refunded - ic_delete_canister (ic00viaWithCycles cid1 (def_cycles `div` 4)) cid2 - queryBalance cid1 >>= isRoughly (def_cycles `div` 2), - testGroup - "deposit_cycles" - [ testCase "as controller" $ do - cid1 <- create noop - cid2 <- create_via cid1 (def_cycles `div` 2) - queryBalance cid1 >>= isRoughly (def_cycles `div` 2) - queryBalance cid2 >>= isRoughly (def_cycles `div` 2) - ic_deposit_cycles (ic00viaWithCycles cid1 (def_cycles `div` 4)) cid2 - queryBalance cid1 >>= isRoughly (def_cycles `div` 4) - queryBalance cid2 >>= isRoughly (def_cycles - def_cycles `div` 4), - testCase "as other non-controlling canister" $ do - cid1 <- create noop - cid2 <- create_via cid1 (def_cycles `div` 2) - queryBalance cid1 >>= isRoughly (def_cycles `div` 2) - queryBalance cid2 >>= isRoughly (def_cycles `div` 2) - ic_deposit_cycles (ic00viaWithCycles cid2 (def_cycles `div` 4)) cid1 - queryBalance cid1 >>= isRoughly (def_cycles - def_cycles `div` 4) - queryBalance cid2 >>= isRoughly (def_cycles `div` 4), - testCase "to non-existing canister" $ do - cid1 <- create noop - queryBalance cid1 >>= isRoughly def_cycles - ic_deposit_cycles' (ic00viaWithCycles cid1 (def_cycles `div` 4)) doesn'tExist - >>= isReject [3, 4, 5] - queryBalance cid1 >>= isRoughly def_cycles - ], - testCase "two-step-refund" $ do - cid1 <- create noop - do - call cid1 $ - inter_call - cid1 - "update" - defArgs - { cycles = 10, - other_side = - inter_call - cid1 - "update" - defArgs - { cycles = 5, - other_side = reply, -- no accept - on_reply = - -- remember refund - replyDataAppend (i64tob getRefund) - >>> reply, - on_reject = trap "unexpected reject" - }, - on_reply = - -- remember the refund above and this refund - replyDataAppend argData - >>> replyDataAppend (i64tob getRefund) - >>> reply, - on_reject = trap "unexpected reject" - } - >>= as2Word64 - >>= is (5, 10) - queryBalance cid1 >>= isRoughly def_cycles, -- nothing lost? - testGroup - "provisional top up" - [ testCase "as user" $ do - cid <- create noop - queryBalance cid >>= isRoughly def_cycles - ic_top_up ic00 cid (fromIntegral def_cycles) - queryBalance cid >>= isRoughly (2 * def_cycles), - testCase "as self" $ do - cid <- create noop - queryBalance cid >>= isRoughly def_cycles - ic_top_up (ic00via cid) cid (fromIntegral def_cycles) - queryBalance cid >>= isRoughly (2 * def_cycles), - testCase "as other canister" $ do - cid <- create noop - cid2 <- create noop - queryBalance cid >>= isRoughly def_cycles - ic_top_up (ic00via cid2) cid (fromIntegral def_cycles) - queryBalance cid >>= isRoughly (2 * def_cycles), - testCaseSteps "more than 2^128" $ \step -> do - cid <- create noop - ic_top_up ic00 cid (10 * 2 ^ (128 :: Int)) - cycles <- queryBalance128 cid - step $ "Cycle balance now at " ++ show cycles, - testCase "nonexisting canister" $ do - ic_top_up''' ic00' unused_canister_id (fromIntegral def_cycles) - >>= isErrOrReject [3, 5] - ] - ], - testGroup - "canister_inspect_message" - [ testCase "empty canister" $ do - cid <- create ecid - call'' cid reply >>= isNoErrReject [5] - callToQuery'' cid reply >>= isNoErrReject [5], - testCase "accept all" $ do - cid <- install ecid $ onInspectMessage $ callback acceptMessage - call_ cid reply - callToQuery'' cid reply >>= is2xx >>= isReply >>= is "", - testCase "no accept_message" $ do - cid <- install ecid $ onInspectMessage $ callback noop - call'' cid reply >>= isNoErrReject [4] - callToQuery'' cid reply >>= isNoErrReject [4] - -- check that inter-canister calls still work - cid2 <- install ecid noop - call cid2 (inter_update cid defArgs) - >>= isRelay - >>= isReply - >>= is ("Hello " <> cid2 <> " this is " <> cid), - testCase "two calls to accept_message" $ do - cid <- install ecid $ onInspectMessage $ callback $ acceptMessage >>> acceptMessage - call'' cid reply >>= isNoErrReject [5] - callToQuery'' cid reply >>= isNoErrReject [5], - testCase "trap" $ do - cid <- install ecid $ onInspectMessage $ callback $ trap "no no no" - call'' cid reply >>= isNoErrReject [5] - callToQuery'' cid reply >>= isNoErrReject [5], - testCase "method_name correct" $ do - cid <- - install ecid $ - onInspectMessage $ - callback $ - trapIfEq methodName "update" "no no no" >>> acceptMessage - - call'' cid reply >>= isNoErrReject [5] - callToQuery'' cid reply >>= is2xx >>= isReply >>= is "", - testCase "caller correct" $ do - cid <- - install ecid $ - onInspectMessage $ - callback $ - trapIfEq caller (bytes defaultUser) "no no no" >>> acceptMessage - - call'' cid reply >>= isNoErrReject [5] - callToQuery'' cid reply >>= isNoErrReject [5] - - awaitCall' cid (callRequestAs otherUser cid reply) - >>= is2xx - >>= isReply - >>= is "" - awaitCall' cid (callToQueryRequestAs otherUser cid reply) - >>= is2xx - >>= isReply - >>= is "", - testCase "arg correct" $ do - cid <- - install ecid $ - onInspectMessage $ - callback $ - trapIfEq argData (callback reply) "no no no" >>> acceptMessage - - call'' cid reply >>= isNoErrReject [5] - callToQuery'' cid reply >>= isNoErrReject [5] - - call cid (replyData "foo") >>= is "foo" - callToQuery'' cid (replyData "foo") >>= is2xx >>= isReply >>= is "foo", - testCase "management canister: raw_rand not accepted" $ do - ic_raw_rand'' defaultUser ecid >>= isNoErrReject [4], - testCase "management canister: http_request not accepted" $ do - ic_http_get_request'' defaultUser ecid httpbin_proto >>= isNoErrReject [4], - testCase "management canister: ecdsa_public_key not accepted" $ do - ic_ecdsa_public_key'' defaultUser ecid >>= isNoErrReject [4], - testCase "management canister: sign_with_ecdsa not accepted" $ do - ic_sign_with_ecdsa'' defaultUser ecid (sha256 "dummy") >>= isNoErrReject [4], - simpleTestCase "management canister: deposit_cycles not accepted" ecid $ \cid -> do - ic_deposit_cycles'' defaultUser cid >>= isNoErrReject [4], - simpleTestCase "management canister: wrong sender not accepted" ecid $ \cid -> do - ic_canister_status'' otherUser cid >>= isNoErrReject [5] - ], - testGroup "Delegation targets" $ - let callReq cid = - ( rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "update", - "arg" =: GBlob (run reply) - ], - rec - [ "request_type" =: GText "query", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run reply) - ] - ) - - mgmtReq cid = - ( rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob "", - "method_name" =: GText "canister_status", - "arg" =: GBlob (Candid.encode (#canister_id .== Principal cid)) - ], - rec - [ "request_type" =: GText "query", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob "", - "method_name" =: GText "canister_status", - "arg" =: GBlob (Candid.encode (#canister_id .== Principal cid)) - ] - ) - - good cid call query dels = do - call <- addExpiry call - let rid = requestId call - -- sign request with delegations - delegationEnv defaultSK dels call >>= postCallCBOR cid >>= code2xx - -- wait for it - void $ awaitStatus (getRequestStatus' defaultUser cid rid) >>= isReply - -- also read status with delegation - sreq <- - addExpiry $ - rec - [ "request_type" =: GText "read_state", - "sender" =: GBlob defaultUser, - "paths" =: GList [GList [GBlob "request_status", GBlob rid]] - ] - delegationEnv defaultSK dels sreq >>= postReadStateCBOR cid >>= void . code2xx - -- also make query call - query <- addExpiry query - let qrid = requestId query - delegationEnv defaultSK dels query >>= postQueryCBOR cid >>= code2xx - - badSubmit cid req dels = do - req <- addExpiry req - -- sign request with delegations (should fail) - delegationEnv defaultSK dels req >>= postCallCBOR cid >>= code400 - - badRead cid req dels error_code = do - req <- addExpiry req - let rid = requestId req - -- submit with plain signature - envelope defaultSK req >>= postCallCBOR cid >>= code202 - -- wait for it - void $ awaitStatus (getRequestStatus' defaultUser cid rid) >>= isReply - -- also read status with delegation - sreq <- - addExpiry $ - rec - [ "request_type" =: GText "read_state", - "sender" =: GBlob defaultUser, - "paths" =: GList [GList [GBlob "request_status", GBlob rid]] + { on_reply = reply, + on_cleanup = Just (setGlobal "BAD") + } + query cid (replyData getGlobal) >>= is "", + simpleTestCase "cleanup not executed when reject callback does not trap" ecid $ \cid -> do + call_ cid $ + inter_query + cid + defArgs + { other_side = reject "meh", + on_reject = reply, + on_cleanup = Just (setGlobal "BAD") + } + query cid (replyData getGlobal) >>= is "", + testGroup + "two callbacks" + [ simpleTestCase "reply after trap" ecid $ \cid -> + do + call cid $ + inter_query cid defArgs {on_reply = trap "first callback traps"} + >>> inter_query cid defArgs {on_reply = replyData "good"} + >>= is "good", + simpleTestCase "trap after reply" ecid $ \cid -> + do + call cid $ + inter_query cid defArgs {on_reply = replyData "good"} + >>> inter_query cid defArgs {on_reply = trap "second callback traps"} + >>= is "good", + simpleTestCase "both trap" ecid $ \cid -> + do + call' cid $ + inter_query cid defArgs {on_reply = trap "first callback traps"} + >>> inter_query cid defArgs {on_reply = trap "second callback traps"} + >>= isReject [5] + ], + simpleTestCase "Call to other canister (to update)" ecid $ \cid -> do + cid2 <- install ecid noop + do call cid $ inter_update cid2 defArgs + >>= isRelay + >>= isReply + >>= is ("Hello " <> cid <> " this is " <> cid2), + simpleTestCase "Call to other canister (to query)" ecid $ \cid -> do + cid2 <- install ecid noop + do call cid $ inter_query cid2 defArgs + >>= isRelay + >>= isReply + >>= is ("Hello " <> cid <> " this is " <> cid2) + ], + testCaseSteps "stable memory" $ \step -> do + cid <- install ecid noop + + step "Stable mem size is zero" + query cid (replyData (i2b stableSize)) >>= is "\x0\x0\x0\x0" + + step "Writing stable memory (failing)" + call' cid (stableWrite (int 0) "FOO") >>= isReject [5] + step "Set stable mem (failing, query)" + query' cid (stableWrite (int 0) "FOO") >>= isQueryReject ecid [5] + + step "Growing stable memory" + call cid (replyData (i2b (stableGrow (int 1)))) >>= is "\x0\x0\x0\x0" + + step "Growing stable memory again" + call cid (replyData (i2b (stableGrow (int 1)))) >>= is "\x1\x0\x0\x0" + + step "Growing stable memory in query" + query cid (replyData (i2b (stableGrow (int 1)))) >>= is "\x2\x0\x0\x0" + + step "Stable mem size is two" + query cid (replyData (i2b stableSize)) >>= is "\x2\x0\x0\x0" + + step "Try growing stable memory beyond 4GiB" + call cid (replyData (i2b (stableGrow (int 65535)))) >>= is "\xff\xff\xff\xff" + + step "Writing stable memory" + call_ cid $ stableWrite (int 0) "FOO" >>> reply + + step "Writing stable memory (query)" + query_ cid $ stableWrite (int 0) "BAR" >>> reply + + step "Reading stable memory" + call cid (replyData (stableRead (int 0) (int 3))) >>= is "FOO", + testCaseSteps "64 bit stable memory" $ \step -> do + cid <- install ecid noop + + step "Stable mem size is zero" + query cid (replyData (i64tob stable64Size)) >>= is "\x0\x0\x0\x0\x0\x0\x0\x0" + + step "Writing stable memory (failing)" + call' cid (stable64Write (int64 0) "FOO") >>= isReject [5] + + step "Set stable mem (failing, query)" + query' cid (stable64Write (int64 0) "FOO") >>= isQueryReject ecid [5] + + step "Growing stable memory" + call cid (replyData (i64tob (stable64Grow (int64 1)))) >>= is "\x0\x0\x0\x0\x0\x0\x0\x0" + + step "Growing stable memory again" + call cid (replyData (i64tob (stable64Grow (int64 1)))) >>= is "\x1\x0\x0\x0\x0\x0\x0\x0" + + step "Growing stable memory in query" + query cid (replyData (i64tob (stable64Grow (int64 1)))) >>= is "\x2\x0\x0\x0\x0\x0\x0\x0" + + step "Stable mem size is two" + query cid (replyData (i2b stableSize)) >>= is "\x2\x0\x0\x0" + query cid (replyData (i64tob stable64Size)) >>= is "\x2\x0\x0\x0\x0\x0\x0\x0" + + step "Writing stable memory" + call_ cid $ stable64Write (int64 0) "FOO" >>> reply + + step "Writing stable memory (query)" + query_ cid $ stable64Write (int64 0) "BAR" >>> reply + + step "Reading stable memory" + call cid (replyData (stable64Read (int64 0) (int64 3))) >>= is "FOO" + call cid (replyData (stableRead (int 0) (int 3))) >>= is "FOO" + + step "Writing in 32 bit mode" + call_ cid $ stableWrite (int 0) "BAR" >>> reply + + step "Reading back in 64 bit mode" + call cid (replyData (stable64Read (int64 0) (int64 3))) >>= is "BAR" + + step "Growing stable memory beyond 4GiB" + call cid (replyData (i64tob (stable64Grow (int64 65535)))) >>= is "\x2\x0\x0\x0\x0\x0\x0\x0" + query cid (replyData (i64tob stable64Size)) >>= is "\x01\x00\x01\x00\x0\x0\x0\x0" + + step "Using 32 bit API with large stable memory" + query' cid (ignore stableSize) >>= isQueryReject ecid [5] + query' cid (ignore $ stableGrow (int 1)) >>= isQueryReject ecid [5] + query' cid (stableWrite (int 0) "BAZ") >>= isQueryReject ecid [5] + query' cid (ignore $ stableRead (int 0) (int 3)) >>= isQueryReject ecid [5] + + step "Using 64 bit API with large stable memory" + call cid (replyData (i64tob (stable64Grow (int64 1)))) >>= is "\x01\x00\x01\x00\x0\x0\x0\x0" + query cid (replyData (i64tob stable64Size)) >>= is "\x02\x00\x01\x00\x0\x0\x0\x0" + call cid (replyData (stable64Read (int64 0) (int64 3))) >>= is "BAR" + call_ cid $ stable64Write (int64 0) "BAZ" >>> reply + call cid (replyData (stable64Read (int64 0) (int64 3))) >>= is "BAZ", + testGroup "time" $ + let getTimeTwice = cat (i64tob getTime) (i64tob getTime) + in [ simpleTestCase "in query" ecid $ \cid -> + query cid (replyData getTimeTwice) >>= as2Word64 >>= bothSame, + simpleTestCase "in update" ecid $ \cid -> + call cid (replyData getTimeTwice) >>= as2Word64 >>= bothSame, + testCase "in install" $ do + cid <- install ecid $ setGlobal getTimeTwice + query cid (replyData getGlobal) >>= as2Word64 >>= bothSame, + testCase "in pre_upgrade" $ do + cid <- + install ecid $ + ignore (stableGrow (int 1)) + >>> onPreUpgrade (callback $ stableWrite (int 0) getTimeTwice) + upgrade cid noop + query cid (replyData (stableRead (int 0) (int (2 * 8)))) >>= as2Word64 >>= bothSame, + simpleTestCase "in post_upgrade" ecid $ \cid -> do + upgrade cid $ setGlobal getTimeTwice + query cid (replyData getGlobal) >>= as2Word64 >>= bothSame + ], + testGroup "canister global timer" $ canister_timer_tests ecid, + testGroup "canister version" $ canister_version_tests ecid, + testGroup "canister history" $ canister_history_tests ecid, + testGroup "is_controller system API" $ + [ simpleTestCase "argument is controller" ecid $ \cid -> do + res <- query cid (replyData $ i2b $ isController (bytes defaultUser)) >>= asWord32 + res @?= 1, + simpleTestCase "argument is not controller" ecid $ \cid -> do + res <- query cid (replyData $ i2b $ isController (bytes "")) >>= asWord32 + res @?= 0, + simpleTestCase "argument is a valid principal" ecid $ \cid -> do + res <- query cid (replyData $ i2b $ isController (bytes $ BS.replicate 29 0)) >>= asWord32 + res @?= 0, + simpleTestCase "argument is not a valid principal" ecid $ \cid -> do + query' cid (replyData $ i2b $ isController (bytes $ BS.replicate 30 0)) >>= isQueryReject ecid [5] + ], + testGroup "upgrades" $ + let installForUpgrade on_pre_upgrade = + install ecid $ + setGlobal "FOO" + >>> ignore (stableGrow (int 1)) + >>> stableWrite (int 0) "BAR______" + >>> onPreUpgrade (callback on_pre_upgrade) + + checkNoUpgrade cid = do + query cid (replyData getGlobal) >>= is "FOO" + query cid (replyData (stableRead (int 0) (int 9))) >>= is "BAR______" + in [ testCase "succeeding" $ do + -- check that the pre-upgrade hook has access to the old state + cid <- installForUpgrade $ stableWrite (int 3) getGlobal + checkNoUpgrade cid + + upgrade cid $ stableWrite (int 6) (stableRead (int 0) (int 3)) + + query cid (replyData getGlobal) >>= is "" + query cid (replyData (stableRead (int 0) (int 9))) >>= is "BARFOOBAR", + testCase "trapping in pre-upgrade" $ do + cid <- installForUpgrade $ trap "trap in pre-upgrade" + checkNoUpgrade cid + + upgrade' cid noop >>= isReject [5] + checkNoUpgrade cid, + testCase "trapping in pre-upgrade (by calling)" $ do + cid <- installForUpgrade $ trap "trap in pre-upgrade" + call_ cid $ + reply + >>> onPreUpgrade + ( callback + ( inter_query cid defArgs {other_side = noop} + ) + ) + checkNoUpgrade cid + + upgrade' cid noop >>= isReject [5] + checkNoUpgrade cid, + testCase "trapping in pre-upgrade (by accessing arg)" $ do + cid <- installForUpgrade $ ignore argData + checkNoUpgrade cid + + upgrade' cid noop >>= isReject [5] + checkNoUpgrade cid, + testCase "trapping in post-upgrade" $ do + cid <- installForUpgrade $ stableWrite (int 3) getGlobal + checkNoUpgrade cid + + upgrade' cid (trap "trap in post-upgrade") >>= isReject [5] + checkNoUpgrade cid, + testCase "trapping in post-upgrade (by calling)" $ do + cid <- installForUpgrade $ stableWrite (int 3) getGlobal + checkNoUpgrade cid + + do upgrade' cid $ inter_query cid defArgs {other_side = noop} + >>= isReject [5] + checkNoUpgrade cid + ], + testGroup + "heartbeat" + [ testCase "called once for all canisters" $ do + cid <- install ecid $ onHeartbeat $ callback $ ignore (stableGrow (int 1)) >>> stableWrite (int 0) "FOO" + cid2 <- install ecid $ onHeartbeat $ callback $ ignore (stableGrow (int 1)) >>> stableWrite (int 0) "BAR" + -- Heartbeat cannot respond. Should be trapped. + cid3 <- install ecid $ onHeartbeat $ callback $ setGlobal "FIZZ" >>> replyData "FIZZ" + + -- The spec currently gives no guarantee about when or how frequent heartbeats are executed. + -- But all implementations have the property: if update call B is submitted after call A is completed, + -- then a heartbeat runs before the execution of B. + -- We use this here to make sure that heartbeats have been attempted: + call_ cid reply + call_ cid reply + + query cid (replyData (stableRead (int 0) (int 3))) >>= is "FOO" + query cid2 (replyData (stableRead (int 0) (int 3))) >>= is "BAR" + query cid3 (replyData getGlobal) >>= is "" + ], + testGroup + "reinstall" + [ testCase "succeeding" $ do + cid <- + install ecid $ + setGlobal "FOO" + >>> ignore (stableGrow (int 1)) + >>> stableWrite (int 0) "FOO______" + query cid (replyData getGlobal) >>= is "FOO" + query cid (replyData (stableRead (int 0) (int 9))) >>= is "FOO______" + query cid (replyData (i2b stableSize)) >>= asWord32 >>= is 1 + + reinstall cid $ + setGlobal "BAR" + >>> ignore (stableGrow (int 2)) + >>> stableWrite (int 0) "BAR______" + + query cid (replyData getGlobal) >>= is "BAR" + query cid (replyData (stableRead (int 0) (int 9))) >>= is "BAR______" + query cid (replyData (i2b stableSize)) >>= asWord32 >>= is 2 + + reinstall cid noop + + query cid (replyData getGlobal) >>= is "" + query cid (replyData (i2b stableSize)) >>= asWord32 >>= is 0, + testCase "trapping" $ do + cid <- + install ecid $ + setGlobal "FOO" + >>> ignore (stableGrow (int 1)) + >>> stableWrite (int 0) "FOO______" + query cid (replyData getGlobal) >>= is "FOO" + query cid (replyData (stableRead (int 0) (int 9))) >>= is "FOO______" + query cid (replyData (i2b stableSize)) >>= is "\1\0\0\0" + + reinstall' cid (trap "Trapping the reinstallation") >>= isReject [5] + + query cid (replyData getGlobal) >>= is "FOO" + query cid (replyData (stableRead (int 0) (int 9))) >>= is "FOO______" + query cid (replyData (i2b stableSize)) >>= is "\1\0\0\0" + ], + testGroup + "uninstall" + [ testCase "uninstall empty canister" $ do + cid <- create ecid + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #running + cs .! #settings .! #controllers @?= Vec.fromList [Principal defaultUser] + cs .! #module_hash @?= Nothing + ic_uninstall ic00 cid + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #running + cs .! #settings .! #controllers @?= Vec.fromList [Principal defaultUser] + cs .! #module_hash @?= Nothing, + testCase "uninstall as wrong user" $ do + cid <- create ecid + ic_uninstall'' otherUser cid >>= isErrOrReject [3, 5], + testCase "uninstall and reinstall wipes state" $ do + cid <- install ecid (setGlobal "FOO") + ic_uninstall ic00 cid + ic_install_single_chunk ic00 (enum #install) cid store_canister_id ucan_chunk_hash (run (setGlobal "BAR")) + query cid (replyData getGlobal) >>= is "BAR", + testCase "uninstall and reinstall wipes stable memory" $ do + cid <- install ecid (ignore (stableGrow (int 1)) >>> stableWrite (int 0) "FOO") + ic_uninstall ic00 cid + ic_install_single_chunk ic00 (enum #install) cid store_canister_id ucan_chunk_hash (run (setGlobal "BAR")) + query cid (replyData (i2b stableSize)) >>= asWord32 >>= is 0 + do + query cid $ + ignore (stableGrow (int 1)) + >>> replyData (stableRead (int 0) (int 3)) + >>= is "\0\0\0" + do + call cid $ + ignore (stableGrow (int 1)) + >>> replyData (stableRead (int 0) (int 3)) + >>= is "\0\0\0", + testCase "uninstall and reinstall wipes certified data" $ do + cid <- install ecid $ setCertifiedData "FOO" + query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO" + ic_uninstall ic00 cid + ic_install_single_chunk ic00 (enum #install) cid store_canister_id ucan_chunk_hash (run noop) + query cid (replyData getCertificate) >>= extractCertData cid >>= is "", + simpleTestCase "uninstalled rejects calls" ecid $ \cid -> do + call cid (replyData "Hi") >>= is "Hi" + query cid (replyData "Hi") >>= is "Hi" + ic_uninstall ic00 cid + -- should be http error, due to inspection + call'' cid (replyData "Hi") >>= isNoErrReject [5] + query' cid (replyData "Hi") >>= isQueryReject ecid [5], + testCaseSteps "open call contexts are rejected" $ \step -> do + cid <- install ecid noop + + step "Create message hold" + (messageHold, release) <- createMessageHold ecid + + step "Create long-running call" + grs1 <- submitCall cid $ callRequest cid messageHold + awaitKnown grs1 >>= isPendingOrProcessing + + step "Uninstall" + ic_uninstall ic00 cid + + step "Long-running call is rejected" + awaitStatus grs1 >>= isReject [4] + + step "Now release" + release + awaitStatus grs1 >>= isReject [4], -- still a reject + testCaseSteps "deleted call contexts prevent stopping" $ \step -> do + cid <- install ecid noop + + step "Create message hold" + (messageHold, release) <- createMessageHold ecid + + step "Create long-running call" + grs1 <- submitCall cid $ callRequest cid messageHold + awaitKnown grs1 >>= isPendingOrProcessing + + step "Uninstall" + ic_uninstall ic00 cid + + step "Long-running call is rejected" + awaitStatus grs1 >>= isReject [4] + + step "Stop" + grs2 <- submitCall cid $ stopRequest cid + awaitKnown grs2 >>= isPendingOrProcessing + + step "Is stopping (via management)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #stopping + + step "Next stop waits, too" + grs3 <- submitCall cid $ stopRequest cid + awaitKnown grs3 >>= isPendingOrProcessing + + step "Release the held message" + release + + step "Wait for calls to complete" + awaitStatus grs1 >>= isReject [4] -- still a reject + awaitStatus grs2 >>= isReply >>= is (Candid.encode ()) + awaitStatus grs3 >>= isReply >>= is (Candid.encode ()) + + step "Is stopped (via management)?" + cs <- ic_canister_status ic00 cid + cs .! #status @?= enum #stopped, + testCaseSteps "deleted call contexts are not delivered" $ \step -> do + -- This is a tricky one: We make one long-running call, + -- then uninstall (rejecting the call), then re-install fresh code, + -- make another long-running call, then release the first one. The system + -- should not confuse the two callbacks. + cid <- install ecid noop + helper <- install ecid noop + + step "Create message holds" + (messageHold1, release1) <- createMessageHold ecid + (messageHold2, release2) <- createMessageHold ecid + + step "Create first long-running call" + grs1 <- + submitCall cid $ + callRequest cid $ + inter_call + helper + "update" + defArgs + { other_side = messageHold1, + on_reply = replyData "First" + } + awaitKnown grs1 >>= isPendingOrProcessing + + step "Uninstall" + ic_uninstall ic00 cid + awaitStatus grs1 >>= isReject [4] + + step "Reinstall" + ic_install_single_chunk ic00 (enum #install) cid store_canister_id ucan_chunk_hash (run (setGlobal "BAR")) + + step "Create second long-running call" + grs2 <- + submitCall cid $ + callRequest cid $ + inter_call + helper + "update" + defArgs + { other_side = messageHold2, + on_reply = replyData "Second" + } + awaitStatus grs1 >>= isReject [4] + awaitKnown grs2 >>= isPendingOrProcessing + + step "Release first call" + release1 + awaitStatus grs1 >>= isReject [4] + awaitKnown grs2 >>= isPendingOrProcessing + + step "Release second call" + release2 + awaitStatus grs1 >>= isReject [4] + awaitStatus grs2 >>= isReply >>= is "Second" + ], + testGroup + "debug facilities" + [ simpleTestCase "Using debug_print" ecid $ \cid -> + call_ cid (debugPrint "ic-ref-test print" >>> reply), + simpleTestCase "Using debug_print (query)" ecid $ \cid -> + query_ cid $ debugPrint "ic-ref-test print" >>> reply, + simpleTestCase "Using debug_print with invalid bounds" ecid $ \cid -> + query_ cid $ badPrint >>> reply, + simpleTestCase "Explicit trap" ecid $ \cid -> + call' cid (trap "trapping") >>= isReject [5], + simpleTestCase "Explicit trap (query)" ecid $ \cid -> do + query' cid (trap "trapping") >>= isQueryReject ecid [5] + ], + testCase "caller (in init)" $ do + cid <- install ecid $ setGlobal caller + query cid (replyData getGlobal) >>= is defaultUser, + testCase "self (in init)" $ do + cid <- install ecid $ setGlobal self + query cid (replyData getGlobal) >>= is cid, + testGroup "trapping in init" $ + let failInInit pgm = do + cid <- create ecid + install' cid pgm >>= isReject [5] + -- canister does not exist + query' cid noop >>= isQueryReject ecid [5] + in [ testCase "explicit trap" $ failInInit $ trap "trapping in install", + testCase "call" $ failInInit $ inter_query "dummy" defArgs, + testCase "reply" $ failInInit reply, + testCase "reject" $ failInInit $ reject "rejecting in init" + ], + testGroup + "query" + [ testGroup "required fields" $ do + -- TODO: Begin with a succeeding request to a real canister, to rule + -- out other causes of failure than missing fields + omitFields queryToNonExistent $ \req -> do + cid <- create ecid + addExpiry req >>= envelope defaultSK >>= postQueryCBOR cid >>= code4xx, + simpleTestCase "non-existing (deleted) canister" ecid $ \cid -> do + ic_stop_canister ic00 cid + ic_delete_canister ic00 cid + query' cid reply >>= isQueryReject ecid [3], + simpleTestCase "does not commit" ecid $ \cid -> do + call_ cid (setGlobal "FOO" >>> reply) + query cid (setGlobal "BAR" >>> replyData getGlobal) >>= is "BAR" + query cid (replyData getGlobal) >>= is "FOO" + ], + testGroup "read state" $ + let ensure_request_exists cid user = do + req <- + addNonce >=> addExpiry $ + rec + [ "request_type" =: GText "call", + "sender" =: GBlob user, + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run (replyData "\xff\xff")) + ] + awaitCall cid req >>= isReply >>= is "\xff\xff" + + -- check that the request is there + getRequestStatus user cid (requestId req) >>= is (Responded (Reply "\xff\xff")) + + return (requestId req) + ensure_provisional_create_canister_request_exists ecid user = do + let arg :: ProvisionalCreateCanisterArgs = + empty + .+ #amount + .== Just initial_cycles + .+ #settings + .== Nothing + .+ #specified_id + .== Nothing + .+ #sender_canister_version + .== Nothing + req <- + addNonce >=> addExpiry $ + rec + [ "request_type" =: GText "call", + "sender" =: GBlob user, + "canister_id" =: GBlob "", + "method_name" =: GText "provisional_create_canister_with_cycles", + "arg" =: GBlob (Candid.encode arg) + ] + _ <- awaitCall ecid req >>= isReply + + -- check that the request is there + getRequestStatus user ecid (requestId req) >>= isResponded + + return (requestId req) + in [ testGroup "required fields" $ + omitFields readStateEmpty $ \req -> do + cid <- create ecid + addExpiry req >>= envelope defaultSK >>= postReadStateCBOR cid >>= code4xx, + simpleTestCase "certificate validates" ecid $ \cid -> do + cert <- getStateCert defaultUser cid [] + validateStateCert cid cert, + simpleTestCase "certificate does not validate if canister range check fails" ecid $ \cid -> do + unless my_is_root $ do + cert <- getStateCert defaultUser cid [] + result <- try (validateStateCert other_ecid cert) :: IO (Either DelegationCanisterRangeCheck ()) + assertBool "certificate should not validate" $ isLeft result, + testCaseSteps "time is present" $ \step -> do + cid <- create ecid + cert <- getStateCert defaultUser cid [] + time <- certValue @Natural cert ["time"] + step $ "Reported time: " ++ show time, + testCase "time can be asked for" $ do + cid <- create ecid + cert <- getStateCert defaultUser cid [["time"]] + void $ certValue @Natural cert ["time"], + testCase "can ask for /subnet" $ do + cert <- getStateCert defaultUser ecid [["subnet"]] + void $ certValue @Blob cert ["subnet", my_subnet_id, "public_key"] + void $ certValue @Blob cert ["subnet", my_subnet_id, "canister_ranges"] + void $ certValue @Blob cert ["subnet", other_subnet_id, "public_key"] + void $ certValue @Blob cert ["subnet", other_subnet_id, "canister_ranges"] + void $ forM my_nodes $ \nid -> do + void $ certValue @Blob cert ["subnet", my_subnet_id, "node", rawEntityId nid, "public_key"] + void $ forM other_nodes $ \nid -> do + certValueAbsent cert ["subnet", other_subnet_id, "node", rawEntityId nid, "public_key"], + testCase "controller of empty canister" $ do + cid <- create ecid + cert <- getStateCert defaultUser cid [["canister", cid, "controllers"]] + certValue @Blob cert ["canister", cid, "controllers"] >>= asCBORBlobList >>= isSet [defaultUser], + testCase "module_hash of empty canister" $ do + cid <- create ecid + cert <- getStateCert defaultUser cid [["canister", cid, "module_hash"]] + lookupPath (cert_tree cert) ["canister", cid, "module_hash"] @?= Absent, + testCase "single controller of installed canister" $ do + cid <- install ecid noop + -- also vary user, just for good measure + cert <- getStateCert anonymousUser cid [["canister", cid, "controllers"]] + certValue @Blob cert ["canister", cid, "controllers"] >>= asCBORBlobList >>= isSet [defaultUser], + testCase "multiple controllers of installed canister" $ do + cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing + ic_set_controllers ic00 cid [defaultUser, otherUser] + cert <- getStateCert defaultUser cid [["canister", cid, "controllers"]] + certValue @Blob cert ["canister", cid, "controllers"] >>= asCBORBlobList >>= isSet [defaultUser, otherUser], + testCase "zero controllers of installed canister" $ do + cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing + ic_set_controllers ic00 cid [] + cert <- getStateCert defaultUser cid [["canister", cid, "controllers"]] + certValue @Blob cert ["canister", cid, "controllers"] >>= asCBORBlobList >>= isSet [], + testCase "module_hash of universal canister" $ do + cid <- install ecid noop + cert <- getStateCert anonymousUser cid [["canister", cid, "module_hash"]] + certValue @Blob cert ["canister", cid, "module_hash"] >>= is ucan_chunk_hash, + testGroup + "malformed request id" + [ simpleTestCase ("rid \"" ++ shorten 8 (asHex rid) ++ "\"") ecid $ \cid -> do + getStateCert' defaultUser cid [["request_status", rid]] >>= isErr4xx + | rid <- ["", "foo"] + ], + testGroup + "non-existence proofs for non-existing request id" + [ simpleTestCase ("rid \"" ++ shorten 8 (asHex rid) ++ "\"") ecid $ \cid -> do + cert <- getStateCert defaultUser cid [["request_status", rid]] + certValueAbsent cert ["request_status", rid, "status"] + | rid <- [BS.replicate 32 0, BS.replicate 32 8, BS.replicate 32 255] + ], + simpleTestCase "can ask for portion of request status" ecid $ \cid -> do + rid <- ensure_request_exists cid defaultUser + cert <- getStateCert defaultUser cid [["request_status", rid, "status"], ["request_status", rid, "reply"]] + void $ certValue @T.Text cert ["request_status", rid, "status"] + void $ certValue @Blob cert ["request_status", rid, "reply"], + simpleTestCase "access denied for other users request" ecid $ \cid -> do + rid <- ensure_request_exists cid defaultUser + getStateCert' otherUser cid [["request_status", rid]] >>= isErr4xx, + simpleTestCase "reading two statuses to same canister in one go" ecid $ \cid -> do + rid1 <- ensure_request_exists cid defaultUser + rid2 <- ensure_request_exists cid defaultUser + getStateCert' defaultUser cid [["request_status", rid1], ["request_status", rid2]] >>= isErr4xx, + simpleTestCase "access denied for other users request (mixed request)" ecid $ \cid -> do + rid1 <- ensure_request_exists cid defaultUser + rid2 <- ensure_request_exists cid otherUser + getStateCert' defaultUser cid [["request_status", rid1], ["request_status", rid2]] >>= isErr4xx, + simpleTestCase "access denied for two statuses to different canisters" ecid $ \cid -> do + cid2 <- install ecid noop + rid1 <- ensure_request_exists cid defaultUser + rid2 <- ensure_request_exists cid2 defaultUser + getStateCert' defaultUser cid [["request_status", rid1], ["request_status", rid2]] >>= isErr4xx, + simpleTestCase "access denied with different effective canister id" ecid $ \cid -> do + cid2 <- install ecid noop + rid <- ensure_provisional_create_canister_request_exists cid defaultUser + getStateCert' defaultUser cid2 [["request_status", rid]] >>= isErr4xx, + simpleTestCase "access denied for bogus path" ecid $ \cid -> do + getStateCert' otherUser cid [["hello", "world"]] >>= isErr4xx, + simpleTestCase "access denied for fetching full state tree" ecid $ \cid -> do + getStateCert' otherUser cid [[]] >>= isErr4xx, + testGroup "metadata" $ + let withCustomSection mod (name, content) = mod <> BS.singleton 0 <> sized (sized name <> content) + where + sized x = BS.fromStrict (toLEB128 @Natural (fromIntegral (BS.length x))) <> x + withSections xs = foldl withCustomSection trivialWasmModule xs + in [ simpleTestCase "absent" ecid $ \cid -> do + cert <- getStateCert defaultUser cid [["canister", cid, "metadata", "foo"]] + lookupPath (cert_tree cert) ["canister", cid, "metadata", "foo"] @?= Absent, + testCase "public" $ do + let mod = withSections [("icp:public test", "bar")] + cid <- create ecid + ic_install ic00 (enum #install) cid mod "" + cert <- getStateCert otherUser cid [["canister", cid, "metadata", "test"]] + lookupPath (cert_tree cert) ["canister", cid, "metadata", "test"] @?= Found "bar" + cert <- getStateCert anonymousUser cid [["canister", cid, "metadata", "test"]] + lookupPath (cert_tree cert) ["canister", cid, "metadata", "test"] @?= Found "bar", + testCase "private" $ do + let mod = withSections [("icp:private test", "bar")] + cid <- create ecid + ic_install ic00 (enum #install) cid mod "" + getStateCert' otherUser cid [["canister", cid, "metadata", "test"]] >>= isErr4xx + getStateCert' anonymousUser cid [["canister", cid, "metadata", "test"]] >>= isErr4xx + cert <- getStateCert defaultUser cid [["canister", cid, "metadata", "test"]] + lookupPath (cert_tree cert) ["canister", cid, "metadata", "test"] @?= Found "bar", + testCase "duplicate public" $ do + let mod = withSections [("icp:public test", "bar"), ("icp:public test", "baz")] + cid <- create ecid + ic_install' ic00 (enum #install) cid mod "" >>= isReject [5], + testCase "duplicate private" $ do + let mod = withSections [("icp:private test", "bar"), ("icp:private test", "baz")] + cid <- create ecid + ic_install' ic00 (enum #install) cid mod "" >>= isReject [5], + testCase "duplicate mixed" $ do + let mod = withSections [("icp:private test", "bar"), ("icp:public test", "baz")] + cid <- create ecid + ic_install' ic00 (enum #install) cid mod "" >>= isReject [5], + testCase "invalid utf8 in module" $ do + let mod = withSections [("icp:public \xe2\x28\xa1", "baz")] + cid <- create ecid + ic_install' ic00 (enum #install) cid mod "" >>= isReject [5], + simpleTestCase "invalid utf8 in read_state" ecid $ \cid -> do + getStateCert' defaultUser cid [["canister", cid, "metadata", "\xe2\x28\xa1"]] >>= isErr4xx, + testCase "unicode metadata name" $ do + let mod = withSections [("icp:public ☃️", "bar")] + cid <- create ecid + ic_install ic00 (enum #install) cid mod "" + cert <- getStateCert anonymousUser cid [["canister", cid, "metadata", "☃️"]] + lookupPath (cert_tree cert) ["canister", cid, "metadata", "☃️"] @?= Found "bar", + testCase "zero-length metadata name" $ do + let mod = withSections [("icp:public ", "bar")] + cid <- create ecid + ic_install ic00 (enum #install) cid mod "" + cert <- getStateCert anonymousUser cid [["canister", cid, "metadata", ""]] + lookupPath (cert_tree cert) ["canister", cid, "metadata", ""] @?= Found "bar", + testCase "metadata section name with spaces" $ do + let mod = withSections [("icp:public metadata section name with spaces", "bar")] + cid <- create ecid + ic_install ic00 (enum #install) cid mod "" + cert <- getStateCert anonymousUser cid [["canister", cid, "metadata", "metadata section name with spaces"]] + lookupPath (cert_tree cert) ["canister", cid, "metadata", "metadata section name with spaces"] @?= Found "bar" + ] + ], + testGroup + "certified variables" + [ simpleTestCase "initially empty" ecid $ \cid -> do + query cid (replyData getCertificate) >>= extractCertData cid >>= is "", + simpleTestCase "validates" ecid $ \cid -> do + query cid (replyData getCertificate) + >>= decodeCert' + >>= validateStateCert cid, + simpleTestCase "present in query method (query call)" ecid $ \cid -> do + query cid (replyData (i2b getCertificatePresent)) + >>= is "\1\0\0\0", + simpleTestCase "not present in query method (update call)" ecid $ \cid -> do + callToQuery'' cid (replyData (i2b getCertificatePresent)) + >>= is2xx + >>= isReply + >>= is "\0\0\0\0", + simpleTestCase "not present in query method (inter-canister call)" ecid $ \cid -> do + do + call cid $ + inter_call + cid + "query" + defArgs + { other_side = replyData (i2b getCertificatePresent) + } + >>= isRelay + >>= isReply + >>= is "\0\0\0\0", + simpleTestCase "not present in update method" ecid $ \cid -> do + call cid (replyData (i2b getCertificatePresent)) + >>= is "\0\0\0\0", + simpleTestCase "set and get" ecid $ \cid -> do + call_ cid $ setCertifiedData "FOO" >>> reply + query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", + simpleTestCase "set twice" ecid $ \cid -> do + call_ cid $ setCertifiedData "FOO" >>> setCertifiedData "BAR" >>> reply + query cid (replyData getCertificate) >>= extractCertData cid >>= is "BAR", + simpleTestCase "set then trap" ecid $ \cid -> do + call_ cid $ setCertifiedData "FOO" >>> reply + call' cid (setCertifiedData "BAR" >>> trap "Trapped") >>= isReject [5] + query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", + simpleTestCase "too large traps, old value retained" ecid $ \cid -> do + call_ cid $ setCertifiedData "FOO" >>> reply + call' cid (setCertifiedData (bytes (BS.replicate 33 0x42)) >>> reply) + >>= isReject [5] + query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", + testCase "set in init" $ do + cid <- install ecid $ setCertifiedData "FOO" + query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", + testCase "set in pre-upgrade" $ do + cid <- install ecid $ onPreUpgrade (callback $ setCertifiedData "FOO") + upgrade cid noop + query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", + simpleTestCase "set in post-upgrade" ecid $ \cid -> do + upgrade cid $ setCertifiedData "FOO" + query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO", + simpleTestCase "cleared in reinstall" ecid $ \cid -> do + call_ cid $ setCertifiedData "FOO" >>> reply + query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO" + reinstall cid noop + query cid (replyData getCertificate) >>= extractCertData cid >>= is "", + simpleTestCase "cleared in uninstall" ecid $ \cid -> do + call_ cid $ setCertifiedData "FOO" >>> reply + query cid (replyData getCertificate) >>= extractCertData cid >>= is "FOO" + ic_uninstall ic00 cid + installAt cid noop + query cid (replyData getCertificate) >>= extractCertData cid >>= is "" + ], + testGroup "cycles" $ + let replyBalance = replyData (i64tob getBalance) + replyBalance128 = replyData getBalance128 + replyBalanceBalance128 = replyDataAppend (i64tob getBalance) >>> replyDataAppend getBalance128 >>> reply + rememberBalance = + ignore (stableGrow (int 1)) + >>> stableWrite (int 0) (i64tob getBalance) + recallBalance = replyData (stableRead (int 0) (int 8)) + acceptAll = ignore (acceptCycles getAvailableCycles) + queryBalance cid = query cid replyBalance >>= asWord64 + queryBalance128 cid = query cid replyBalance128 >>= asWord128 + queryBalanceBalance128 cid = query cid replyBalanceBalance128 >>= asWord64Word128 + + -- At the time of writing, creating a canister needs at least 1T + -- and the freezing limit is 5T + -- (At some point, the max was 100T, but that is no longer the case) + -- So lets try to stay away from these limits. + -- The lowest denomination we deal with below is def_cycles`div`4 + def_cycles = 80_000_000_000_000 :: Word64 + + -- The system burns cycles at unspecified rates. To cater for such behaviour, + -- we make the assumption that no test burns more than the following epsilon. + -- + -- The biggest fee we currently deal with is the system deducing 1T + -- upon canister creation. So our epsilon needs to allow that and then + -- some more. + eps = 3_000_000_000_000 :: Integer + + isRoughly :: (HasCallStack, Show a, Num a, Integral a, Show b, Num b, Integral b) => a -> b -> Assertion + isRoughly exp act = + assertBool + (show act ++ " not near " ++ show exp) + (abs (fromIntegral exp - fromIntegral act) < eps) + + create prog = do + cid <- ic_provisional_create ic00 ecid Nothing (Just (fromIntegral def_cycles)) Nothing + installAt cid prog + return cid + create_via cid initial_cycles = do + cid2 <- ic_create (ic00viaWithCycles cid initial_cycles) ecid Nothing + ic_set_controllers (ic00via cid) cid2 [store_canister_id, cid] + ic_install_single_chunk (ic00via store_canister_id) (enum #install) cid2 store_canister_id ucan_chunk_hash (run noop) + return cid2 + in [ testGroup "cycles API - backward compatibility" $ + [ simpleTestCase "canister_cycle_balance = canister_cycle_balance128 for numbers fitting in 64 bits" ecid $ \cid -> do + (a, b) <- queryBalanceBalance128 cid + bothSame (a, fromIntegral b), + testCase "legacy API traps when a result is too big" $ do + cid <- create noop + let large = 2 ^ (65 :: Int) + ic_top_up ic00 cid large + query' cid replyBalance >>= isQueryReject ecid [5] + queryBalance128 cid >>= isRoughly (large + fromIntegral def_cycles) + ], + testGroup "can use balance API" $ + let getBalanceTwice = join cat (i64tob getBalance) + test = replyData getBalanceTwice + in [ simpleTestCase "in query" ecid $ \cid -> + query cid test >>= as2Word64 >>= bothSame, + simpleTestCase "in update" ecid $ \cid -> + call cid test >>= as2Word64 >>= bothSame, + testCase "in init" $ do + cid <- install ecid (setGlobal getBalanceTwice) + query cid (replyData getGlobal) >>= as2Word64 >>= bothSame, + simpleTestCase "in callback" ecid $ \cid -> + call cid (inter_query cid defArgs {on_reply = test}) >>= as2Word64 >>= bothSame + ], + testGroup "can use available cycles API" $ + let getAvailableCyclesTwice = join cat (i64tob getAvailableCycles) + test = replyData getAvailableCyclesTwice + in [ simpleTestCase "in update" ecid $ \cid -> + call cid test >>= as2Word64 >>= bothSame, + simpleTestCase "in callback" ecid $ \cid -> + call cid (inter_query cid defArgs {on_reply = test}) >>= as2Word64 >>= bothSame + ], + simpleTestCase "can accept zero cycles" ecid $ \cid -> + call cid (replyData (i64tob (acceptCycles (int64 0)))) >>= asWord64 >>= is 0, + simpleTestCase "can accept more than available cycles" ecid $ \cid -> + call cid (replyData (i64tob (acceptCycles (int64 1)))) >>= asWord64 >>= is 0, + simpleTestCase "can accept absurd amount of cycles" ecid $ \cid -> + call cid (replyData (acceptCycles128 (int64 maxBound) (int64 maxBound))) >>= asWord128 >>= is 0, + testGroup + "provisional_create_canister_with_cycles" + [ testCase "balance as expected" $ do + cid <- create noop + queryBalance cid >>= isRoughly def_cycles, + testCaseSteps "default (i.e. max) balance" $ \step -> do + cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing + installAt cid noop + cycles <- queryBalance128 cid + step $ "Cycle balance now at " ++ show cycles, + testCaseSteps "> 2^128 succeeds" $ \step -> do + cid <- ic_provisional_create ic00 ecid Nothing (Just (10 * 2 ^ (128 :: Int))) Nothing + installAt cid noop + cycles <- queryBalance128 cid + step $ "Cycle balance now at " ++ show cycles + ], + testCase "cycles in canister_status" $ do + cid <- create noop + cs <- ic_canister_status ic00 cid + isRoughly def_cycles (cs .! #cycles), + testGroup + "cycle balance" + [ testCase "install" $ do + cid <- create rememberBalance + query cid recallBalance >>= asWord64 >>= isRoughly def_cycles, + testCase "update" $ do + cid <- create noop + call cid replyBalance >>= asWord64 >>= isRoughly def_cycles, + testCase "query" $ do + cid <- create noop + query cid replyBalance >>= asWord64 >>= isRoughly def_cycles, + testCase "in pre_upgrade" $ do + cid <- create $ onPreUpgrade (callback rememberBalance) + upgrade cid noop + query cid recallBalance >>= asWord64 >>= isRoughly def_cycles, + testCase "in post_upgrade" $ do + cid <- create noop + upgrade cid rememberBalance + query cid recallBalance >>= asWord64 >>= isRoughly def_cycles + queryBalance cid >>= isRoughly def_cycles + ], + testCase "can send cycles" $ do + cid1 <- create noop + cid2 <- create noop + do + call cid1 $ + inter_call + cid2 + "update" + defArgs + { other_side = + replyDataAppend (i64tob getAvailableCycles) + >>> acceptAll + >>> reply, + cycles = def_cycles `div` 4 + } + >>= isRelay + >>= isReply + >>= asWord64 + >>= isRoughly (def_cycles `div` 4) + queryBalance cid1 >>= isRoughly (def_cycles - def_cycles `div` 4) + queryBalance cid2 >>= isRoughly (def_cycles + def_cycles `div` 4), + testCase "sending more cycles than in balance traps" $ do + cid <- create noop + cycles <- queryBalance cid + call' cid (inter_call cid cid defArgs {cycles = cycles + 1000_000}) + >>= isReject [5], + testCase "relay cycles before accept traps" $ do + cid1 <- create noop + cid2 <- create noop + cid3 <- create noop + do + call cid1 $ + inter_call + cid2 + "update" + defArgs + { cycles = def_cycles `div` 2, + other_side = + inter_call + cid3 + "update" + defArgs + { other_side = acceptAll >>> reply, + cycles = def_cycles + def_cycles `div` 4, + on_reply = noop -- must not double reply + } + >>> acceptAll + >>> reply, + on_reply = trap "unexpected reply", + on_reject = replyData (i64tob getRefund) + } + >>= asWord64 + >>= isRoughly (def_cycles `div` 2) + queryBalance cid1 >>= isRoughly def_cycles + queryBalance cid2 >>= isRoughly def_cycles + queryBalance cid3 >>= isRoughly def_cycles, + testCase "relay cycles after accept works" $ do + cid1 <- create noop + cid2 <- create noop + cid3 <- create noop + do + call cid1 $ + inter_call + cid2 + "update" + defArgs + { cycles = def_cycles `div` 2, + other_side = + acceptAll + >>> inter_call + cid3 + "update" + defArgs + { other_side = acceptAll >>> reply, + cycles = def_cycles + def_cycles `div` 4 + }, + on_reply = replyData (i64tob getRefund), + on_reject = trap "unexpected reject" + } + >>= asWord64 + >>= isRoughly (0 :: Word64) + queryBalance cid1 >>= isRoughly (def_cycles `div` 2) + queryBalance cid2 >>= isRoughly (def_cycles `div` 4) + queryBalance cid3 >>= isRoughly (2 * def_cycles + def_cycles `div` 4), + testCase "aborting call resets balance" $ do + cid <- create noop + (a, b) <- + do + call cid $ + callNew "Foo" "Bar" "baz" "quux" + >>> callCyclesAdd (int64 (def_cycles `div` 2)) + >>> replyDataAppend (i64tob getBalance) + >>> callNew "Foo" "Bar" "baz" "quux" + >>> replyDataAppend (i64tob getBalance) + >>> reply + >>= as2Word64 + isRoughly (def_cycles `div` 2) a + isRoughly def_cycles b, + testCase "partial refund" $ do + cid1 <- create noop + cid2 <- create noop + do + call cid1 $ + inter_call + cid2 + "update" + defArgs + { cycles = def_cycles `div` 2, + other_side = ignore (acceptCycles (int64 (def_cycles `div` 4))) >>> reply, + on_reply = replyData (i64tob getRefund), + on_reject = trap "unexpected reject" + } + >>= asWord64 + >>= isRoughly (def_cycles `div` 4) + queryBalance cid1 >>= isRoughly (def_cycles - def_cycles `div` 4) + queryBalance cid2 >>= isRoughly (def_cycles + def_cycles `div` 4), + testCase "cycles not in balance while in transit" $ do + cid1 <- create noop + do + call cid1 $ + inter_call + cid1 + "update" + defArgs + { cycles = def_cycles `div` 4, + other_side = replyBalance, + on_reject = trap "unexpected reject" + } + >>= isRelay + >>= isReply + >>= asWord64 + >>= isRoughly (def_cycles - def_cycles `div` 4) + queryBalance cid1 >>= isRoughly def_cycles, + testCase "create and delete canister with cycles" $ do + cid1 <- create noop + cid2 <- create_via cid1 (def_cycles `div` 2) + queryBalance cid1 >>= isRoughly (def_cycles `div` 2) + queryBalance cid2 >>= isRoughly (def_cycles `div` 2) + ic_stop_canister (ic00via cid1) cid2 + -- We load some cycles on the deletion call, just to check that they are refunded + ic_delete_canister (ic00viaWithCycles cid1 (def_cycles `div` 4)) cid2 + queryBalance cid1 >>= isRoughly (def_cycles `div` 2), + testGroup + "deposit_cycles" + [ testCase "as controller" $ do + cid1 <- create noop + cid2 <- create_via cid1 (def_cycles `div` 2) + queryBalance cid1 >>= isRoughly (def_cycles `div` 2) + queryBalance cid2 >>= isRoughly (def_cycles `div` 2) + ic_deposit_cycles (ic00viaWithCycles cid1 (def_cycles `div` 4)) cid2 + queryBalance cid1 >>= isRoughly (def_cycles `div` 4) + queryBalance cid2 >>= isRoughly (def_cycles - def_cycles `div` 4), + testCase "as other non-controlling canister" $ do + cid1 <- create noop + cid2 <- create_via cid1 (def_cycles `div` 2) + queryBalance cid1 >>= isRoughly (def_cycles `div` 2) + queryBalance cid2 >>= isRoughly (def_cycles `div` 2) + ic_deposit_cycles (ic00viaWithCycles cid2 (def_cycles `div` 4)) cid1 + queryBalance cid1 >>= isRoughly (def_cycles - def_cycles `div` 4) + queryBalance cid2 >>= isRoughly (def_cycles `div` 4), + testCase "to non-existing canister" $ do + cid1 <- create noop + queryBalance cid1 >>= isRoughly def_cycles + ic_deposit_cycles' (ic00viaWithCycles cid1 (def_cycles `div` 4)) doesn'tExist + >>= isReject [3, 4, 5] + queryBalance cid1 >>= isRoughly def_cycles + ], + testCase "two-step-refund" $ do + cid1 <- create noop + do + call cid1 $ + inter_call + cid1 + "update" + defArgs + { cycles = 10, + other_side = + inter_call + cid1 + "update" + defArgs + { cycles = 5, + other_side = reply, -- no accept + on_reply = + -- remember refund + replyDataAppend (i64tob getRefund) + >>> reply, + on_reject = trap "unexpected reject" + }, + on_reply = + -- remember the refund above and this refund + replyDataAppend argData + >>> replyDataAppend (i64tob getRefund) + >>> reply, + on_reject = trap "unexpected reject" + } + >>= as2Word64 + >>= is (5, 10) + queryBalance cid1 >>= isRoughly def_cycles, -- nothing lost? + testGroup + "provisional top up" + [ testCase "as user" $ do + cid <- create noop + queryBalance cid >>= isRoughly def_cycles + ic_top_up ic00 cid (fromIntegral def_cycles) + queryBalance cid >>= isRoughly (2 * def_cycles), + testCase "as self" $ do + cid <- create noop + queryBalance cid >>= isRoughly def_cycles + ic_top_up (ic00via cid) cid (fromIntegral def_cycles) + queryBalance cid >>= isRoughly (2 * def_cycles), + testCase "as other canister" $ do + cid <- create noop + cid2 <- create noop + queryBalance cid >>= isRoughly def_cycles + ic_top_up (ic00via cid2) cid (fromIntegral def_cycles) + queryBalance cid >>= isRoughly (2 * def_cycles), + testCaseSteps "more than 2^128" $ \step -> do + cid <- create noop + ic_top_up ic00 cid (10 * 2 ^ (128 :: Int)) + cycles <- queryBalance128 cid + step $ "Cycle balance now at " ++ show cycles, + testCase "nonexisting canister" $ do + ic_top_up''' ic00' unused_canister_id (fromIntegral def_cycles) + >>= isErrOrReject [3, 5] + ] + ], + testGroup + "canister_inspect_message" + [ testCase "empty canister" $ do + cid <- create ecid + call'' cid reply >>= isNoErrReject [5] + callToQuery'' cid reply >>= isNoErrReject [5], + testCase "accept all" $ do + cid <- install ecid $ onInspectMessage $ callback acceptMessage + call_ cid reply + callToQuery'' cid reply >>= is2xx >>= isReply >>= is "", + testCase "no accept_message" $ do + cid <- install ecid $ onInspectMessage $ callback noop + call'' cid reply >>= isNoErrReject [4] + callToQuery'' cid reply >>= isNoErrReject [4] + -- check that inter-canister calls still work + cid2 <- install ecid noop + call cid2 (inter_update cid defArgs) + >>= isRelay + >>= isReply + >>= is ("Hello " <> cid2 <> " this is " <> cid), + testCase "two calls to accept_message" $ do + cid <- install ecid $ onInspectMessage $ callback $ acceptMessage >>> acceptMessage + call'' cid reply >>= isNoErrReject [5] + callToQuery'' cid reply >>= isNoErrReject [5], + testCase "trap" $ do + cid <- install ecid $ onInspectMessage $ callback $ trap "no no no" + call'' cid reply >>= isNoErrReject [5] + callToQuery'' cid reply >>= isNoErrReject [5], + testCase "method_name correct" $ do + cid <- + install ecid $ + onInspectMessage $ + callback $ + trapIfEq methodName "update" "no no no" >>> acceptMessage + + call'' cid reply >>= isNoErrReject [5] + callToQuery'' cid reply >>= is2xx >>= isReply >>= is "", + testCase "caller correct" $ do + cid <- + install ecid $ + onInspectMessage $ + callback $ + trapIfEq caller (bytes defaultUser) "no no no" >>> acceptMessage + + call'' cid reply >>= isNoErrReject [5] + callToQuery'' cid reply >>= isNoErrReject [5] + + awaitCall' cid (callRequestAs otherUser cid reply) + >>= is2xx + >>= isReply + >>= is "" + awaitCall' cid (callToQueryRequestAs otherUser cid reply) + >>= is2xx + >>= isReply + >>= is "", + testCase "arg correct" $ do + cid <- + install ecid $ + onInspectMessage $ + callback $ + trapIfEq argData (callback reply) "no no no" >>> acceptMessage + + call'' cid reply >>= isNoErrReject [5] + callToQuery'' cid reply >>= isNoErrReject [5] + + call cid (replyData "foo") >>= is "foo" + callToQuery'' cid (replyData "foo") >>= is2xx >>= isReply >>= is "foo", + testCase "management canister: raw_rand not accepted" $ do + ic_raw_rand'' defaultUser ecid >>= isNoErrReject [4], + testCase "management canister: http_request not accepted" $ do + ic_http_get_request'' defaultUser ecid httpbin_proto >>= isNoErrReject [4], + testCase "management canister: ecdsa_public_key not accepted" $ do + ic_ecdsa_public_key'' defaultUser ecid >>= isNoErrReject [4], + testCase "management canister: sign_with_ecdsa not accepted" $ do + ic_sign_with_ecdsa'' defaultUser ecid (sha256 "dummy") >>= isNoErrReject [4], + simpleTestCase "management canister: deposit_cycles not accepted" ecid $ \cid -> do + ic_deposit_cycles'' defaultUser cid >>= isNoErrReject [4], + simpleTestCase "management canister: wrong sender not accepted" ecid $ \cid -> do + ic_canister_status'' otherUser cid >>= isNoErrReject [5] + ], + testGroup "Delegation targets" $ + let callReq cid = + ( rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "update", + "arg" =: GBlob (run reply) + ], + rec + [ "request_type" =: GText "query", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run reply) + ] + ) + + mgmtReq cid = + ( rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob "", + "method_name" =: GText "canister_status", + "arg" =: GBlob (Candid.encode (#canister_id .== Principal cid)) + ], + rec + [ "request_type" =: GText "query", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob "", + "method_name" =: GText "canister_status", + "arg" =: GBlob (Candid.encode (#canister_id .== Principal cid)) + ] + ) + + good cid call query dels = do + call <- addExpiry call + let rid = requestId call + -- sign request with delegations + delegationEnv defaultSK dels call >>= postCallCBOR cid >>= code2xx + -- wait for it + void $ awaitStatus (getRequestStatus' defaultUser cid rid) >>= isReply + -- also read status with delegation + sreq <- + addExpiry $ + rec + [ "request_type" =: GText "read_state", + "sender" =: GBlob defaultUser, + "paths" =: GList [GList [GBlob "request_status", GBlob rid]] + ] + delegationEnv defaultSK dels sreq >>= postReadStateCBOR cid >>= void . code2xx + -- also make query call + query <- addExpiry query + let qrid = requestId query + delegationEnv defaultSK dels query >>= postQueryCBOR cid >>= code2xx + + badSubmit cid req dels = do + req <- addExpiry req + -- sign request with delegations (should fail) + delegationEnv defaultSK dels req >>= postCallCBOR cid >>= code400 + + badRead cid req dels error_code = do + req <- addExpiry req + let rid = requestId req + -- submit with plain signature + envelope defaultSK req >>= postCallCBOR cid >>= code202 + -- wait for it + void $ awaitStatus (getRequestStatus' defaultUser cid rid) >>= isReply + -- also read status with delegation + sreq <- + addExpiry $ + rec + [ "request_type" =: GText "read_state", + "sender" =: GBlob defaultUser, + "paths" =: GList [GList [GBlob "request_status", GBlob rid]] + ] + delegationEnv defaultSK dels sreq >>= postReadStateCBOR cid >>= void . error_code + + badQuery cid req dels = do + req <- addExpiry req + -- sign request with delegations (should fail) + delegationEnv defaultSK dels req >>= postQueryCBOR cid >>= code400 + + goodTestCase name mkReq mkDels = + simpleTestCase name ecid $ \cid -> good cid (fst $ mkReq cid) (snd $ mkReq cid) (mkDels cid) + + badTestCase name mkReq read_state_error_code mkDels = + testGroup + name + [ simpleTestCase "in submit" ecid $ \cid -> badSubmit cid (fst $ mkReq cid) (mkDels cid), + simpleTestCase "in read_state" ecid $ \cid -> badRead cid (fst $ mkReq cid) (mkDels cid) read_state_error_code, + simpleTestCase "in query" ecid $ \cid -> badQuery cid (snd $ mkReq cid) (mkDels cid) + ] + + withEd25519 = zip [createSecretKeyEd25519 (BS.singleton n) | n <- [0 ..]] + withWebAuthnECDSA = zip [createSecretKeyWebAuthnECDSA (BS.singleton n) | n <- [0 ..]] + withWebAuthnRSA = zip [createSecretKeyWebAuthnRSA (BS.singleton n) | n <- [0 ..]] + withSelfLoop = zip [createSecretKeyEd25519 (BS.singleton n) | n <- repeat 0] + withCycle = zip [createSecretKeyEd25519 (BS.singleton n) | n <- [y | _ <- [(0 :: Integer) ..], y <- [0, 1]]] + in [ goodTestCase "one delegation, singleton target" callReq $ \cid -> + withEd25519 [Just [cid]], + badTestCase "one delegation, wrong singleton target" callReq code403 $ \_cid -> + withEd25519 [Just [doesn'tExist]], + goodTestCase "one delegation, two targets" callReq $ \cid -> + withEd25519 [Just [cid, doesn'tExist]], + goodTestCase "one delegation, many targets" callReq $ \cid -> + withEd25519 [Just (cid : map wordToId' [0 .. 998])], + badTestCase "one delegation, too many targets" callReq code400 $ \cid -> + withEd25519 [Just (cid : map wordToId' [0 .. 999])], + goodTestCase "two delegations, two targets, webauthn ECDSA" callReq $ \cid -> + withWebAuthnECDSA [Just [cid, doesn'tExist], Just [cid, doesn'tExist]], + goodTestCase "two delegations, two targets, webauthn RSA" callReq $ \cid -> + withWebAuthnRSA [Just [cid, doesn'tExist], Just [cid, doesn'tExist]], + goodTestCase "one delegation, redundant targets" callReq $ \cid -> + withEd25519 [Just [cid, cid, doesn'tExist]], + goodTestCase "two delegations, singletons" callReq $ \cid -> + withEd25519 [Just [cid], Just [cid]], + goodTestCase "two delegations, first restricted" callReq $ \cid -> + withEd25519 [Just [cid], Nothing], + goodTestCase "two delegations, second restricted" callReq $ \cid -> + withEd25519 [Nothing, Just [cid]], + badTestCase "two delegations, empty intersection" callReq code403 $ \cid -> + withEd25519 [Just [cid], Just [doesn'tExist]], + badTestCase "two delegations, first empty target set" callReq code403 $ \cid -> + withEd25519 [Just [], Just [cid]], + badTestCase "two delegations, second empty target set" callReq code403 $ \cid -> + withEd25519 [Just [cid], Just []], + goodTestCase "20 delegations" callReq $ \cid -> + withEd25519 $ take 20 $ repeat $ Just [cid], + badTestCase "too many delegations" callReq code400 $ \cid -> + withEd25519 $ take 21 $ repeat $ Just [cid], + badTestCase "self-loop in delegations" callReq code400 $ \cid -> + withSelfLoop [Just [cid], Just [cid]], + badTestCase "cycle in delegations" callReq code400 $ \cid -> + withCycle [Just [cid], Just [cid], Just [cid]], + goodTestCase "management canister: correct target" mgmtReq $ \_cid -> + withEd25519 [Just [""]], + badTestCase "management canister: empty target set" mgmtReq code403 $ \_cid -> + withEd25519 [Just []], + badTestCase "management canister: bogus target" mgmtReq code403 $ \_cid -> + withEd25519 [Just [doesn'tExist]], + badTestCase "management canister: bogus target (using target canister)" mgmtReq code403 $ \cid -> + withEd25519 [Just [cid]] + ], + testGroup "Authentication schemes" $ + let ed25519SK2 = createSecretKeyEd25519 "more keys" + ed25519SK3 = createSecretKeyEd25519 "yet more keys" + ed25519SK4 = createSecretKeyEd25519 "even more keys" + delEnv sks = delegationEnv otherSK (map (,Nothing) sks) -- no targets in these tests + in flip + foldMap + [ ("Ed25519", otherUser, envelope otherSK), + ("ECDSA", ecdsaUser, envelope ecdsaSK), + ("secp256k1", secp256k1User, envelope secp256k1SK), + ("WebAuthn ECDSA", webAuthnECDSAUser, envelope webAuthnECDSASK), + ("WebAuthn RSA", webAuthnRSAUser, envelope webAuthnRSASK), + ("empty delegations", otherUser, delEnv []), + ("three delegations", otherUser, delEnv [ed25519SK2, ed25519SK3]), + ("four delegations", otherUser, delEnv [ed25519SK2, ed25519SK3, ed25519SK4]), + ("mixed delegations", otherUser, delEnv [defaultSK, webAuthnRSASK, ecdsaSK, secp256k1SK]) + ] + $ \(name, user, env) -> + [ simpleTestCase (name ++ " in query") ecid $ \cid -> do + let cbor = + rec + [ "request_type" =: GText "query", + "sender" =: GBlob user, + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run reply) + ] + req <- addExpiry cbor + signed_req <- env req + postQueryCBOR cid signed_req >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "", + simpleTestCase (name ++ " in update") ecid $ \cid -> do + req <- + addExpiry $ + rec + [ "request_type" =: GText "call", + "sender" =: GBlob user, + "canister_id" =: GBlob cid, + "method_name" =: GText "update", + "arg" =: GBlob (run reply) + ] + signed_req <- env req + postCallCBOR cid signed_req >>= code2xx + + awaitStatus (getRequestStatus' user cid (requestId req)) >>= isReply >>= is "" + ], + testGroup "signature checking" $ + [ ("with bad signature", return . badEnvelope, id), + ("with wrong key", envelope otherSK, id), + ("with empty domain separator", noDomainSepEnv defaultSK, id), + ("with no expiry", envelope defaultSK, noExpiryEnv), + ("with expiry in the past", envelope defaultSK, pastExpiryEnv), + ("with expiry in the future", envelope defaultSK, futureExpiryEnv) + ] + <&> \(name, env, mod_req) -> + testGroup + name + [ simpleTestCase "in query" ecid $ \cid -> do + let good_cbor = + rec + [ "request_type" =: GText "query", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run ((debugPrint $ i2b $ int 0) >>> reply)) + ] + let bad_cbor = + rec + [ "request_type" =: GText "query", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run ((debugPrint $ i2b $ int 1) >>> reply)) + ] + good_req <- addNonce >=> addExpiry $ good_cbor + bad_req <- addNonce >=> addExpiry $ bad_cbor + (rid, res) <- queryCBOR cid good_req + res <- queryResponse res + isQueryReply ecid (rid, res) >>= is "" + env (mod_req bad_req) >>= postQueryCBOR cid >>= code4xx, + simpleTestCase "in empty read state request" ecid $ \cid -> do + good_req <- addNonce >=> addExpiry $ readStateEmpty + envelope defaultSK good_req >>= postReadStateCBOR cid >>= code2xx + env (mod_req good_req) >>= postReadStateCBOR cid >>= code4xx, + simpleTestCase "in call" ecid $ \cid -> do + good_req <- + addNonce >=> addExpiry $ + rec + [ "request_type" =: GText "call", + "sender" =: GBlob defaultUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run reply) + ] + let req = mod_req good_req + env req >>= postCallCBOR cid >>= code202_or_4xx + + -- Also check that the request was not created + ingressDelay + getRequestStatus defaultUser cid (requestId req) >>= is UnknownStatus + + -- check that with a valid signature, this would have worked + awaitCall cid good_req >>= isReply >>= is "" + ], + testGroup "Canister signatures" $ + let genId cid seed = + DER.encode DER.CanisterSig $ CanisterSig.genPublicKey (EntityId cid) seed + + genSig cid seed msg = do + -- Create the tree + let tree = + construct $ + SubTrees $ + M.singleton "sig" $ + SubTrees $ + M.singleton (sha256 seed) $ + SubTrees $ + M.singleton (sha256 msg) $ + Value "" + -- Store it as certified data + call_ cid (setCertifiedData (bytes (reconstruct tree)) >>> reply) + -- Get certificate + cert <- query cid (replyData getCertificate) >>= decodeCert' + -- double check it certifies + validateStateCert cid cert + certValue cert ["canister", cid, "certified_data"] >>= is (reconstruct tree) + + return $ CanisterSig.genSig cert tree + + exampleQuery cid userKey = + addExpiry $ + rec + [ "request_type" =: GText "query", + "sender" =: GBlob (mkSelfAuthenticatingId userKey), + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run (replyData "It works!")) + ] + simpleEnv userKey sig req delegations = + rec $ + [ "sender_pubkey" =: GBlob userKey, + "sender_sig" =: GBlob sig, + "content" =: req + ] + ++ ["sender_delegation" =: GList delegations | not (null delegations)] + in [ simpleTestCase "direct signature" ecid $ \cid -> do + let userKey = genId cid "Hello!" + req <- exampleQuery cid userKey + sig <- genSig cid "Hello!" $ "\x0Aic-request" <> requestId req + let env = simpleEnv userKey sig req [] + postQueryCBOR cid env >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "It works!", + simpleTestCase "direct signature (empty seed)" ecid $ \cid -> do + let userKey = genId cid "" + req <- exampleQuery cid userKey + sig <- genSig cid "" $ "\x0Aic-request" <> requestId req + let env = simpleEnv userKey sig req [] + postQueryCBOR cid env >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "It works!", + simpleTestCase "direct signature (wrong seed)" ecid $ \cid -> do + let userKey = genId cid "Hello" + req <- exampleQuery cid userKey + sig <- genSig cid "Hullo" $ "\x0Aic-request" <> requestId req + let env = simpleEnv userKey sig req [] + postQueryCBOR cid env >>= code4xx, + simpleTestCase "direct signature (wrong cid)" ecid $ \cid -> do + let userKey = genId doesn'tExist "Hello" + req <- exampleQuery cid userKey + sig <- genSig cid "Hello" $ "\x0Aic-request" <> requestId req + let env = simpleEnv userKey sig req [] + postQueryCBOR cid env >>= code4xx, + simpleTestCase "direct signature (wrong root key)" ecid $ \cid -> do + let seed = "Hello" + let userKey = genId cid seed + req <- exampleQuery cid userKey + let msg = "\x0Aic-request" <> requestId req + -- Create the tree + let tree = + construct $ + SubTrees $ + M.singleton "sig" $ + SubTrees $ + M.singleton (sha256 seed) $ + SubTrees $ + M.singleton (sha256 msg) $ + Value "" + -- Create a fake certificate + let cert_tree = + construct $ + SubTrees $ + M.singleton "canister" $ + SubTrees $ + M.singleton cid $ + SubTrees $ + M.singleton "certified_data" $ + Value (reconstruct tree) + let fake_root_key = createSecretKeyBLS "not the root key" + cert_sig <- sign "ic-state-root" fake_root_key (reconstruct cert_tree) + let cert = Certificate {cert_tree, cert_sig, cert_delegation = Nothing} + let sig = CanisterSig.genSig cert tree + let env = simpleEnv userKey sig req [] + postQueryCBOR cid env >>= code4xx, + simpleTestCase "delegation to Ed25519" ecid $ \cid -> do + let userKey = genId cid "Hello!" + + t <- getPOSIXTime + let expiry = round ((t + 5 * 60) * 1000_000_000) + let delegation = + rec + [ "pubkey" =: GBlob (toPublicKey otherSK), + "expiration" =: GNat expiry + ] + sig <- genSig cid "Hello!" $ "\x1Aic-request-auth-delegation" <> requestId delegation + let signed_delegation = rec ["delegation" =: delegation, "signature" =: GBlob sig] + + req <- exampleQuery cid userKey + sig <- sign "ic-request" otherSK (requestId req) + let env = simpleEnv userKey sig req [signed_delegation] + postQueryCBOR cid env >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "It works!", + simpleTestCase "delegation from Ed25519" ecid $ \cid -> do + let userKey = genId cid "Hello!" + + t <- getPOSIXTime + let expiry = round ((t + 5 * 60) * 1000_000_000) + let delegation = + rec + [ "pubkey" =: GBlob userKey, + "expiration" =: GNat expiry + ] + sig <- sign "ic-request-auth-delegation" otherSK (requestId delegation) + let signed_delegation = rec ["delegation" =: delegation, "signature" =: GBlob sig] + + req <- + addExpiry $ + rec + [ "request_type" =: GText "query", + "sender" =: GBlob otherUser, + "canister_id" =: GBlob cid, + "method_name" =: GText "query", + "arg" =: GBlob (run (replyData "It works!")) + ] + sig <- genSig cid "Hello!" $ "\x0Aic-request" <> requestId req + let env = simpleEnv (toPublicKey otherSK) sig req [signed_delegation] + postQueryCBOR cid env >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "It works!" + ] ] - delegationEnv defaultSK dels sreq >>= postReadStateCBOR cid >>= void . error_code - - badQuery cid req dels = do - req <- addExpiry req - -- sign request with delegations (should fail) - delegationEnv defaultSK dels req >>= postQueryCBOR cid >>= code400 - - goodTestCase name mkReq mkDels = - simpleTestCase name ecid $ \cid -> good cid (fst $ mkReq cid) (snd $ mkReq cid) (mkDels cid) - - badTestCase name mkReq read_state_error_code mkDels = - testGroup - name - [ simpleTestCase "in submit" ecid $ \cid -> badSubmit cid (fst $ mkReq cid) (mkDels cid), - simpleTestCase "in read_state" ecid $ \cid -> badRead cid (fst $ mkReq cid) (mkDels cid) read_state_error_code, - simpleTestCase "in query" ecid $ \cid -> badQuery cid (snd $ mkReq cid) (mkDels cid) - ] - - withEd25519 = zip [createSecretKeyEd25519 (BS.singleton n) | n <- [0 ..]] - withWebAuthnECDSA = zip [createSecretKeyWebAuthnECDSA (BS.singleton n) | n <- [0 ..]] - withWebAuthnRSA = zip [createSecretKeyWebAuthnRSA (BS.singleton n) | n <- [0 ..]] - withSelfLoop = zip [createSecretKeyEd25519 (BS.singleton n) | n <- repeat 0] - withCycle = zip [createSecretKeyEd25519 (BS.singleton n) | n <- [y | _ <- [(0 :: Integer) ..], y <- [0, 1]]] - in [ goodTestCase "one delegation, singleton target" callReq $ \cid -> - withEd25519 [Just [cid]], - badTestCase "one delegation, wrong singleton target" callReq code403 $ \_cid -> - withEd25519 [Just [doesn'tExist]], - goodTestCase "one delegation, two targets" callReq $ \cid -> - withEd25519 [Just [cid, doesn'tExist]], - goodTestCase "one delegation, many targets" callReq $ \cid -> - withEd25519 [Just (cid : map wordToId' [0 .. 998])], - badTestCase "one delegation, too many targets" callReq code400 $ \cid -> - withEd25519 [Just (cid : map wordToId' [0 .. 999])], - goodTestCase "two delegations, two targets, webauthn ECDSA" callReq $ \cid -> - withWebAuthnECDSA [Just [cid, doesn'tExist], Just [cid, doesn'tExist]], - goodTestCase "two delegations, two targets, webauthn RSA" callReq $ \cid -> - withWebAuthnRSA [Just [cid, doesn'tExist], Just [cid, doesn'tExist]], - goodTestCase "one delegation, redundant targets" callReq $ \cid -> - withEd25519 [Just [cid, cid, doesn'tExist]], - goodTestCase "two delegations, singletons" callReq $ \cid -> - withEd25519 [Just [cid], Just [cid]], - goodTestCase "two delegations, first restricted" callReq $ \cid -> - withEd25519 [Just [cid], Nothing], - goodTestCase "two delegations, second restricted" callReq $ \cid -> - withEd25519 [Nothing, Just [cid]], - badTestCase "two delegations, empty intersection" callReq code403 $ \cid -> - withEd25519 [Just [cid], Just [doesn'tExist]], - badTestCase "two delegations, first empty target set" callReq code403 $ \cid -> - withEd25519 [Just [], Just [cid]], - badTestCase "two delegations, second empty target set" callReq code403 $ \cid -> - withEd25519 [Just [cid], Just []], - goodTestCase "20 delegations" callReq $ \cid -> - withEd25519 $ take 20 $ repeat $ Just [cid], - badTestCase "too many delegations" callReq code400 $ \cid -> - withEd25519 $ take 21 $ repeat $ Just [cid], - badTestCase "self-loop in delegations" callReq code400 $ \cid -> - withSelfLoop [Just [cid], Just [cid]], - badTestCase "cycle in delegations" callReq code400 $ \cid -> - withCycle [Just [cid], Just [cid], Just [cid]], - goodTestCase "management canister: correct target" mgmtReq $ \_cid -> - withEd25519 [Just [""]], - badTestCase "management canister: empty target set" mgmtReq code403 $ \_cid -> - withEd25519 [Just []], - badTestCase "management canister: bogus target" mgmtReq code403 $ \_cid -> - withEd25519 [Just [doesn'tExist]], - badTestCase "management canister: bogus target (using target canister)" mgmtReq code403 $ \cid -> - withEd25519 [Just [cid]] - ], - testGroup "Authentication schemes" $ - let ed25519SK2 = createSecretKeyEd25519 "more keys" - ed25519SK3 = createSecretKeyEd25519 "yet more keys" - ed25519SK4 = createSecretKeyEd25519 "even more keys" - delEnv sks = delegationEnv otherSK (map (,Nothing) sks) -- no targets in these tests - in flip - foldMap - [ ("Ed25519", otherUser, envelope otherSK), - ("ECDSA", ecdsaUser, envelope ecdsaSK), - ("secp256k1", secp256k1User, envelope secp256k1SK), - ("WebAuthn ECDSA", webAuthnECDSAUser, envelope webAuthnECDSASK), - ("WebAuthn RSA", webAuthnRSAUser, envelope webAuthnRSASK), - ("empty delegations", otherUser, delEnv []), - ("three delegations", otherUser, delEnv [ed25519SK2, ed25519SK3]), - ("four delegations", otherUser, delEnv [ed25519SK2, ed25519SK3, ed25519SK4]), - ("mixed delegations", otherUser, delEnv [defaultSK, webAuthnRSASK, ecdsaSK, secp256k1SK]) ] - $ \(name, user, env) -> - [ simpleTestCase (name ++ " in query") ecid $ \cid -> do - let cbor = - rec - [ "request_type" =: GText "query", - "sender" =: GBlob user, - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run reply) - ] - req <- addExpiry cbor - signed_req <- env req - postQueryCBOR cid signed_req >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "", - simpleTestCase (name ++ " in update") ecid $ \cid -> do - req <- - addExpiry $ - rec - [ "request_type" =: GText "call", - "sender" =: GBlob user, - "canister_id" =: GBlob cid, - "method_name" =: GText "update", - "arg" =: GBlob (run reply) - ] - signed_req <- env req - postCallCBOR cid signed_req >>= code2xx - - awaitStatus (getRequestStatus' user cid (requestId req)) >>= isReply >>= is "" - ], - testGroup "signature checking" $ - [ ("with bad signature", return . badEnvelope, id), - ("with wrong key", envelope otherSK, id), - ("with empty domain separator", noDomainSepEnv defaultSK, id), - ("with no expiry", envelope defaultSK, noExpiryEnv), - ("with expiry in the past", envelope defaultSK, pastExpiryEnv), - ("with expiry in the future", envelope defaultSK, futureExpiryEnv) - ] - <&> \(name, env, mod_req) -> - testGroup - name - [ simpleTestCase "in query" ecid $ \cid -> do - let good_cbor = - rec - [ "request_type" =: GText "query", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run ((debugPrint $ i2b $ int 0) >>> reply)) - ] - let bad_cbor = - rec - [ "request_type" =: GText "query", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run ((debugPrint $ i2b $ int 1) >>> reply)) - ] - good_req <- addNonce >=> addExpiry $ good_cbor - bad_req <- addNonce >=> addExpiry $ bad_cbor - (rid, res) <- queryCBOR cid good_req - res <- queryResponse res - isQueryReply ecid (rid, res) >>= is "" - env (mod_req bad_req) >>= postQueryCBOR cid >>= code4xx, - simpleTestCase "in empty read state request" ecid $ \cid -> do - good_req <- addNonce >=> addExpiry $ readStateEmpty - envelope defaultSK good_req >>= postReadStateCBOR cid >>= code2xx - env (mod_req good_req) >>= postReadStateCBOR cid >>= code4xx, - simpleTestCase "in call" ecid $ \cid -> do - good_req <- - addNonce >=> addExpiry $ - rec - [ "request_type" =: GText "call", - "sender" =: GBlob defaultUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run reply) - ] - let req = mod_req good_req - env req >>= postCallCBOR cid >>= code202_or_4xx - - -- Also check that the request was not created - ingressDelay - getRequestStatus defaultUser cid (requestId req) >>= is UnknownStatus - - -- check that with a valid signature, this would have worked - awaitCall cid good_req >>= isReply >>= is "" - ], - testGroup "Canister signatures" $ - let genId cid seed = - DER.encode DER.CanisterSig $ CanisterSig.genPublicKey (EntityId cid) seed - - genSig cid seed msg = do - -- Create the tree - let tree = - construct $ - SubTrees $ - M.singleton "sig" $ - SubTrees $ - M.singleton (sha256 seed) $ - SubTrees $ - M.singleton (sha256 msg) $ - Value "" - -- Store it as certified data - call_ cid (setCertifiedData (bytes (reconstruct tree)) >>> reply) - -- Get certificate - cert <- query cid (replyData getCertificate) >>= decodeCert' - -- double check it certifies - validateStateCert cid cert - certValue cert ["canister", cid, "certified_data"] >>= is (reconstruct tree) - - return $ CanisterSig.genSig cert tree - - exampleQuery cid userKey = - addExpiry $ - rec - [ "request_type" =: GText "query", - "sender" =: GBlob (mkSelfAuthenticatingId userKey), - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run (replyData "It works!")) - ] - simpleEnv userKey sig req delegations = - rec $ - [ "sender_pubkey" =: GBlob userKey, - "sender_sig" =: GBlob sig, - "content" =: req - ] - ++ ["sender_delegation" =: GList delegations | not (null delegations)] - in [ simpleTestCase "direct signature" ecid $ \cid -> do - let userKey = genId cid "Hello!" - req <- exampleQuery cid userKey - sig <- genSig cid "Hello!" $ "\x0Aic-request" <> requestId req - let env = simpleEnv userKey sig req [] - postQueryCBOR cid env >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "It works!", - simpleTestCase "direct signature (empty seed)" ecid $ \cid -> do - let userKey = genId cid "" - req <- exampleQuery cid userKey - sig <- genSig cid "" $ "\x0Aic-request" <> requestId req - let env = simpleEnv userKey sig req [] - postQueryCBOR cid env >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "It works!", - simpleTestCase "direct signature (wrong seed)" ecid $ \cid -> do - let userKey = genId cid "Hello" - req <- exampleQuery cid userKey - sig <- genSig cid "Hullo" $ "\x0Aic-request" <> requestId req - let env = simpleEnv userKey sig req [] - postQueryCBOR cid env >>= code4xx, - simpleTestCase "direct signature (wrong cid)" ecid $ \cid -> do - let userKey = genId doesn'tExist "Hello" - req <- exampleQuery cid userKey - sig <- genSig cid "Hello" $ "\x0Aic-request" <> requestId req - let env = simpleEnv userKey sig req [] - postQueryCBOR cid env >>= code4xx, - simpleTestCase "direct signature (wrong root key)" ecid $ \cid -> do - let seed = "Hello" - let userKey = genId cid seed - req <- exampleQuery cid userKey - let msg = "\x0Aic-request" <> requestId req - -- Create the tree - let tree = - construct $ - SubTrees $ - M.singleton "sig" $ - SubTrees $ - M.singleton (sha256 seed) $ - SubTrees $ - M.singleton (sha256 msg) $ - Value "" - -- Create a fake certificate - let cert_tree = - construct $ - SubTrees $ - M.singleton "canister" $ - SubTrees $ - M.singleton cid $ - SubTrees $ - M.singleton "certified_data" $ - Value (reconstruct tree) - let fake_root_key = createSecretKeyBLS "not the root key" - cert_sig <- sign "ic-state-root" fake_root_key (reconstruct cert_tree) - let cert = Certificate {cert_tree, cert_sig, cert_delegation = Nothing} - let sig = CanisterSig.genSig cert tree - let env = simpleEnv userKey sig req [] - postQueryCBOR cid env >>= code4xx, - simpleTestCase "delegation to Ed25519" ecid $ \cid -> do - let userKey = genId cid "Hello!" - - t <- getPOSIXTime - let expiry = round ((t + 5 * 60) * 1000_000_000) - let delegation = - rec - [ "pubkey" =: GBlob (toPublicKey otherSK), - "expiration" =: GNat expiry - ] - sig <- genSig cid "Hello!" $ "\x1Aic-request-auth-delegation" <> requestId delegation - let signed_delegation = rec ["delegation" =: delegation, "signature" =: GBlob sig] - - req <- exampleQuery cid userKey - sig <- sign "ic-request" otherSK (requestId req) - let env = simpleEnv userKey sig req [signed_delegation] - postQueryCBOR cid env >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "It works!", - simpleTestCase "delegation from Ed25519" ecid $ \cid -> do - let userKey = genId cid "Hello!" - - t <- getPOSIXTime - let expiry = round ((t + 5 * 60) * 1000_000_000) - let delegation = - rec - [ "pubkey" =: GBlob userKey, - "expiration" =: GNat expiry - ] - sig <- sign "ic-request-auth-delegation" otherSK (requestId delegation) - let signed_delegation = rec ["delegation" =: delegation, "signature" =: GBlob sig] - - req <- - addExpiry $ - rec - [ "request_type" =: GText "query", - "sender" =: GBlob otherUser, - "canister_id" =: GBlob cid, - "method_name" =: GText "query", - "arg" =: GBlob (run (replyData "It works!")) - ] - sig <- genSig cid "Hello!" $ "\x0Aic-request" <> requestId req - let env = simpleEnv (toPublicKey otherSK) sig req [signed_delegation] - postQueryCBOR cid env >>= okCBOR >>= queryResponse >>= \res -> isQueryReply ecid (requestId req, res) >>= is "It works!" - ] - ] - ] diff --git a/hs/spec_compliance/src/IC/Test/Spec/CanisterHistory.hs b/hs/spec_compliance/src/IC/Test/Spec/CanisterHistory.hs index 1fcb8e5f0ac..0159758782a 100644 --- a/hs/spec_compliance/src/IC/Test/Spec/CanisterHistory.hs +++ b/hs/spec_compliance/src/IC/Test/Spec/CanisterHistory.hs @@ -47,23 +47,23 @@ canister_history_tests ecid = c .! #origin @?= mapChangeOrigin o c .! #details @?= mapChangeDetails d in [ simpleTestCase "after creation and code deployments" ecid $ \unican -> do - universal_wasm <- getTestWasm "universal_canister.wasm.gz" + let Just ucan_chunk_hash = tc_ucan_chunk_hash agentConfig cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing info <- get_canister_info unican cid (Just 1) void $ check_history info 1 [(0, ChangeFromUser (EntityId defaultUser), Creation [(EntityId defaultUser)])] - ic_install ic00 (enum #install) cid universal_wasm (run no_heartbeat) + installAt cid no_heartbeat info <- get_canister_info unican cid (Just 1) - void $ check_history info 2 [(1, ChangeFromUser (EntityId defaultUser), CodeDeployment Install (sha256 universal_wasm))] + void $ check_history info 2 [(1, ChangeFromUser (EntityId defaultUser), CodeDeployment Install ucan_chunk_hash)] ic_install_with_sender_canister_version ic00 (enum #reinstall) cid trivialWasmModule "" (Just 666) -- sender_canister_version in ingress message is ignored info <- get_canister_info unican cid (Just 1) void $ check_history info 3 [(2, ChangeFromUser (EntityId defaultUser), CodeDeployment Reinstall (sha256 trivialWasmModule))] - ic_install ic00 (enumNothing #upgrade) cid universal_wasm (run no_heartbeat) + upgrade cid no_heartbeat info <- get_canister_info unican cid (Just 1) - void $ check_history info 4 [(3, ChangeFromUser (EntityId defaultUser), CodeDeployment Upgrade (sha256 universal_wasm))] + void $ check_history info 4 [(3, ChangeFromUser (EntityId defaultUser), CodeDeployment Upgrade ucan_chunk_hash)] return (), simpleTestCase "after uninstall" ecid $ \unican -> do @@ -129,15 +129,15 @@ canister_history_tests ecid = simpleTestCase "user call to canister_info" ecid $ \cid -> ic_canister_info'' defaultUser cid Nothing >>= is2xx >>= isReject [4], simpleTestCase "calling canister_info" ecid $ \unican -> do - universal_wasm <- getTestWasm "universal_canister.wasm.gz" + let Just ucan_chunk_hash = tc_ucan_chunk_hash agentConfig cid <- ic_provisional_create ic00 ecid Nothing Nothing Nothing ic_install ic00 (enum #install) cid trivialWasmModule "" - ic_install ic00 (enum #reinstall) cid universal_wasm (run no_heartbeat) + reinstall cid no_heartbeat info <- get_canister_info unican cid Nothing info .! #controllers @?= (Vec.fromList [Principal defaultUser]) - info .! #module_hash @?= (Just $ sha256 universal_wasm) + info .! #module_hash @?= (Just $ ucan_chunk_hash) ic_install ic00 (enumNothing #upgrade) cid trivialWasmModule "" @@ -156,7 +156,7 @@ canister_history_tests ecid = let hist = [ (0, ChangeFromUser (EntityId defaultUser), Creation [(EntityId defaultUser)]), (1, ChangeFromUser (EntityId defaultUser), CodeDeployment Install (sha256 trivialWasmModule)), - (2, ChangeFromUser (EntityId defaultUser), CodeDeployment Reinstall (sha256 universal_wasm)), + (2, ChangeFromUser (EntityId defaultUser), CodeDeployment Reinstall ucan_chunk_hash), (3, ChangeFromUser (EntityId defaultUser), CodeDeployment Upgrade (sha256 trivialWasmModule)), (4, ChangeFromUser (EntityId defaultUser), CodeUninstall), (5, ChangeFromUser (EntityId defaultUser), ControllersChange [EntityId otherUser, EntityId defaultUser]) diff --git a/hs/spec_compliance/src/IC/Test/Spec/Timer.hs b/hs/spec_compliance/src/IC/Test/Spec/Timer.hs index acfa9eda033..02b0bb58f41 100644 --- a/hs/spec_compliance/src/IC/Test/Spec/Timer.hs +++ b/hs/spec_compliance/src/IC/Test/Spec/Timer.hs @@ -17,6 +17,7 @@ import qualified Codec.Candid as Candid import Data.ByteString.Builder import Data.Row as R import Data.Time.Clock.POSIX +import qualified Data.Vector as Vec import IC.Management (InstallMode) import IC.Test.Agent import IC.Test.Agent.UnsafeCalls @@ -63,8 +64,7 @@ canister_timer_tests ecid = far_future_time <- get_far_future_time cid <- install ecid $ (on_timer_prog (2 :: Int) >>> onPreUpgrade (callback $ set_timer_prog far_past_time)) _ <- reset_stable cid - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - _ <- ic_install ic00 (enumNothing #upgrade) cid universal_wasm (run noop) + upgrade cid noop timer1 <- get_stable cid timer2 <- set_timer cid far_future_time timer1 @?= blob 0 @@ -75,8 +75,7 @@ canister_timer_tests ecid = far_future_time <- get_far_future_time timer1 <- set_timer cid far_future_time far_far_future_time <- get_far_far_future_time - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - _ <- ic_install ic00 (enumNothing #upgrade) cid universal_wasm (run $ set_timer_prog far_far_future_time) + upgrade cid (set_timer_prog far_far_future_time) timer2 <- get_stable cid timer3 <- set_timer cid far_future_time timer1 @?= blob 0 @@ -88,40 +87,43 @@ canister_timer_tests ecid = far_future_time <- get_far_future_time timer1 <- set_timer cid far_future_time past_time <- get_far_past_time - universal_wasm <- getTestWasm "universal_canister.wasm.gz" _ <- ic_stop_canister ic00 cid waitFor $ do cs <- ic_canister_status ic00 cid if cs .! #status == enum #stopped then return $ Just () else return Nothing - _ <- ic_install ic00 (enumNothing #upgrade) cid universal_wasm (run $ on_timer_prog (2 :: Int) >>> set_timer_prog past_time) + upgrade cid (on_timer_prog (2 :: Int) >>> set_timer_prog past_time) _ <- ic_start_canister ic00 cid wait_for_timer cid 2 timer2 <- set_timer cid far_future_time timer1 @?= blob 0 timer2 @?= blob 0, testCase "in post-upgrade on stopping canister" $ do + let Just store_canister_id = tc_store_canister_id agentConfig + let Just ucan_chunk_hash = tc_ucan_chunk_hash agentConfig cid <- install_canister_with_global_timer (2 :: Int) _ <- reset_stable cid far_future_time <- get_far_future_time timer1 <- set_timer cid far_future_time - cid2 <- install ecid noop - ic_set_controllers ic00 cid [defaultUser, cid2] - universal_wasm <- getTestWasm "universal_canister.wasm.gz" + ic_set_controllers ic00 cid [defaultUser, store_canister_id] past_time <- get_far_past_time let upgrade = - update_call "" "install_code" $ + update_call "" "install_chunked_code" $ defUpdateArgs { uc_arg = Candid.encode $ empty .+ #mode .== ((enumNothing #upgrade) :: InstallMode) - .+ #canister_id + .+ #target_canister .== Principal cid - .+ #wasm_module - .== universal_wasm + .+ #store_canister + .== Principal store_canister_id + .+ #chunk_hashes_list + .== Vec.fromList [empty .+ #hash .== ucan_chunk_hash] + .+ #wasm_module_hash + .== ucan_chunk_hash .+ #arg .== (run $ on_timer_prog (2 :: Int) >>> set_timer_prog past_time) } @@ -133,7 +135,7 @@ canister_timer_tests ecid = ) >>> upgrade let relay = - oneway_call cid2 "update" $ + oneway_call store_canister_id "update" $ defOneWayArgs { ow_arg = run stop_and_upgrade } @@ -205,9 +207,8 @@ canister_timer_tests ecid = far_future_time <- get_far_future_time timer1 <- set_timer cid far_future_time timer2 <- set_timer cid far_future_time - universal_wasm <- getTestWasm "universal_canister.wasm.gz" _ <- ic_uninstall ic00 cid - _ <- ic_install ic00 (enum #install) cid universal_wasm (run $ on_timer_prog (2 :: Int)) + installAt cid (on_timer_prog (2 :: Int)) timer3 <- set_timer cid far_future_time timer1 @?= blob 0 timer2 @?= blob far_future_time @@ -218,8 +219,7 @@ canister_timer_tests ecid = far_future_time <- get_far_future_time timer1 <- set_timer cid far_future_time timer2 <- set_timer cid far_future_time - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - _ <- ic_install ic00 (enumNothing #upgrade) cid universal_wasm (run $ on_timer_prog (2 :: Int)) + upgrade cid (on_timer_prog (2 :: Int)) timer3 <- set_timer cid far_future_time timer1 @?= blob 0 timer2 @?= blob far_future_time @@ -230,8 +230,7 @@ canister_timer_tests ecid = far_future_time <- get_far_future_time timer1 <- set_timer cid far_future_time timer2 <- set_timer cid far_future_time - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - _ <- ic_install ic00 (enum #reinstall) cid universal_wasm (run $ on_timer_prog (2 :: Int)) + reinstall cid (on_timer_prog (2 :: Int)) timer3 <- set_timer cid far_future_time timer1 @?= blob 0 timer2 @?= blob far_future_time diff --git a/hs/spec_compliance/src/IC/Test/Spec/Utils.hs b/hs/spec_compliance/src/IC/Test/Spec/Utils.hs index 1f9d7248059..2f1eb6130c4 100644 --- a/hs/spec_compliance/src/IC/Test/Spec/Utils.hs +++ b/hs/spec_compliance/src/IC/Test/Spec/Utils.hs @@ -205,14 +205,16 @@ ic00viaWithCyclesRefund amount = ic00viaWithCyclesSubnetImpl (relayReplyRefund a -- The unprimed variant expect a reply. install' :: (HasCallStack, HasAgentConfig) => Blob -> Prog -> IO ReqResponse -install' cid prog = do - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install' ic00 (enum #install) cid universal_wasm (run prog) +install' cid prog = ic_install_single_chunk' ic00 (enum #install) cid store_canister_id ucan_chunk_hash (run prog) + where + Just store_canister_id = tc_store_canister_id agentConfig + Just ucan_chunk_hash = tc_ucan_chunk_hash agentConfig installAt :: (HasCallStack, HasAgentConfig) => Blob -> Prog -> IO () -installAt cid prog = do - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install ic00 (enum #install) cid universal_wasm (run prog) +installAt cid prog = ic_install_single_chunk ic00 (enum #install) cid store_canister_id ucan_chunk_hash (run prog) + where + Just store_canister_id = tc_store_canister_id agentConfig + Just ucan_chunk_hash = tc_ucan_chunk_hash agentConfig -- Also calls create, used default 'ic00' install :: (HasCallStack, HasAgentConfig) => Blob -> Prog -> IO Blob @@ -225,24 +227,28 @@ create :: (HasCallStack, HasAgentConfig) => Blob -> IO Blob create ecid = ic_provisional_create ic00 ecid Nothing (Just (2 ^ (60 :: Int))) Nothing upgrade' :: (HasCallStack, HasAgentConfig) => Blob -> Prog -> IO ReqResponse -upgrade' cid prog = do - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install' ic00 (enumNothing #upgrade) cid universal_wasm (run prog) +upgrade' cid prog = ic_install_single_chunk' ic00 (enumNothing #upgrade) cid store_canister_id ucan_chunk_hash (run prog) + where + Just store_canister_id = tc_store_canister_id agentConfig + Just ucan_chunk_hash = tc_ucan_chunk_hash agentConfig upgrade :: (HasCallStack, HasAgentConfig) => Blob -> Prog -> IO () -upgrade cid prog = do - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install ic00 (enumNothing #upgrade) cid universal_wasm (run prog) +upgrade cid prog = ic_install_single_chunk ic00 (enumNothing #upgrade) cid store_canister_id ucan_chunk_hash (run prog) + where + Just store_canister_id = tc_store_canister_id agentConfig + Just ucan_chunk_hash = tc_ucan_chunk_hash agentConfig reinstall' :: (HasCallStack, HasAgentConfig) => Blob -> Prog -> IO ReqResponse -reinstall' cid prog = do - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install' ic00 (enum #reinstall) cid universal_wasm (run prog) +reinstall' cid prog = ic_install_single_chunk' ic00 (enum #reinstall) cid store_canister_id ucan_chunk_hash (run prog) + where + Just store_canister_id = tc_store_canister_id agentConfig + Just ucan_chunk_hash = tc_ucan_chunk_hash agentConfig reinstall :: (HasCallStack, HasAgentConfig) => Blob -> Prog -> IO () -reinstall cid prog = do - universal_wasm <- getTestWasm "universal_canister.wasm.gz" - ic_install ic00 (enum #reinstall) cid universal_wasm (run prog) +reinstall cid prog = ic_install_single_chunk ic00 (enum #reinstall) cid store_canister_id ucan_chunk_hash (run prog) + where + Just store_canister_id = tc_store_canister_id agentConfig + Just ucan_chunk_hash = tc_ucan_chunk_hash agentConfig callRequestAs :: (HasCallStack, HasAgentConfig) => Blob -> Blob -> Prog -> GenR callRequestAs user cid prog = diff --git a/rs/pocket_ic_server/tests/spec_test.rs b/rs/pocket_ic_server/tests/spec_test.rs index 0ce04e39949..7a744209f65 100644 --- a/rs/pocket_ic_server/tests/spec_test.rs +++ b/rs/pocket_ic_server/tests/spec_test.rs @@ -161,7 +161,7 @@ fn setup_and_run_ic_ref_test(test_nns: bool, excluded_tests: Vec<&str>, included peer_subnet_config, excluded_tests, included_tests, - 16, + 64, ); } From 0df03ce7448dc94b9eb5b07e65498ba0d3c500bb Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:17:29 +0200 Subject: [PATCH 21/22] chore(PocketIC): use sequence numbers as state labels (#2157) This PR replaces a state label derived from the certified state by a sequence number. The reason for this change is that parts of a PocketIC instance state are not certified (e.g., ingress and canister http pools) and thus they are not reflected in the state label. Because it is not clear why having the same state label for two different instances with the same state would be beneficial at the moment, this PR uses sequence numbers as state labels. This PR also fixes a bug when deleting an instance: an instance should not be deleted if it is still busy with a computation. Finally, - the size of the test `//packages/pocket-ic:test` is reduced back to small (this test is fast again); - outdated comments are removed; - unnecessary Debug implementations are removed. --------- Co-authored-by: IDX GitHub Automation --- packages/pocket-ic/BUILD.bazel | 2 +- rs/pocket_ic_server/src/pocket_ic.rs | 297 +++----------------- rs/pocket_ic_server/src/state_api/routes.rs | 28 +- rs/pocket_ic_server/src/state_api/state.rs | 88 +++--- 4 files changed, 89 insertions(+), 326 deletions(-) diff --git a/packages/pocket-ic/BUILD.bazel b/packages/pocket-ic/BUILD.bazel index 6a085ff5af5..d6b0948ceb8 100644 --- a/packages/pocket-ic/BUILD.bazel +++ b/packages/pocket-ic/BUILD.bazel @@ -46,7 +46,7 @@ rust_library( rust_test_suite( name = "test", - size = "medium", + size = "small", srcs = ["tests/tests.rs"], data = [ "//packages/pocket-ic/test_canister:test_canister.wasm", diff --git a/rs/pocket_ic_server/src/pocket_ic.rs b/rs/pocket_ic_server/src/pocket_ic.rs index b2ff44996eb..f93d5496556 100644 --- a/rs/pocket_ic_server/src/pocket_ic.rs +++ b/rs/pocket_ic_server/src/pocket_ic.rs @@ -231,10 +231,7 @@ pub struct PocketIc { routing_table: RoutingTable, /// Created on initialization and updated if a new subnet is created. topology: TopologyInternal, - // The initial state hash used for computing the state label - // to distinguish PocketIC instances with different initial configs. - initial_state_hash: [u8; 32], - // The following fields are used to create a new subnet. + state_label: StateLabel, range_gen: RangeGen, registry_data_provider: Arc, runtime: Arc, @@ -389,6 +386,7 @@ impl PocketIc { pub(crate) fn new( runtime: Arc, + seed: u64, subnet_configs: ExtendedSubnetConfigSet, state_dir: Option, nonmainnet_features: bool, @@ -656,15 +654,6 @@ impl PocketIc { subnet.execute_round(); } - let mut hasher = Sha256::new(); - let subnet_configs_string = format!("{:?}", subnet_configs); - hasher.write(subnet_configs_string.as_bytes()); - let initial_state_hash = compute_state_label( - &hasher.finish(), - subnets.read().unwrap().values().cloned().collect(), - ) - .0; - let canister_http_adapters = Arc::new(TokioMutex::new( subnets .read() @@ -699,13 +688,15 @@ impl PocketIc { default_effective_canister_id, }; + let state_label = StateLabel::new(seed); + Self { state_dir, subnets, canister_http_adapters, routing_table, topology, - initial_state_hash, + state_label, range_gen, registry_data_provider, runtime, @@ -716,6 +707,10 @@ impl PocketIc { } } + pub(crate) fn bump_state_label(&mut self) { + self.state_label.bump(); + } + fn try_route_canister(&self, canister_id: CanisterId) -> Option> { let subnet_id = self.routing_table.route(canister_id.into()); subnet_id.map(|subnet_id| self.get_subnet_with_id(subnet_id).unwrap()) @@ -765,6 +760,7 @@ impl Default for PocketIc { fn default() -> Self { Self::new( Runtime::new().unwrap().into(), + 0, ExtendedSubnetConfigSet { application: vec![SubnetSpec::default()], ..Default::default() @@ -777,31 +773,9 @@ impl Default for PocketIc { } } -fn compute_state_label( - initial_state_hash: &[u8; 32], - subnets: Vec>, -) -> StateLabel { - let mut hasher = Sha256::new(); - hasher.write(initial_state_hash); - for subnet in subnets { - let subnet_state_hash = subnet - .state_manager - .latest_state_certification_hash() - .map(|(_, h)| h.0) - .unwrap_or_else(|| [0u8; 32].to_vec()); - let nanos = systemtime_to_unix_epoch_nanos(subnet.time()); - hasher.write(&subnet_state_hash[..]); - hasher.write(&nanos.to_be_bytes()); - } - StateLabel(hasher.finish()) -} - impl HasStateLabel for PocketIc { fn get_state_label(&self) -> StateLabel { - compute_state_label( - &self.initial_state_hash, - self.subnets.read().unwrap().values().cloned().collect(), - ) + self.state_label.clone() } } @@ -2597,165 +2571,11 @@ mod tests { #[test] fn state_label_test() { // State label changes. - let pic = PocketIc::default(); - let state0 = pic.get_state_label(); - let canister_id = pic.any_subnet().create_canister(None); - pic.any_subnet().add_cycles(canister_id, 2_000_000_000_000); - let state1 = pic.get_state_label(); - pic.any_subnet().stop_canister(canister_id).unwrap(); - pic.any_subnet().delete_canister(canister_id).unwrap(); - let state2 = pic.get_state_label(); - - assert_ne!(state0, state1); - assert_ne!(state1, state2); - assert_ne!(state0, state2); - - // Empyt IC. - let pic = PocketIc::default(); - let state1 = pic.get_state_label(); - let pic = PocketIc::default(); - let state2 = pic.get_state_label(); - - assert_eq!(state1, state2); - - // Two ICs with the same state. - let pic = PocketIc::default(); - let cid = pic.any_subnet().create_canister(None); - pic.any_subnet().add_cycles(cid, 2_000_000_000_000); - pic.any_subnet().stop_canister(cid).unwrap(); - let state3 = pic.get_state_label(); - - let pic = PocketIc::default(); - let cid = pic.any_subnet().create_canister(None); - pic.any_subnet().add_cycles(cid, 2_000_000_000_000); - pic.any_subnet().stop_canister(cid).unwrap(); - let state4 = pic.get_state_label(); - - assert_eq!(state3, state4); - } - - #[test] - fn test_time() { - let mut pic = PocketIc::default(); - - let unix_time_ns = 1640995200000000000; // 1st Jan 2022 - let time = Time::from_nanos_since_unix_epoch(unix_time_ns); - compute_assert_state_change(&mut pic, SetTime { time }); - let actual_time = compute_assert_state_immutable(&mut pic, GetTime {}); - - match actual_time { - OpOut::Time(actual_time_ns) => assert_eq!(unix_time_ns, actual_time_ns), - _ => panic!("Unexpected OpOut: {:?}", actual_time), - }; - } - - #[test] - fn test_execute_message() { - let (mut pic, canister_id) = new_pic_counter_installed(); - let amount: u128 = 20_000_000_000_000; - let add_cycles = AddCycles { - canister_id, - amount, - }; - add_cycles.compute(&mut pic); - - let update = ExecuteIngressMessage(CanisterCall { - sender: PrincipalId::new_anonymous(), - canister_id, - method: "write".into(), - payload: vec![], - effective_principal: EffectivePrincipal::None, - }); - - compute_assert_state_change(&mut pic, update); - } - - #[test] - fn test_cycles_burn_app_subnet() { - let (mut pic, canister_id) = new_pic_counter_installed(); - let (_, update) = query_update_constructors(canister_id); - let cycles_balance = GetCyclesBalance { canister_id }; - let OpOut::Cycles(initial_balance) = - compute_assert_state_immutable(&mut pic, cycles_balance.clone()) - else { - unreachable!() - }; - compute_assert_state_change(&mut pic, update("write")); - let OpOut::Cycles(new_balance) = compute_assert_state_immutable(&mut pic, cycles_balance) - else { - unreachable!() - }; - assert_ne!(initial_balance, new_balance); - } - - #[test] - fn test_cycles_burn_system_subnet() { - let (mut pic, canister_id) = new_pic_counter_installed_system_subnet(); - let (_, update) = query_update_constructors(canister_id); - - let cycles_balance = GetCyclesBalance { canister_id }; - let OpOut::Cycles(initial_balance) = - compute_assert_state_immutable(&mut pic, cycles_balance.clone()) - else { - unreachable!() - }; - compute_assert_state_change(&mut pic, update("write")); - let OpOut::Cycles(new_balance) = compute_assert_state_immutable(&mut pic, cycles_balance) - else { - unreachable!() - }; - assert_eq!(initial_balance, new_balance); - } - - fn query_update_constructors( - canister_id: CanisterId, - ) -> ( - impl Fn(&str) -> Query, - impl Fn(&str) -> ExecuteIngressMessage, - ) { - let call = move |method: &str| CanisterCall { - sender: PrincipalId::new_anonymous(), - canister_id, - method: method.into(), - payload: vec![], - effective_principal: EffectivePrincipal::None, - }; - - let update = move |m: &str| ExecuteIngressMessage(call(m)); - let query = move |m: &str| Query(call(m)); - - (query, update) - } - - fn new_pic_counter_installed() -> (PocketIc, CanisterId) { - let mut pic = PocketIc::default(); - let canister_id = pic.any_subnet().create_canister(None); - - let amount: u128 = 20_000_000_000_000; - let add_cycles = AddCycles { - canister_id, - amount, - }; - add_cycles.compute(&mut pic); - - let module = counter_wasm(); - let install_op = InstallCanisterAsController { - canister_id, - mode: CanisterInstallMode::Install, - module, - payload: vec![], - }; - - compute_assert_state_change(&mut pic, install_op); - - (pic, canister_id) - } - - fn new_pic_counter_installed_system_subnet() -> (PocketIc, CanisterId) { - let mut pic = PocketIc::new( + let mut pic0 = PocketIc::new( Runtime::new().unwrap().into(), + 0, ExtendedSubnetConfigSet { - ii: Some(SubnetSpec::default()), + application: vec![SubnetSpec::default()], ..Default::default() }, None, @@ -2763,74 +2583,29 @@ mod tests { None, None, ); - let canister_id = pic.any_subnet().create_canister(None); - - let module = counter_wasm(); - let install_op = InstallCanisterAsController { - canister_id, - mode: CanisterInstallMode::Install, - module, - payload: vec![], - }; - - compute_assert_state_change(&mut pic, install_op); - - (pic, canister_id) - } - - fn compute_assert_state_change(pic: &mut PocketIc, op: impl Operation) -> OpOut { - let state0 = pic.get_state_label(); - let res = op.compute(pic); - let state1 = pic.get_state_label(); - assert_ne!(state0, state1); - res - } + let mut pic1 = PocketIc::new( + Runtime::new().unwrap().into(), + 1, + ExtendedSubnetConfigSet { + application: vec![SubnetSpec::default()], + ..Default::default() + }, + None, + false, + None, + None, + ); + assert_ne!(pic0.get_state_label(), pic1.get_state_label()); - fn compute_assert_state_immutable(pic: &mut PocketIc, op: impl Operation) -> OpOut { - let state0 = pic.get_state_label(); - let res = op.compute(pic); - let state1 = pic.get_state_label(); - assert_eq!(state0, state1); - res - } + let pic0_state_label = pic0.get_state_label(); + pic0.bump_state_label(); + assert_ne!(pic0.get_state_label(), pic0_state_label); + assert_ne!(pic0.get_state_label(), pic1.get_state_label()); - fn counter_wasm() -> Vec { - wat::parse_str(COUNTER_WAT).unwrap().as_slice().to_vec() + let pic1_state_label = pic1.get_state_label(); + pic1.bump_state_label(); + assert_ne!(pic1.get_state_label(), pic0_state_label); + assert_ne!(pic1.get_state_label(), pic1_state_label); + assert_ne!(pic1.get_state_label(), pic0.get_state_label()); } - - const COUNTER_WAT: &str = r#" -;; Counter with global variable ;; -(module - (import "ic0" "msg_reply" (func $msg_reply)) - (import "ic0" "msg_reply_data_append" - (func $msg_reply_data_append (param i32 i32))) - - (func $read - (i32.store - (i32.const 0) - (global.get 0) - ) - (call $msg_reply_data_append - (i32.const 0) - (i32.const 4)) - (call $msg_reply)) - - (func $write - (global.set 0 - (i32.add - (global.get 0) - (i32.const 1) - ) - ) - (call $read) - ) - - (memory $memory 1) - (export "memory" (memory $memory)) - (global (export "counter_global") (mut i32) (i32.const 0)) - (export "canister_query read" (func $read)) - (export "canister_query inc_read" (func $write)) - (export "canister_update write" (func $write)) -) - "#; } diff --git a/rs/pocket_ic_server/src/state_api/routes.rs b/rs/pocket_ic_server/src/state_api/routes.rs index a66f2e62be5..a2d196f5cf0 100644 --- a/rs/pocket_ic_server/src/state_api/routes.rs +++ b/rs/pocket_ic_server/src/state_api/routes.rs @@ -1112,21 +1112,19 @@ pub async fn create_instance( None }; - let pocket_ic = tokio::task::spawn_blocking(move || { - PocketIc::new( - runtime, - subnet_configs, - instance_config.state_dir, - instance_config.nonmainnet_features, - log_level, - instance_config.bitcoind_addr, - ) - }) - .await - .expect("Failed to launch PocketIC"); - - let topology = pocket_ic.topology().clone(); - let instance_id = api_state.add_instance(pocket_ic).await; + let (instance_id, topology) = api_state + .add_instance(move |seed| { + PocketIc::new( + runtime, + seed, + subnet_configs, + instance_config.state_dir, + instance_config.nonmainnet_features, + log_level, + instance_config.bitcoind_addr, + ) + }) + .await; ( StatusCode::CREATED, Json(rest::CreateInstanceResponse::Created { diff --git a/rs/pocket_ic_server/src/state_api/state.rs b/rs/pocket_ic_server/src/state_api/state.rs index 9ac7a5b08eb..976ef947e5f 100644 --- a/rs/pocket_ic_server/src/state_api/state.rs +++ b/rs/pocket_ic_server/src/state_api/state.rs @@ -52,7 +52,10 @@ use pocket_ic::common::rest::{ }; use pocket_ic::{ErrorCode, UserError, WasmResult}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; +use std::{ + collections::HashMap, fmt, path::PathBuf, str::FromStr, sync::atomic::AtomicU64, sync::Arc, + time::Duration, +}; use tokio::{ sync::mpsc::error::TryRecvError, sync::mpsc::Receiver, @@ -74,12 +77,26 @@ const MIN_OPERATION_DELAY: Duration = Duration::from_millis(100); // The minimum delay between consecutive attempts to read the graph in auto progress mode. const READ_GRAPH_DELAY: Duration = Duration::from_millis(100); -pub const STATE_LABEL_HASH_SIZE: usize = 32; +pub const STATE_LABEL_HASH_SIZE: usize = 16; /// Uniquely identifies a state. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize)] +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] pub struct StateLabel(pub [u8; STATE_LABEL_HASH_SIZE]); +impl StateLabel { + pub fn new(seed: u64) -> Self { + let mut seq_no: u128 = seed.into(); + seq_no <<= 64; + Self(seq_no.to_le_bytes()) + } + + pub fn bump(&mut self) { + let mut seq_no: u128 = u128::from_le_bytes(self.0); + seq_no += 1; + self.0 = seq_no.to_le_bytes(); + } +} + // The only error condition is if the vector has the wrong size. pub struct InvalidSize; @@ -116,18 +133,12 @@ struct Instance { state: InstanceState, } -impl std::fmt::Debug for Instance { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "{:?}", self.state)?; - Ok(()) - } -} - /// The state of the PocketIC API. pub struct ApiState { // impl note: If locks are acquired on both fields, acquire first on `instances` and then on `graph`. instances: Arc>>>, graph: Arc>>, + seed: AtomicU64, sync_wait_time: Duration, // PocketIC server port port: Option, @@ -194,6 +205,7 @@ impl PocketIcApiStateBuilder { Arc::new(ApiState { instances, graph, + seed: AtomicU64::new(0), sync_wait_time, port: self.port, http_gateways: Arc::new(RwLock::new(Vec::new())), @@ -711,14 +723,23 @@ impl ApiState { Self::read_result(self.graph.clone(), state_label, op_id) } - pub async fn add_instance(&self, instance: PocketIc) -> InstanceId { + pub async fn add_instance(&self, f: F) -> (InstanceId, Topology) + where + F: FnOnce(u64) -> PocketIc + std::marker::Send + 'static, + { + let seed = self.seed.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + // create the instance using `spawn_blocking` before acquiring a lock + let instance = tokio::task::spawn_blocking(move || f(seed)) + .await + .expect("Failed to create PocketIC instance"); + let topology = instance.topology(); let mut instances = self.instances.write().await; let instance_id = instances.len(); instances.push(Mutex::new(Instance { progress_thread: None, state: InstanceState::Available(instance), })); - instance_id + (instance_id, topology) } pub async fn delete_instance(&self, instance_id: InstanceId) { @@ -726,9 +747,9 @@ impl ApiState { loop { let instances = self.instances.read().await; let mut instance = instances[instance_id].lock().await; - match std::mem::replace(&mut instance.state, InstanceState::Deleted) { - InstanceState::Available(pocket_ic) => { - std::mem::drop(pocket_ic); + match &instance.state { + InstanceState::Available(_) => { + let _ = std::mem::replace(&mut instance.state, InstanceState::Deleted); break; } InstanceState::Deleted => { @@ -1407,6 +1428,7 @@ impl ApiState { op_id.0, ); let result = op.compute(&mut pocket_ic); + pocket_ic.bump_state_label(); let new_state_label = pocket_ic.get_state_label(); // add result to graph, but grab instance lock first! let instances = instances.blocking_read(); @@ -1424,8 +1446,7 @@ impl ApiState { instance.state = InstanceState::Available(pocket_ic); } trace!("bg_task::end instance_id={} op_id={}", instance_id, op_id.0); - // also return old_state_label so we can prune graph if we return quickly - (result, old_state_label) + result } }; @@ -1458,7 +1479,7 @@ impl ApiState { // note: this assumes that cancelling the JoinHandle does not stop the execution of the // background task. This only works because the background thread, in this case, is a // kernel thread. - if let Ok(Ok((op_out, _old_state_label))) = time::timeout(sync_wait_time, bg_handle).await { + if let Ok(Ok(op_out)) = time::timeout(sync_wait_time, bg_handle).await { trace!( "update_with_timeout::synchronous instance_id={} op_id={}", instance_id, @@ -1475,34 +1496,3 @@ impl ApiState { Ok(busy_outcome) } } - -impl std::fmt::Debug for InstanceState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Busy { state_label, op_id } => { - write!(f, "Busy {{ {state_label:?}, {op_id:?} }}")? - } - Self::Available(pic) => write!(f, "Available({:?})", pic.get_state_label())?, - Self::Deleted => write!(f, "Deleted")?, - } - Ok(()) - } -} - -impl std::fmt::Debug for ApiState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let instances = self.instances.blocking_read(); - let graph = self.graph.blocking_read(); - - writeln!(f, "Instances:")?; - for (idx, instance) in instances.iter().enumerate() { - writeln!(f, " [{idx}] {instance:?}")?; - } - - writeln!(f, "Graph:")?; - for (k, v) in graph.iter() { - writeln!(f, " {k:?} => {v:?}")?; - } - Ok(()) - } -} From b56d5e1ba9c513ec5960fdcd26e2293be6bca388 Mon Sep 17 00:00:00 2001 From: mihailjianu1 <32812419+mihailjianu1@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:22:49 +0200 Subject: [PATCH 22/22] fix: synchronize properly with tokio::Notify instead of sleeping (#2178) --- rs/bitcoin/adapter/benches/e2e.rs | 31 ++------ rs/bitcoin/adapter/src/lib.rs | 77 ++++++++++++------ rs/bitcoin/adapter/src/router.rs | 112 +++++++++++++-------------- rs/bitcoin/adapter/src/rpc_server.rs | 20 ++--- 4 files changed, 125 insertions(+), 115 deletions(-) diff --git a/rs/bitcoin/adapter/benches/e2e.rs b/rs/bitcoin/adapter/benches/e2e.rs index f4700968ca0..cb4070648b6 100644 --- a/rs/bitcoin/adapter/benches/e2e.rs +++ b/rs/bitcoin/adapter/benches/e2e.rs @@ -1,11 +1,8 @@ use bitcoin::{Block, BlockHash, BlockHeader, Network}; use criterion::{criterion_group, criterion_main, Criterion}; use ic_btc_adapter::config::IncomingSource; -use ic_btc_adapter::start_grpc_server; -use ic_btc_adapter::AdapterState; -use ic_btc_adapter::{ - config::Config, BlockchainManagerRequest, BlockchainState, GetSuccessorsHandler, -}; +use ic_btc_adapter::start_server; +use ic_btc_adapter::{config::Config, BlockchainState}; use ic_btc_adapter_client::setup_bitcoin_adapter_clients; use ic_btc_adapter_test_utils::generate_headers; use ic_btc_replica_types::BitcoinAdapterRequestWrapper; @@ -18,9 +15,7 @@ use ic_interfaces_adapter_client::RpcAdapterClient; use ic_logger::replica_logger::no_op_logger; use ic_metrics::MetricsRegistry; use std::path::Path; -use std::sync::{Arc, Mutex}; use tempfile::Builder; -use tokio::sync::mpsc::channel; type BitcoinAdapterClient = Box< dyn RpcAdapterClient, @@ -96,8 +91,6 @@ fn e2e(criterion: &mut Criterion) { 1975, ); - let blockchain_state = Arc::new(Mutex::new(blockchain_state)); - let rt = tokio::runtime::Runtime::new().unwrap(); let (client, _temp) = Builder::new() @@ -105,26 +98,12 @@ fn e2e(criterion: &mut Criterion) { Ok(rt.block_on(async { config.incoming_source = IncomingSource::Path(uds_path.to_path_buf()); - let (blockchain_manager_tx, _) = channel::(10); - let handler = GetSuccessorsHandler::new( - &config, - blockchain_state.clone(), - blockchain_manager_tx, + start_server( + &no_op_logger(), &MetricsRegistry::default(), - ); - - let adapter_state = AdapterState::new(config.idle_seconds); - - let (transaction_manager_tx, _) = channel(100); - start_grpc_server( + rt.handle(), config.clone(), - no_op_logger(), - adapter_state.clone(), - handler, - transaction_manager_tx, - &MetricsRegistry::default(), ); - start_client(uds_path).await })) }) diff --git a/rs/bitcoin/adapter/src/lib.rs b/rs/bitcoin/adapter/src/lib.rs index 94637d1e303..a5178d4fec4 100644 --- a/rs/bitcoin/adapter/src/lib.rs +++ b/rs/bitcoin/adapter/src/lib.rs @@ -7,13 +7,17 @@ use bitcoin::{network::message::NetworkMessage, BlockHash, BlockHeader}; use ic_logger::ReplicaLogger; use ic_metrics::MetricsRegistry; -use parking_lot::RwLock; +use std::time::Duration; use std::{ net::SocketAddr, sync::{Arc, Mutex}, time::Instant, }; -use tokio::sync::mpsc::channel; +use tokio::{ + select, + sync::{mpsc::channel, watch}, + time::sleep, +}; /// This module contains the AddressManager struct. The struct stores addresses /// that will be used to create new connections. It also tracks addresses that /// are in current use to encourage use from non-utilized addresses. @@ -141,7 +145,11 @@ pub enum TransactionManagerRequest { /// thread-safe. #[derive(Clone)] pub struct AdapterState { - /// The field contains instant of the latest received request. + /// The field contains how long the adapter should wait before becoming idle. + idle_seconds: u64, + + /// The watch channel that holds the last received time. + /// The field contains the instant of the latest received request. /// None means that we haven't reveived a request yet and the adapter should be in idle mode! /// /// !!! BE CAREFUL HERE !!! since the adapter should ALWAYS be idle when starting up. @@ -151,33 +159,54 @@ pub struct AdapterState { /// This way the adapter would always be in idle when starting since 'elapsed()' is greater than 'idle_seconds'. /// On MacOS this approach caused issues since on MacOS Instant::now() is time since boot and when subtracting /// 'idle_seconds' we encountered an underflow and panicked. - last_received_at: Arc>>, - /// The field contains how long the adapter should wait to before becoming idle. - idle_seconds: u64, + /// + /// It's simportant that this value is set to [`None`] on startup. + last_received_rx: watch::Receiver>, } impl AdapterState { - /// Crates new instance of the AdapterState. - pub fn new(idle_seconds: u64) -> Self { - Self { - last_received_at: Arc::new(RwLock::new(None)), - idle_seconds, - } + /// Creates a new instance of the [`AdapterState`]. + pub fn new(idle_seconds: u64) -> (Self, watch::Sender>) { + // Initialize the watch channel with `None`, indicating no requests have been received yet. + let (tx, last_received_rx) = watch::channel(None); + ( + Self { + idle_seconds, + last_received_rx, + }, + tx, + ) } - /// Returns if the adapter is idle. - pub fn is_idle(&self) -> bool { - match *self.last_received_at.read() { - Some(last) => last.elapsed().as_secs() > self.idle_seconds, - // Nothing received yet still in idle from startup. - None => true, + /// A future that returns when/if the adapter becomes/is idle. + pub async fn idle(&mut self) { + let mut last_time = self + .last_received_rx + .borrow_and_update() + .unwrap_or_else(Instant::now); + + loop { + let seconds_left_until_idle = self.idle_seconds - last_time.elapsed().as_secs(); + select! { + _ = sleep(Duration::from_secs(seconds_left_until_idle)) => {return}, + Ok(_) = self.last_received_rx.changed() => { + last_time = self.last_received_rx.borrow_and_update().unwrap_or_else(Instant::now); + } + } } } - /// Updates the current state of the adapter given a request was received. - pub fn received_now(&self) { - // Instant::now() is monotonically nondecreasing clock. - *self.last_received_at.write() = Some(Instant::now()); + /// A future that returns when/if the adapter becomes/is awake. + pub async fn active(&mut self) { + let _ = self + .last_received_rx + .wait_for(|v| { + if let Some(last) = v { + return last.elapsed().as_secs() < self.idle_seconds; + } + false + }) + .await; } } @@ -190,7 +219,7 @@ pub fn start_server( ) { let _enter = rt_handle.enter(); - let adapter_state = AdapterState::new(config.idle_seconds); + let (adapter_state, tx) = AdapterState::new(config.idle_seconds); let (blockchain_manager_tx, blockchain_manager_rx) = channel(100); let blockchain_state = Arc::new(Mutex::new(BlockchainState::new(&config, metrics_registry))); @@ -208,7 +237,7 @@ pub fn start_server( start_grpc_server( config.clone(), log.clone(), - adapter_state.clone(), + tx, get_successors_handler, transaction_manager_tx, metrics_registry, diff --git a/rs/bitcoin/adapter/src/router.rs b/rs/bitcoin/adapter/src/router.rs index 05e54fab985..34d7d02737c 100644 --- a/rs/bitcoin/adapter/src/router.rs +++ b/rs/bitcoin/adapter/src/router.rs @@ -15,7 +15,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::{ sync::mpsc::{channel, Receiver}, - time::{interval, sleep}, + time::interval, }; /// The function starts a Tokio task that awaits messages from the ConnectionManager. @@ -28,7 +28,7 @@ pub fn start_main_event_loop( logger: ReplicaLogger, blockchain_state: Arc>, mut transaction_manager_rx: Receiver, - adapter_state: AdapterState, + mut adapter_state: AdapterState, mut blockchain_manager_rx: Receiver, metrics_registry: &MetricsRegistry, ) { @@ -50,67 +50,67 @@ pub fn start_main_event_loop( tokio::task::spawn(async move { let mut tick_interval = interval(Duration::from_millis(100)); loop { - let sleep_idle_interval = Duration::from_millis(100); - if adapter_state.is_idle() { - connection_manager.make_idle(); - blockchain_manager.make_idle(); - // TODO: instead of sleeping here add some async synchronization. - sleep(sleep_idle_interval).await; - continue; - } + adapter_state.active().await; // We do a select over tokio::sync::mpsc::Receiver::recv, tokio::sync::mpsc::UnboundedReceiver::recv, // tokio::time::Interval::tick which are all cancellation safe. - tokio::select! { - event = connection_manager.receive_stream_event() => { - if let Err(ProcessBitcoinNetworkMessageError::InvalidMessage) = - connection_manager.process_event(&event) - { - connection_manager.discard(&event.address); - } - }, - network_message = network_message_receiver.recv() => { - let (address, message) = network_message.unwrap(); - router_metrics - .bitcoin_messages_received - .with_label_values(&[message.cmd()]) - .inc(); - if let Err(ProcessBitcoinNetworkMessageError::InvalidMessage) = - connection_manager.process_bitcoin_network_message(address, &message) { - connection_manager.discard(&address); - } + loop { + tokio::select! { + _ = adapter_state.idle() => { + break; + }, + event = connection_manager.receive_stream_event() => { + if let Err(ProcessBitcoinNetworkMessageError::InvalidMessage) = + connection_manager.process_event(&event) + { + connection_manager.discard(&event.address); + } + }, + network_message = network_message_receiver.recv() => { + let (address, message) = network_message.unwrap(); + router_metrics + .bitcoin_messages_received + .with_label_values(&[message.cmd()]) + .inc(); + if let Err(ProcessBitcoinNetworkMessageError::InvalidMessage) = + connection_manager.process_bitcoin_network_message(address, &message) { + connection_manager.discard(&address); + } - if let Err(ProcessBitcoinNetworkMessageError::InvalidMessage) = blockchain_manager.process_bitcoin_network_message(&mut connection_manager, address, &message) { - connection_manager.discard(&address); - } - if let Err(ProcessBitcoinNetworkMessageError::InvalidMessage) = transaction_manager.process_bitcoin_network_message(&mut connection_manager, address, &message) { - connection_manager.discard(&address); - } - }, - result = blockchain_manager_rx.recv() => { - let command = result.expect("Receiving should not fail because the sender part of the channel is never closed."); - match command { - BlockchainManagerRequest::EnqueueNewBlocksToDownload(next_headers) => { - blockchain_manager.enqueue_new_blocks_to_download(next_headers); + if let Err(ProcessBitcoinNetworkMessageError::InvalidMessage) = blockchain_manager.process_bitcoin_network_message(&mut connection_manager, address, &message) { + connection_manager.discard(&address); } - BlockchainManagerRequest::PruneBlocks(anchor, processed_block_hashes) => { - blockchain_manager.prune_blocks(anchor, processed_block_hashes); + if let Err(ProcessBitcoinNetworkMessageError::InvalidMessage) = transaction_manager.process_bitcoin_network_message(&mut connection_manager, address, &message) { + connection_manager.discard(&address); } - }; - } - transaction_manager_request = transaction_manager_rx.recv() => { - match transaction_manager_request.unwrap() { - TransactionManagerRequest::SendTransaction(transaction) => transaction_manager.enqueue_transaction(&transaction), + }, + result = blockchain_manager_rx.recv() => { + let command = result.expect("Receiving should not fail because the sender part of the channel is never closed."); + match command { + BlockchainManagerRequest::EnqueueNewBlocksToDownload(next_headers) => { + blockchain_manager.enqueue_new_blocks_to_download(next_headers); + } + BlockchainManagerRequest::PruneBlocks(anchor, processed_block_hashes) => { + blockchain_manager.prune_blocks(anchor, processed_block_hashes); + } + }; } - }, - _ = tick_interval.tick() => { - // After an event is dispatched, the managers `tick` method is called to process possible - // outgoing messages. - connection_manager.tick(blockchain_manager.get_height(), handle_stream); - blockchain_manager.tick(&mut connection_manager); - transaction_manager.advertise_txids(&mut connection_manager); - } - }; + transaction_manager_request = transaction_manager_rx.recv() => { + match transaction_manager_request.unwrap() { + TransactionManagerRequest::SendTransaction(transaction) => transaction_manager.enqueue_transaction(&transaction), + } + }, + _ = tick_interval.tick() => { + // After an event is dispatched, the managers `tick` method is called to process possible + // outgoing messages. + connection_manager.tick(blockchain_manager.get_height(), handle_stream); + blockchain_manager.tick(&mut connection_manager); + transaction_manager.advertise_txids(&mut connection_manager); + } + }; + } + connection_manager.make_idle(); + blockchain_manager.make_idle(); } }); } diff --git a/rs/bitcoin/adapter/src/rpc_server.rs b/rs/bitcoin/adapter/src/rpc_server.rs index fe86e727075..fd9239e4197 100644 --- a/rs/bitcoin/adapter/src/rpc_server.rs +++ b/rs/bitcoin/adapter/src/rpc_server.rs @@ -2,7 +2,7 @@ use crate::{ config::{Config, IncomingSource}, get_successors_handler::{GetSuccessorsRequest, GetSuccessorsResponse}, metrics::{ServiceMetrics, LABEL_GET_SUCCESSOR, LABEL_SEND_TRANSACTION}, - AdapterState, GetSuccessorsHandler, TransactionManagerRequest, + GetSuccessorsHandler, TransactionManagerRequest, }; use bitcoin::{consensus::Encodable, hashes::Hash, BlockHash}; use ic_async_utils::{incoming_from_first_systemd_socket, incoming_from_path}; @@ -14,13 +14,15 @@ use ic_btc_service::{ use ic_logger::{debug, ReplicaLogger}; use ic_metrics::MetricsRegistry; use std::convert::{TryFrom, TryInto}; -use tokio::sync::mpsc::Sender; +use std::time::Instant; +use tokio::sync::mpsc; +use tokio::sync::watch; use tonic::{transport::Server, Request, Response, Status}; struct BtcServiceImpl { - adapter_state: AdapterState, + last_received_tx: watch::Sender>, get_successors_handler: GetSuccessorsHandler, - transaction_manager_tx: Sender, + transaction_manager_tx: mpsc::Sender, logger: ReplicaLogger, metrics: ServiceMetrics, } @@ -83,7 +85,7 @@ impl BtcService for BtcServiceImpl { .request_duration .with_label_values(&[LABEL_GET_SUCCESSOR]) .start_timer(); - self.adapter_state.received_now(); + let _ = self.last_received_tx.send(Some(Instant::now())); let inner = request.into_inner(); debug!(self.logger, "Received GetSuccessorsRequest: {:?}", inner); let request = inner.try_into()?; @@ -108,7 +110,7 @@ impl BtcService for BtcServiceImpl { .request_duration .with_label_values(&[LABEL_SEND_TRANSACTION]) .start_timer(); - self.adapter_state.received_now(); + let _ = self.last_received_tx.send(Some(Instant::now())); let transaction = request.into_inner().transaction; self.transaction_manager_tx .send(TransactionManagerRequest::SendTransaction(transaction)) @@ -124,13 +126,13 @@ impl BtcService for BtcServiceImpl { pub fn start_grpc_server( config: Config, logger: ReplicaLogger, - adapter_state: AdapterState, + last_received_tx: watch::Sender>, get_successors_handler: GetSuccessorsHandler, - transaction_manager_tx: Sender, + transaction_manager_tx: mpsc::Sender, metrics_registry: &MetricsRegistry, ) { let btc_adapter_impl = BtcServiceImpl { - adapter_state, + last_received_tx, get_successors_handler, transaction_manager_tx, logger,