From 858c038162bfbd9b451ffdad9d8706fd43b0a13e Mon Sep 17 00:00:00 2001 From: michael-weigelt <122277901+michael-weigelt@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:21:42 +0100 Subject: [PATCH] feat: [EXC-1753] Add mint_cycles128 API (#2589) This PR adds a new system API method `ic0_mint_cycles128` which, like its predecessor `ic0_mint_cycles`, is not usable by any canister except the CMC. It is therefore not necessary to add it to the interface spec or the SDKs. It also means that this system call does not need an instruction cost adjustment. The method's signature deviates from the predecessor because 128 bit values cannot be handled by WASM natively. Therefore, the argument is split into two `u64`s, representing the high and low bits. The amount of actually minted cycles cannot be returned as a WASM return value either. Instead, the result is written to a caller-specified location in memory, where it occupies the next 16 bytes. --------- Co-authored-by: Michael Weigelt Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> --- rs/cycles_account_manager/src/lib.rs | 6 +- rs/embedders/src/wasm_utils/validation.rs | 10 +++ .../src/wasmtime_embedder/system_api.rs | 12 +++ .../benches/system_api/diff-old-vs-new.sh | 5 ++ .../benches/system_api/execute_update.rs | 26 +++++- .../src/query_handler/query_cache/tests.rs | 1 + rs/execution_environment/tests/hypervisor.rs | 66 ++++++++++++++ rs/interfaces/src/execution_environment.rs | 22 ++++- rs/system_api/src/lib.rs | 43 +++++++++- .../src/sandbox_safe_system_state.rs | 2 +- .../tests/sandbox_safe_system_state.rs | 53 ++++++++++++ rs/system_api/tests/system_api.rs | 18 +++- rs/tests/execution/general_execution_test.rs | 4 + .../general_execution_tests/nns_shielding.rs | 85 +++++++++++++++++-- rs/universal_canister/impl/src/api.rs | 8 ++ rs/universal_canister/impl/src/lib.rs | 1 + rs/universal_canister/impl/src/main.rs | 5 ++ rs/universal_canister/lib/src/lib.rs | 8 ++ 18 files changed, 352 insertions(+), 23 deletions(-) diff --git a/rs/cycles_account_manager/src/lib.rs b/rs/cycles_account_manager/src/lib.rs index 64583fdc160..18c5e8e2c45 100644 --- a/rs/cycles_account_manager/src/lib.rs +++ b/rs/cycles_account_manager/src/lib.rs @@ -1015,7 +1015,7 @@ impl CyclesAccountManager { canister_id: CanisterId, cycles_balance: &mut Cycles, amount_to_mint: Cycles, - ) -> Result<(), CyclesAccountManagerError> { + ) -> Result { if canister_id != CYCLES_MINTING_CANISTER_ID { let error_str = format!( "ic0.mint_cycles cannot be executed on non Cycles Minting Canister: {} != {}", @@ -1023,8 +1023,10 @@ impl CyclesAccountManager { ); Err(CyclesAccountManagerError::ContractViolation(error_str)) } else { + let before_balance = *cycles_balance; *cycles_balance += amount_to_mint; - Ok(()) + // equal to amount_to_mint, except when the addition saturated + Ok(*cycles_balance - before_balance) } } diff --git a/rs/embedders/src/wasm_utils/validation.rs b/rs/embedders/src/wasm_utils/validation.rs index f6cf898a876..f2517c87ba3 100644 --- a/rs/embedders/src/wasm_utils/validation.rs +++ b/rs/embedders/src/wasm_utils/validation.rs @@ -527,6 +527,16 @@ fn get_valid_system_apis_common(I: ValType) -> HashMap, amount_high: u64, amount_low: u64, dst: I| { + with_memory_and_system_api(&mut caller, |s, memory| { + let dst: usize = dst.try_into().expect("Failed to convert I to usize"); + s.ic0_mint_cycles128(Cycles::from_parts(amount_high, amount_low), dst, memory) + }) + .map_err(|e| anyhow::Error::msg(format!("ic0_mint_cycles128 failed: {}", e))) + } + }) + .unwrap(); + linker .func_wrap("ic0", "cycles_burn128", { move |mut caller: Caller<'_, StoreData>, amount_high: u64, amount_low: u64, dst: I| { diff --git a/rs/execution_environment/benches/system_api/diff-old-vs-new.sh b/rs/execution_environment/benches/system_api/diff-old-vs-new.sh index f6405fd9f2a..049b784e8c3 100755 --- a/rs/execution_environment/benches/system_api/diff-old-vs-new.sh +++ b/rs/execution_environment/benches/system_api/diff-old-vs-new.sh @@ -17,6 +17,11 @@ set -ue ## | update/ic0_canister_status() | 1.27G | 1.34G | +5% | 3.73s | ## | inspect/ic0_msg_method_name_size() | - | 1.28G | - | 23.92s | +if ! which bazel rg >/dev/null; then + echo "Error checking dependencies: please ensure 'bazel' and 'rg' are installed" + exit 1 +fi + ## To quickly assess the new changes, run benchmarks just once QUICK=${QUICK:-} if [ -n "${QUICK}" ]; then diff --git a/rs/execution_environment/benches/system_api/execute_update.rs b/rs/execution_environment/benches/system_api/execute_update.rs index dadd339da0a..af476e7d48d 100644 --- a/rs/execution_environment/benches/system_api/execute_update.rs +++ b/rs/execution_environment/benches/system_api/execute_update.rs @@ -336,7 +336,7 @@ pub fn execute_update_bench(c: &mut Criterion) { Result::No, Wasm64::Enabled, ), // 10B max - 529001006, + 529004006, ), common::Benchmark( "wasm32/ic0_debug_print()/1B".into(), @@ -736,7 +736,7 @@ pub fn execute_update_bench(c: &mut Criterion) { Result::No, Wasm64::Enabled, ), - 517001006, + 517004006, ), common::Benchmark( "wasm32/ic0_msg_cycles_available()".into(), @@ -878,6 +878,26 @@ pub fn execute_update_bench(c: &mut Criterion) { Module::Test.from_ic0("mint_cycles", Param1(1_i64), Result::I64, Wasm64::Enabled), 18000006, ), + common::Benchmark( + "wasm32/ic0_mint_cycles128()".into(), + Module::Test.from_ic0( + "mint_cycles128", + Params3(1_i64, 2_i64, 3_i32), + Result::No, + Wasm64::Disabled, + ), + 19001006, + ), + common::Benchmark( + "wasm64/ic0_mint_cycles128()".into(), + Module::Test.from_ic0( + "mint_cycles128", + Params3(1_i64, 2_i64, 3_i64), + Result::No, + Wasm64::Enabled, + ), + 19004006, + ), common::Benchmark( "wasm32/ic0_is_controller()".into(), Module::Test.from_ic0( @@ -922,7 +942,7 @@ pub fn execute_update_bench(c: &mut Criterion) { "wasm32/ic0_cycles_burn128()".into(), Module::Test.from_ic0( "cycles_burn128", - Params3(1_i64, 2_i64, 3), + Params3(1_i64, 2_i64, 3_i32), Result::No, Wasm64::Disabled, ), diff --git a/rs/execution_environment/src/query_handler/query_cache/tests.rs b/rs/execution_environment/src/query_handler/query_cache/tests.rs index 73a65d05c39..009c6beb9d9 100644 --- a/rs/execution_environment/src/query_handler/query_cache/tests.rs +++ b/rs/execution_environment/src/query_handler/query_cache/tests.rs @@ -1488,6 +1488,7 @@ fn query_cache_future_proof_test() { | SystemApiCallId::InReplicatedExecution | SystemApiCallId::IsController | SystemApiCallId::MintCycles + | SystemApiCallId::MintCycles128 | SystemApiCallId::MsgArgDataCopy | SystemApiCallId::MsgArgDataSize | SystemApiCallId::MsgCallerCopy diff --git a/rs/execution_environment/tests/hypervisor.rs b/rs/execution_environment/tests/hypervisor.rs index e8f78482208..7f9f649b104 100644 --- a/rs/execution_environment/tests/hypervisor.rs +++ b/rs/execution_environment/tests/hypervisor.rs @@ -2280,6 +2280,72 @@ fn ic0_mint_cycles_succeeds_on_cmc() { ); } +// helper for mint_cycles128 tests +fn verify_error_and_no_effect(mut test: ExecutionTest) { + let canister_id = test.universal_canister().unwrap(); + let initial_cycles = test.canister_state(canister_id).system_state.balance(); + let payload = wasm() + .mint_cycles128(Cycles::from(10_000_000_000_u128)) + .reply_data_append() + .reply() + .build(); + let err = test.ingress(canister_id, "update", payload).unwrap_err(); + assert_eq!(ErrorCode::CanisterContractViolation, err.code()); + assert!(err + .description() + .contains("ic0.mint_cycles cannot be executed")); + let canister_state = test.canister_state(canister_id); + assert_eq!(0, canister_state.system_state.queues().output_queues_len()); + assert_balance_equals( + initial_cycles, + canister_state.system_state.balance(), + BALANCE_EPSILON, + ); +} + +#[test] +fn ic0_mint_cycles128_fails_on_application_subnet() { + let test = ExecutionTestBuilder::new().build(); + verify_error_and_no_effect(test); +} + +#[test] +fn ic0_mint_cycles128_fails_on_system_subnet_non_cmc() { + let test = ExecutionTestBuilder::new() + .with_subnet_type(SubnetType::System) + .build(); + verify_error_and_no_effect(test); +} + +#[test] +fn ic0_mint_cycles128_succeeds_on_cmc() { + let mut test = ExecutionTestBuilder::new() + .with_subnet_type(SubnetType::System) + .build(); + let mut canister_id = test.universal_canister().unwrap(); + for _ in 0..4 { + canister_id = test.universal_canister().unwrap(); + } + assert_eq!(canister_id, CYCLES_MINTING_CANISTER_ID); + let initial_cycles = test.canister_state(canister_id).system_state.balance(); + let amount: u128 = (1u128 << 64) + 2u128; + let payload = wasm() + .mint_cycles128(Cycles::from(amount)) + .reply_data_append() + .reply() + .build(); + let result = test.ingress(canister_id, "update", payload).unwrap(); + assert_eq!(WasmResult::Reply(amount.to_le_bytes().to_vec()), result); + let canister_state = test.canister_state(canister_id); + + assert_eq!(0, canister_state.system_state.queues().output_queues_len()); + assert_balance_equals( + initial_cycles + Cycles::new(amount), + canister_state.system_state.balance(), + BALANCE_EPSILON, + ); +} + #[test] fn ic0_call_enqueues_request() { let mut test = ExecutionTestBuilder::new().build(); diff --git a/rs/interfaces/src/execution_environment.rs b/rs/interfaces/src/execution_environment.rs index a89b81ae2e9..970adef0fd3 100644 --- a/rs/interfaces/src/execution_environment.rs +++ b/rs/interfaces/src/execution_environment.rs @@ -175,6 +175,8 @@ pub enum SystemApiCallId { IsController, /// Tracker for `ic0.mint_cycles()` MintCycles, + /// Tracker for `ic0.mint_cycles128()` + MintCycles128, /// Tracker for `ic0.msg_arg_data_copy()` MsgArgDataCopy, /// Tracker for `ic0.msg_arg_data_size()` @@ -1125,13 +1127,25 @@ pub trait SystemApi { /// /// Adds no more cycles than `amount`. /// - /// The canister balance afterwards does not exceed - /// maximum amount of cycles it can hold. - /// However, canisters on system subnets have no balance limit. - /// /// Returns the amount of cycles added to the canister's balance. fn ic0_mint_cycles(&mut self, amount: u64) -> HypervisorResult; + /// Mints the `amount` cycles + /// Adds cycles to the canister's balance. + /// + /// Adds no more cycles than `amount`. The balance afterwards cannot + /// exceed u128::MAX, so the amount added may be less than `amount`. + /// + /// The amount of cycles added to the canister's balance is + /// represented by a 128-bit value and is copied in the canister + /// memory starting at the location `dst`. + fn ic0_mint_cycles128( + &mut self, + amount: Cycles, + dst: usize, + heap: &mut [u8], + ) -> HypervisorResult<()>; + /// Checks whether the principal identified by src/size is one of the /// controllers of the canister. If yes, then a value of 1 is returned, /// otherwise a 0 is returned. It can be called multiple times. diff --git a/rs/system_api/src/lib.rs b/rs/system_api/src/lib.rs index dc29d477041..88b69568043 100644 --- a/rs/system_api/src/lib.rs +++ b/rs/system_api/src/lib.rs @@ -3168,7 +3168,8 @@ impl SystemApi for SystemApiImpl { trace_syscall!(self, CanisterStatus, result); result } - + // TODO(EXC-1806): This can be removed (in favour of ic0_mint_cycles128) once the CMC is upgraded, so it + // doesn't make sense to deduplicate the shared code. fn ic0_mint_cycles(&mut self, amount: u64) -> HypervisorResult { let result = match self.api_type { ApiType::Start { .. } @@ -3187,9 +3188,12 @@ impl SystemApi for SystemApiImpl { // Access to this syscall not permitted. Err(self.error_for("ic0_mint_cycles")) } else { - self.sandbox_safe_system_state + let actually_minted = self + .sandbox_safe_system_state .mint_cycles(Cycles::from(amount))?; - Ok(amount) + // the actually minted amount cannot be larger than the argument, which is a u64. + debug_assert_eq!(actually_minted.high64(), 0, "ic0_mint_cycles was called with u64 but minted more cycles than fit into 64 bit"); + Ok(actually_minted.low64()) } } }; @@ -3197,6 +3201,39 @@ impl SystemApi for SystemApiImpl { result } + fn ic0_mint_cycles128( + &mut self, + amount: Cycles, + dst: usize, + heap: &mut [u8], + ) -> HypervisorResult<()> { + let result = match self.api_type { + ApiType::Start { .. } + | ApiType::Init { .. } + | ApiType::PreUpgrade { .. } + | ApiType::Cleanup { .. } + | ApiType::ReplicatedQuery { .. } + | ApiType::NonReplicatedQuery { .. } + | ApiType::InspectMessage { .. } => Err(self.error_for("ic0_mint_cycles128")), + ApiType::Update { .. } + | ApiType::SystemTask { .. } + | ApiType::ReplyCallback { .. } + | ApiType::RejectCallback { .. } => { + if self.execution_parameters.execution_mode == ExecutionMode::NonReplicated { + // Non-replicated mode means we are handling a composite query. + // Access to this syscall not permitted. + Err(self.error_for("ic0_mint_cycles128")) + } else { + let actually_minted = self.sandbox_safe_system_state.mint_cycles(amount)?; + copy_cycles_to_heap(actually_minted, dst, heap, "ic0_mint_cycles_128")?; + Ok(()) + } + } + }; + trace_syscall!(self, MintCycles128, result, amount); + result + } + fn ic0_debug_print(&self, src: usize, size: usize, heap: &[u8]) -> HypervisorResult<()> { const MAX_DEBUG_MESSAGE_SIZE: usize = 32 * 1024; let size = size.min(MAX_DEBUG_MESSAGE_SIZE); diff --git a/rs/system_api/src/sandbox_safe_system_state.rs b/rs/system_api/src/sandbox_safe_system_state.rs index 50bbfe3b3fc..27ad59f8c39 100644 --- a/rs/system_api/src/sandbox_safe_system_state.rs +++ b/rs/system_api/src/sandbox_safe_system_state.rs @@ -881,7 +881,7 @@ impl SandboxSafeSystemState { self.update_balance_change(new_balance); } - pub(super) fn mint_cycles(&mut self, amount_to_mint: Cycles) -> HypervisorResult<()> { + pub(super) fn mint_cycles(&mut self, amount_to_mint: Cycles) -> HypervisorResult { let mut new_balance = self.cycles_balance(); let result = self .cycles_account_manager diff --git a/rs/system_api/tests/sandbox_safe_system_state.rs b/rs/system_api/tests/sandbox_safe_system_state.rs index 8945641c532..2307aa6285f 100644 --- a/rs/system_api/tests/sandbox_safe_system_state.rs +++ b/rs/system_api/tests/sandbox_safe_system_state.rs @@ -349,6 +349,59 @@ fn mint_cycles_fails_caller_not_on_nns() { ); } +fn common_mint_cycles_128( + initial_cycles: Cycles, + cycles_to_mint: Cycles, + expected_actually_minted: Cycles, +) { + let cycles_account_manager = CyclesAccountManagerBuilder::new() + .with_subnet_type(SubnetType::System) + .build(); + let system_state = SystemStateBuilder::new() + .initial_cycles(initial_cycles) + .canister_id(CYCLES_MINTING_CANISTER_ID) + .build(); + + let api_type = ApiTypeBuilder::build_update_api(); + let mut api = get_system_api(api_type, &system_state, cycles_account_manager); + let mut balance_before = [0u8; 16]; + api.ic0_canister_cycle_balance128(0, &mut balance_before) + .unwrap(); + let balance_before = u128::from_le_bytes(balance_before); + assert_eq!(balance_before, initial_cycles.get()); + let mut heap = [0u8; 16]; + api.ic0_mint_cycles128(cycles_to_mint, 0, &mut heap) + .unwrap(); + let cycles_minted = u128::from_le_bytes(heap); + assert_eq!(cycles_minted, expected_actually_minted.get()); + let mut balance_after = [0u8; 16]; + api.ic0_canister_cycle_balance128(0, &mut balance_after) + .unwrap(); + let balance_after = u128::from_le_bytes(balance_after); + assert_eq!( + balance_after - balance_before, + expected_actually_minted.get() + ); +} + +#[test] +fn mint_cycles_very_large_value() { + let to_mint = Cycles::from_parts(u64::MAX, 50); + common_mint_cycles_128(INITIAL_CYCLES, to_mint, to_mint); +} + +#[test] +fn mint_cycles_max() { + let to_mint = Cycles::from_parts(u64::MAX, u64::MAX); + common_mint_cycles_128(Cycles::zero(), to_mint, to_mint); +} + +#[test] +fn mint_cycles_saturate() { + let to_mint = Cycles::from_parts(u64::MAX, u64::MAX); + common_mint_cycles_128(INITIAL_CYCLES, to_mint, to_mint - INITIAL_CYCLES); +} + #[test] fn is_controller_test() { let mut system_state = SystemStateBuilder::default().build(); diff --git a/rs/system_api/tests/system_api.rs b/rs/system_api/tests/system_api.rs index 923c2ef4340..5cb55e539c1 100644 --- a/rs/system_api/tests/system_api.rs +++ b/rs/system_api/tests/system_api.rs @@ -293,7 +293,8 @@ fn is_supported(api_type: SystemApiCallId, context: &str) -> bool { SystemApiCallId::InReplicatedExecution => vec!["*", "s"], SystemApiCallId::DebugPrint => vec!["*", "s"], SystemApiCallId::Trap => vec!["*", "s"], - SystemApiCallId::MintCycles => vec!["U", "Ry", "Rt", "T"] + SystemApiCallId::MintCycles => vec!["U", "Ry", "Rt", "T"], + SystemApiCallId::MintCycles128 => vec!["U", "Ry", "Rt", "T"] }; // the semantics of "*" is to cover all modes except for "s" matrix.get(&api_type).unwrap().contains(&context) @@ -745,6 +746,11 @@ fn api_availability_test( let mut api = get_system_api(api_type, &system_state, cycles_account_manager); assert_api_not_supported(api.ic0_mint_cycles(0)); } + SystemApiCallId::MintCycles128 => { + // ic0.mint_cycles128 is only supported for CMC which is tested separately + let mut api = get_system_api(api_type, &system_state, cycles_account_manager); + assert_api_not_supported(api.ic0_mint_cycles128(Cycles::zero(), 0, &mut [0u8; 16])); + } SystemApiCallId::IsController => { assert_api_availability( |api| api.ic0_is_controller(0, 0, &[42; 128]), @@ -822,7 +828,7 @@ fn system_api_availability() { let api = get_system_api(api_type.clone(), &system_state, cycles_account_manager); check_stable_apis_support(api); - // check ic0.mint_cycles API availability for CMC + // check ic0.mint_cycles, ic0.mint_cycles128 API availability for CMC let cmc_system_state = get_cmc_system_state(); assert_api_availability( |mut api| api.ic0_mint_cycles(0), @@ -832,6 +838,14 @@ fn system_api_availability() { SystemApiCallId::MintCycles, context, ); + assert_api_availability( + |mut api| api.ic0_mint_cycles128(Cycles::zero(), 0, &mut [0u8; 16]), + api_type.clone(), + &cmc_system_state, + cycles_account_manager, + SystemApiCallId::MintCycles128, + context, + ); // now check all other API availability for non-CMC for api_type_enum in SystemApiCallId::iter() { diff --git a/rs/tests/execution/general_execution_test.rs b/rs/tests/execution/general_execution_test.rs index 0768a864174..a317c029208 100644 --- a/rs/tests/execution/general_execution_test.rs +++ b/rs/tests/execution/general_execution_test.rs @@ -88,6 +88,10 @@ fn main() -> Result<()> { mint_cycles_supported_only_on_cycles_minting_canister )) .add_test(systest!(mint_cycles_not_supported_on_application_subnet)) + .add_test(systest!( + mint_cycles128_supported_only_on_cycles_minting_canister + )) + .add_test(systest!(mint_cycles128_not_supported_on_application_subnet)) .add_test(systest!(no_cycle_balance_limit_on_nns_subnet)) .add_test(systest!(app_canister_attempt_initiating_dkg_fails)) .add_test(systest!(canister_heartbeat_is_called_at_regular_intervals)) diff --git a/rs/tests/execution/general_execution_tests/nns_shielding.rs b/rs/tests/execution/general_execution_tests/nns_shielding.rs index 7aab49d2083..1c1c69422be 100644 --- a/rs/tests/execution/general_execution_tests/nns_shielding.rs +++ b/rs/tests/execution/general_execution_tests/nns_shielding.rs @@ -9,14 +9,14 @@ use ic_agent::{ use ic_base_types::RegistryVersion; use ic_management_canister_types::SetupInitialDKGArgs; use ic_nns_constants::CYCLES_MINTING_CANISTER_ID; -use ic_system_test_driver::driver::test_env::TestEnv; use ic_system_test_driver::driver::test_env_api::{GetFirstHealthyNodeSnapshot, HasPublicApiUrl}; +use ic_system_test_driver::driver::{test_env::TestEnv, test_env_api::IcNodeSnapshot}; use ic_system_test_driver::{util::CYCLES_LIMIT_PER_CANISTER, util::*}; use ic_types::Cycles; use ic_types_test_utils::ids::node_test_id; +use ic_universal_canister::wasm; use lazy_static::lazy_static; -const BALANCE_EPSILON: Cycles = Cycles::new(10_000_000); const CANISTER_FREEZE_BALANCE_RESERVE: Cycles = Cycles::new(5_000_000_000_000); lazy_static! { static ref INITIAL_CYCLES: Cycles = @@ -64,11 +64,7 @@ pub fn mint_cycles_supported_only_on_cycles_minting_canister(env: TestEnv) { .await; let before_balance = get_balance(&nns_canister_id, &nns_agent).await; - assert_balance_equals( - *INITIAL_CYCLES, - Cycles::from(before_balance), - BALANCE_EPSILON, - ); + assert_eq!(INITIAL_CYCLES.get(), before_balance); let res = nns_agent .update(&nns_canister_id, "test") @@ -134,13 +130,86 @@ pub fn mint_cycles_not_supported_on_application_subnet(env: TestEnv) { let after_balance = get_balance(&canister_id, &agent).await; assert!( after_balance < before_balance, - "expected {} < expected {}", + "expected {} < {}", after_balance, before_balance ); }); } +fn setup_ucan_and_try_mint128(node: IcNodeSnapshot) -> (AgentError, u128, u128, String) { + let agent = node.build_default_agent(); + let effective_canister_id = node.get_last_canister_id_in_allocation_ranges(); + block_on(async move { + let canister_id = + UniversalCanister::new_with_cycles(&agent, effective_canister_id, *INITIAL_CYCLES) + .await + .unwrap() + .canister_id(); + // Check that 'canister_id' is not 'CYCLES_MINTING_CANISTER_ID'. + assert_ne!(canister_id, CYCLES_MINTING_CANISTER_ID.into()); + let before_balance = get_balance(&canister_id, &agent).await; + let res = agent + .update(&canister_id, "update") + .with_arg( + wasm() + .mint_cycles128(Cycles::from(10_000_000_000u128)) + .reply_data_append() + .reply() + .build(), + ) + .call_and_wait() + .await + .expect_err("should not succeed"); + let after_balance = get_balance(&canister_id, &agent).await; + (res, before_balance, after_balance, canister_id.to_string()) + }) +} + +pub fn mint_cycles128_supported_only_on_cycles_minting_canister(env: TestEnv) { + let nns_node = env.get_first_healthy_nns_node_snapshot(); + let (res, before_balance, after_balance, canister_id) = setup_ucan_and_try_mint128(nns_node); + assert_eq!( + res, + AgentError::CertifiedReject( + RejectResponse { + reject_code: RejectCode::CanisterError, + reject_message: format!( + "Error from Canister {}: Canister violated contract: ic0.mint_cycles cannot be executed on non Cycles Minting Canister: {} != {}.\nThis is likely an error with the compiler/CDK toolchain being used to build the canister. Please report the error to IC devs on the forum: https://forum.dfinity.org and include which language/CDK was used to create the canister.", + canister_id, canister_id, + CYCLES_MINTING_CANISTER_ID), + error_code: None}) + ); + assert!( + after_balance == before_balance, + "expected {} == {}", + after_balance, + before_balance + ); +} + +pub fn mint_cycles128_not_supported_on_application_subnet(env: TestEnv) { + let app_node = env.get_first_healthy_application_node_snapshot(); + let (res, before_balance, after_balance, canister_id) = setup_ucan_and_try_mint128(app_node); + assert_eq!( + res, + AgentError::CertifiedReject( + RejectResponse { + reject_code: RejectCode::CanisterError, + reject_message: format!( + "Error from Canister {}: Canister violated contract: ic0.mint_cycles cannot be executed on non Cycles Minting Canister: {} != {}.\nThis is likely an error with the compiler/CDK toolchain being used to build the canister. Please report the error to IC devs on the forum: https://forum.dfinity.org and include which language/CDK was used to create the canister.", + canister_id, canister_id, + CYCLES_MINTING_CANISTER_ID), + error_code: None}) + ); + assert!( + after_balance <= before_balance, + "expected {} <= {}", + after_balance, + before_balance + ); +} + pub fn no_cycle_balance_limit_on_nns_subnet(env: TestEnv) { let logger = env.logger(); let nns_node = env.get_first_healthy_nns_node_snapshot(); diff --git a/rs/universal_canister/impl/src/api.rs b/rs/universal_canister/impl/src/api.rs index fc28d59d9ee..e93dcb5a68c 100644 --- a/rs/universal_canister/impl/src/api.rs +++ b/rs/universal_canister/impl/src/api.rs @@ -71,6 +71,7 @@ mod ic0 { pub fn canister_version() -> u64; pub fn mint_cycles(amount: u64) -> u64; + pub fn mint_cycles128(amount_high: u64, amount_low: u64, dst: u32) -> (); pub fn is_controller(src: u32, size: u32) -> u32; pub fn in_replicated_execution() -> u32; @@ -404,6 +405,13 @@ pub fn mint_cycles(amount: u64) -> u64 { unsafe { ic0::mint_cycles(amount) } } +/// Mint cycles (only works on CMC). +pub fn mint_cycles128(amount_high: u64, amount_low: u64) -> Vec { + let mut result_bytes = vec![0u8; CYCLES_SIZE]; + unsafe { ic0::mint_cycles128(amount_high, amount_low, result_bytes.as_mut_ptr() as u32) } + result_bytes +} + pub fn is_controller(data: &[u8]) -> u32 { unsafe { ic0::is_controller(data.as_ptr() as u32, data.len() as u32) } } diff --git a/rs/universal_canister/impl/src/lib.rs b/rs/universal_canister/impl/src/lib.rs index 0ec29c5dffe..39f3f03c2d0 100644 --- a/rs/universal_canister/impl/src/lib.rs +++ b/rs/universal_canister/impl/src/lib.rs @@ -112,5 +112,6 @@ try_from_u8!( CallWithBestEffortResponse = 82, MsgDeadline = 83, MemorySizeIsAtLeast = 84, + MintCycles128 = 85, } ); diff --git a/rs/universal_canister/impl/src/main.rs b/rs/universal_canister/impl/src/main.rs index d6635d6fe70..2341354f56b 100644 --- a/rs/universal_canister/impl/src/main.rs +++ b/rs/universal_canister/impl/src/main.rs @@ -379,6 +379,11 @@ fn eval(ops_bytes: OpsBytes) { let amount = stack.pop_int64(); stack.push_int64(api::mint_cycles(amount)); } + Ops::MintCycles128 => { + let amount_low = stack.pop_int64(); + let amount_high = stack.pop_int64(); + stack.push_blob(api::mint_cycles128(amount_high, amount_low)) + } Ops::OneWayCallNew => { // pop in reverse order! let method = stack.pop_blob(); diff --git a/rs/universal_canister/lib/src/lib.rs b/rs/universal_canister/lib/src/lib.rs index d45544ec6e2..7b7e7a2172f 100644 --- a/rs/universal_canister/lib/src/lib.rs +++ b/rs/universal_canister/lib/src/lib.rs @@ -516,6 +516,14 @@ impl PayloadBuilder { self } + pub fn mint_cycles128(mut self, amount: Cycles) -> Self { + let (amount_high, amount_low) = amount.into_parts(); + self = self.push_int64(amount_high); + self = self.push_int64(amount_low); + self.0.push(Ops::MintCycles128 as u8); + self + } + pub fn cycles_burn128(mut self, amount: Cycles) -> Self { let (amount_high, amount_low) = amount.into_parts(); self = self.push_int64(amount_high);