diff --git a/rs/cycles_account_manager/src/lib.rs b/rs/cycles_account_manager/src/lib.rs index 8a3a290ed77..693d82cfa40 100644 --- a/rs/cycles_account_manager/src/lib.rs +++ b/rs/cycles_account_manager/src/lib.rs @@ -25,7 +25,10 @@ use ic_replicated_state::{ }; use ic_types::{ canister_http::MAX_CANISTER_HTTP_RESPONSE_BYTES, - messages::{Request, Response, SignedIngressContent, MAX_INTER_CANISTER_PAYLOAD_IN_BYTES}, + messages::{ + Request, Response, SignedIngressContent, MAX_INTER_CANISTER_PAYLOAD_IN_BYTES, + MAX_RESPONSE_COUNT_BYTES, + }, CanisterId, ComputeAllocation, Cycles, MemoryAllocation, NumBytes, NumInstructions, SubnetId, }; use prometheus::IntCounter; @@ -374,6 +377,48 @@ impl CyclesAccountManager { ) } + /// Withdraws up to `cycles` worth of cycles from the canister's balance + /// without putting the canister below its freezing threshold even if + /// the call currently under construction is performed. + /// + /// NOTE: This method is intended for use in inter-canister transfers. + /// It doesn't report these cycles as consumed. To withdraw cycles + /// and have them reported as consumed, use `consume_cycles`. + #[allow(clippy::too_many_arguments)] + pub fn withdraw_up_to_cycles_for_transfer( + &self, + freeze_threshold: NumSeconds, + memory_allocation: MemoryAllocation, + current_payload_size_bytes: NumBytes, + canister_current_memory_usage: NumBytes, + canister_current_message_memory_usage: NumBytes, + canister_compute_allocation: ComputeAllocation, + cycles_balance: &mut Cycles, + cycles: Cycles, + subnet_size: usize, + reserved_balance: Cycles, + ) -> Cycles { + let call_perform_cost = self.xnet_call_performed_fee(subnet_size) + + self.xnet_call_bytes_transmitted_fee(current_payload_size_bytes, subnet_size) + + self.prepayment_for_response_transmission(subnet_size) + + self.prepayment_for_response_execution(subnet_size); + let memory_used_to_enqueue_message = + current_payload_size_bytes.max((MAX_RESPONSE_COUNT_BYTES as u64).into()); + let freeze_threshold = self.freeze_threshold_cycles( + freeze_threshold, + memory_allocation, + canister_current_memory_usage, + canister_current_message_memory_usage + memory_used_to_enqueue_message, + canister_compute_allocation, + subnet_size, + reserved_balance, + ); + let available_for_withdrawal = *cycles_balance - freeze_threshold - call_perform_cost; + let withdrawn_cycles = available_for_withdrawal.min(cycles); + *cycles_balance -= withdrawn_cycles; + withdrawn_cycles + } + /// Charges the canister for ingress induction cost. /// /// Note that this method reports the cycles withdrawn as consumed (i.e. diff --git a/rs/cycles_account_manager/tests/cycles_account_manager.rs b/rs/cycles_account_manager/tests/cycles_account_manager.rs index 7908b7a489d..55e64c6e341 100644 --- a/rs/cycles_account_manager/tests/cycles_account_manager.rs +++ b/rs/cycles_account_manager/tests/cycles_account_manager.rs @@ -17,7 +17,7 @@ use ic_test_utilities_types::{ messages::SignedIngressBuilder, }; use ic_types::{ - messages::{extract_effective_canister_id, SignedIngressContent}, + messages::{extract_effective_canister_id, SignedIngressContent, MAX_RESPONSE_COUNT_BYTES}, nominal_cycles::NominalCycles, CanisterId, ComputeAllocation, Cycles, MemoryAllocation, NumBytes, NumInstructions, }; @@ -880,6 +880,75 @@ fn withdraw_cycles_for_transfer_checks_reserved_balance() { assert_eq!(Cycles::zero(), new_balance); } +#[test] +fn withdraw_up_to_respects_freezing_threshold() { + let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); + let initial_cycles = Cycles::new(200_000_000_000); + let mut system_state = SystemState::new_running_for_testing( + canister_test_id(1), + canister_test_id(2).get(), + initial_cycles, + NumSeconds::from(1_000), + ); + let memory_usage = NumBytes::from(1_000_000); + let message_memory_usage = NumBytes::from(1_000); + let compute_allocation = ComputeAllocation::default(); + let payload_size = NumBytes::from(0); + system_state.memory_allocation = MemoryAllocation::try_from(NumBytes::from(1 << 20)).unwrap(); + let untouched_cycles = cycles_account_manager.freeze_threshold_cycles( + system_state.freeze_threshold, + system_state.memory_allocation, + memory_usage, + message_memory_usage + (MAX_RESPONSE_COUNT_BYTES as u64).into(), + compute_allocation, + SMALL_APP_SUBNET_MAX_SIZE, + system_state.reserved_balance(), + ) + cycles_account_manager + .xnet_call_performed_fee(SMALL_APP_SUBNET_MAX_SIZE) + + cycles_account_manager + .xnet_call_bytes_transmitted_fee(payload_size, SMALL_APP_SUBNET_MAX_SIZE) + + cycles_account_manager.prepayment_for_response_transmission(SMALL_APP_SUBNET_MAX_SIZE) + + cycles_account_manager.prepayment_for_response_execution(SMALL_APP_SUBNET_MAX_SIZE); + + // full amount can be withdrawn + let mut new_balance = system_state.balance(); + let withdraw_amount_1 = Cycles::new(1_000_000); + let withdrawn_amount_1 = cycles_account_manager.withdraw_up_to_cycles_for_transfer( + system_state.freeze_threshold, + system_state.memory_allocation, + payload_size, + memory_usage, + message_memory_usage, + compute_allocation, + &mut new_balance, + withdraw_amount_1, + SMALL_APP_SUBNET_MAX_SIZE, + system_state.reserved_balance(), + ); + assert_eq!(withdraw_amount_1, withdrawn_amount_1); + assert_eq!(initial_cycles - withdraw_amount_1, new_balance); + + // freezing threshold limits the amount that can be withdrawn + let withdraw_amount_2 = Cycles::new(u128::MAX); + let withdrawn_amount_2 = cycles_account_manager.withdraw_up_to_cycles_for_transfer( + system_state.freeze_threshold, + system_state.memory_allocation, + payload_size, + memory_usage, + message_memory_usage, + compute_allocation, + &mut new_balance, + withdraw_amount_2, + SMALL_APP_SUBNET_MAX_SIZE, + system_state.reserved_balance(), + ); + assert_eq!( + initial_cycles - withdrawn_amount_1 - untouched_cycles, + withdrawn_amount_2 + ); + assert_eq!(untouched_cycles, new_balance); +} + #[test] fn freezing_threshold_uses_reserved_balance() { let cycles_account_manager = CyclesAccountManagerBuilder::new().build(); diff --git a/rs/embedders/src/wasm_utils/validation.rs b/rs/embedders/src/wasm_utils/validation.rs index e74f1b70abe..e51004d1819 100644 --- a/rs/embedders/src/wasm_utils/validation.rs +++ b/rs/embedders/src/wasm_utils/validation.rs @@ -535,6 +535,16 @@ fn get_valid_system_apis_common(I: ValType) -> HashMap, amount_high: u64, amount_low: u64, dst: u32| { + charge_for_cpu(&mut caller, overhead::CALL_CYCLES_ADD128_UP_TO)?; + with_memory_and_system_api(&mut caller, |system, memory| { + system.ic0_call_cycles_add128_up_to( + Cycles::from_parts(amount_high, amount_low), + dst as usize, + memory, + ) + }) + } + }) + .unwrap(); + linker .func_wrap("ic0", "call_perform", { move |mut caller: Caller<'_, StoreData>| { diff --git a/rs/embedders/src/wasmtime_embedder/system_api_complexity.rs b/rs/embedders/src/wasmtime_embedder/system_api_complexity.rs index 4dc8a107589..39cb0a9ed4d 100644 --- a/rs/embedders/src/wasmtime_embedder/system_api_complexity.rs +++ b/rs/embedders/src/wasmtime_embedder/system_api_complexity.rs @@ -21,6 +21,7 @@ pub mod overhead { pub const ACCEPT_MESSAGE: NumInstructions = NumInstructions::new(500); pub const CALL_CYCLES_ADD: NumInstructions = NumInstructions::new(500); pub const CALL_CYCLES_ADD128: NumInstructions = NumInstructions::new(500); + pub const CALL_CYCLES_ADD128_UP_TO: NumInstructions = NumInstructions::new(500); pub const CALL_DATA_APPEND: NumInstructions = NumInstructions::new(500); pub const CALL_NEW: NumInstructions = NumInstructions::new(1_500); pub const CALL_ON_CLEANUP: NumInstructions = NumInstructions::new(500); diff --git a/rs/embedders/tests/validation.rs b/rs/embedders/tests/validation.rs index b971e07b85f..6b8f7a5d31d 100644 --- a/rs/embedders/tests/validation.rs +++ b/rs/embedders/tests/validation.rs @@ -926,6 +926,7 @@ fn can_validate_module_cycles_u128_related_imports() { let wasm = wat2wasm( r#"(module (import "ic0" "call_cycles_add128" (func $ic0_call_cycles_add128 (param i64 i64))) + (import "ic0" "call_cycles_add128_up_to" (func $ic0_call_cycles_add128_up_to (param i64 i64 i32))) (import "ic0" "canister_cycle_balance128" (func $ic0_canister_cycle_balance128 (param i32))) (import "ic0" "msg_cycles_available128" (func $ic0_msg_cycles_available128 (param i32))) (import "ic0" "msg_cycles_refunded128" (func $ic0_msg_cycles_refunded128 (param i32))) diff --git a/rs/execution_environment/benches/system_api/execute_update.rs b/rs/execution_environment/benches/system_api/execute_update.rs index 5c5583406ae..85e101ecffc 100644 --- a/rs/execution_environment/benches/system_api/execute_update.rs +++ b/rs/execution_environment/benches/system_api/execute_update.rs @@ -155,6 +155,15 @@ pub fn execute_update_bench(c: &mut Criterion) { Module::CallNewLoop.from_ic0("call_cycles_add128", Params2(0_i64, 100_i64), Result::No), 2059000006, ), + common::Benchmark( + "call_new+ic0_call_cycles_add128_up_to()".into(), + Module::CallNewLoop.from_ic0( + "call_cycles_add128_up_to", + Params3(0_i64, 100_i64, 0_i32), + Result::No, + ), + 2059000006, + ), common::Benchmark( "call_new+ic0_call_perform()".into(), Module::CallNewLoop.from_ic0("call_perform", NoParams, Result::I32), diff --git a/rs/execution_environment/src/hypervisor/tests.rs b/rs/execution_environment/src/hypervisor/tests.rs index fc73e70cbf2..0d30e2bfca0 100644 --- a/rs/execution_environment/src/hypervisor/tests.rs +++ b/rs/execution_environment/src/hypervisor/tests.rs @@ -2182,6 +2182,196 @@ fn ic0_call_cycles_add_has_no_effect_without_ic0_call_perform() { ); } +#[test] +fn ic0_call_cycles_add128_up_to_deducts_cycles() { + let mut test = ExecutionTestBuilder::new() + .with_instruction_limit(MAX_NUM_INSTRUCTIONS.get()) + .build(); + let requested_cycles = Cycles::new(10_000_000_000); + let wat = format!( + r#" + (module + (import "ic0" "call_new" + (func $ic0_call_new + (param i32 i32) + (param $method_name_src i32) (param $method_name_len i32) + (param $reply_fun i32) (param $reply_env i32) + (param $reject_fun i32) (param $reject_env i32) + ) + ) + (import "ic0" "call_cycles_add128_up_to" (func $ic0_call_cycles_add128_up_to (param i64 i64 i32))) + (import "ic0" "call_perform" (func $ic0_call_perform (result i32))) + (import "ic0" "msg_reply_data_append" (func $msg_reply_data_append (param i32 i32))) + (import "ic0" "msg_reply" (func $msg_reply)) + (func (export "canister_update test") + (call $ic0_call_new + (i32.const 100) (i32.const 10) ;; callee canister id = 777 + (i32.const 0) (i32.const 18) ;; refers to "some_remote_method" on the heap + (i32.const 11) (i32.const 22) ;; fictive on_reply closure + (i32.const 33) (i32.const 44) ;; fictive on_reject closure + ) + (call $ic0_call_cycles_add128_up_to + (i64.const 0) ;; amount of cycles used to be added - high + (i64.const {requested_cycles}) ;; amount of cycles used to be added - low + (i32.const 200) ;; where to write amount of cycles added + ) + (call $ic0_call_perform) + drop + ;; return number of cycles attached + (call $msg_reply_data_append (i32.const 200) (i32.const 16)) + (call $msg_reply) + ) + (memory 1) + (data (i32.const 0) "some_remote_method XYZ") + (data (i32.const 100) "\09\03\00\00\00\00\00\00\ff\01") + )"# + ); + let initial_cycles = Cycles::new(100_000_000_000); + let canister_id = test + .canister_from_cycles_and_wat(initial_cycles, wat) + .unwrap(); + let WasmResult::Reply(reply_bytes) = test.ingress(canister_id, "test", vec![]).unwrap() else { + panic!("bad WasmResult") + }; + // The canister has plenty of cycles available to add the requested 10B cycles to the call. + // Therefore we expect that 10B cycles are transferred + let transferred_cycles: Cycles = + u128::from_le_bytes(reply_bytes.try_into().expect("bad number of reply bytes")).into(); + assert_eq!(requested_cycles, transferred_cycles); + assert_eq!(1, test.xnet_messages().len()); + let mgr = test.cycles_account_manager(); + let messaging_fee = mgr.xnet_call_performed_fee(test.subnet_size()) + + mgr.xnet_call_bytes_transmitted_fee( + test.xnet_messages()[0].payload_size_bytes(), + test.subnet_size(), + ) + + mgr.xnet_call_bytes_transmitted_fee( + MAX_INTER_CANISTER_PAYLOAD_IN_BYTES, + test.subnet_size(), + ) + + mgr.execution_cost(MAX_NUM_INSTRUCTIONS, test.subnet_size()); + assert_eq!( + initial_cycles - messaging_fee - transferred_cycles - test.execution_cost(), + test.canister_state(canister_id).system_state.balance(), + ); +} + +#[test] +fn ic0_call_cycles_add128_up_to_limit_allows_performing_call() { + let mut test = ExecutionTestBuilder::new() + .with_instruction_limit(MAX_NUM_INSTRUCTIONS.get()) + .build(); + let wat = r#" + (module + (import "ic0" "call_new" + (func $ic0_call_new + (param i32 i32) + (param $method_name_src i32) (param $method_name_len i32) + (param $reply_fun i32) (param $reply_env i32) + (param $reject_fun i32) (param $reject_env i32) + ) + ) + (import "ic0" "call_cycles_add128_up_to" (func $ic0_call_cycles_add128_up_to (param i64 i64 i32))) + (import "ic0" "call_perform" (func $ic0_call_perform (result i32))) + (import "ic0" "msg_reply_data_append" (func $msg_reply_data_append (param i32 i32))) + (import "ic0" "msg_reply" (func $msg_reply)) + (func (export "canister_update test") + (call $ic0_call_new + (i32.const 100) (i32.const 10) ;; callee canister id = 777 + (i32.const 0) (i32.const 18) ;; refers to "some_remote_method" on the heap + (i32.const 11) (i32.const 22) ;; fictive on_reply closure + (i32.const 33) (i32.const 44) ;; fictive on_reject closure + ) + (call $ic0_call_cycles_add128_up_to + (i64.const 999000000000) ;; amount of cycles used to be added - high + (i64.const 0) ;; amount of cycles used to be added - low + (i32.const 200) ;; where to write amount of cycles added + ) + (call $ic0_call_perform) + drop + ;; return number of cycles attached + (call $msg_reply_data_append (i32.const 200) (i32.const 16)) + (call $msg_reply) + ) + (memory 1) + (data (i32.const 0) "some_remote_method XYZ") + (data (i32.const 100) "\09\03\00\00\00\00\00\00\ff\01") + )"#; + let initial_cycles = Cycles::new(100_000_000_000); + let canister_id = test + .canister_from_cycles_and_wat(initial_cycles, wat) + .unwrap(); + let WasmResult::Reply(reply_bytes) = test.ingress(canister_id, "test", vec![]).unwrap() else { + panic!("bad WasmResult") + }; + // The canister doesn't have enough cycles to attach the requested amount of cycles to the call. + // We expect to see a bunch of cycles transferred, but the subsequent `call_perform` still must have succeeded. + let transferred_cycles = + u128::from_le_bytes(reply_bytes.try_into().expect("bad number of reply bytes")); + assert_eq!(1, test.xnet_messages().len()); + let mgr = test.cycles_account_manager(); + let messaging_fee = mgr.xnet_call_performed_fee(test.subnet_size()) + + mgr.xnet_call_bytes_transmitted_fee( + test.xnet_messages()[0].payload_size_bytes(), + test.subnet_size(), + ) + + mgr.xnet_call_bytes_transmitted_fee( + MAX_INTER_CANISTER_PAYLOAD_IN_BYTES, + test.subnet_size(), + ) + + mgr.execution_cost(MAX_NUM_INSTRUCTIONS, test.subnet_size()); + assert_eq!( + initial_cycles - messaging_fee - transferred_cycles.into() - test.execution_cost(), + test.canister_state(canister_id).system_state.balance(), + ); +} + +#[test] +fn ic0_call_cycles_add128_up_to_has_no_effect_without_ic0_call_perform() { + let mut test = ExecutionTestBuilder::new().build(); + let wat = r#" + (module + (import "ic0" "call_new" + (func $ic0_call_new + (param i32 i32) + (param $method_name_src i32) (param $method_name_len i32) + (param $reply_fun i32) (param $reply_env i32) + (param $reject_fun i32) (param $reject_env i32) + ) + ) + (import "ic0" "call_cycles_add128_up_to" (func $call_cycles_add128_up_to (param i64 i64 i32))) + (func (export "canister_update test") + (call $ic0_call_new + (i32.const 100) (i32.const 10) ;; callee canister id = 777 + (i32.const 0) (i32.const 18) ;; refers to "some_remote_method" on the heap + (i32.const 11) (i32.const 22) ;; fictive on_reply closure + (i32.const 33) (i32.const 44) ;; fictive on_reject closure + ) + (call $call_cycles_add128_up_to + (i64.const 0) ;; amount of cycles used to be added - high + (i64.const 10000000000) ;; amount of cycles used to be added - low + (i32.const 200) ;; where to write amount of cycles added + ) + ) + (memory 1) + (data (i32.const 0) "some_remote_method XYZ") + (data (i32.const 100) "\09\03\00\00\00\00\00\00\ff\01") + )"#; + + let initial_cycles = Cycles::new(100_000_000_000); + let canister_id = test + .canister_from_cycles_and_wat(initial_cycles, wat) + .unwrap(); + let result = test.ingress(canister_id, "test", vec![]); + assert_empty_reply(result); + assert_eq!(0, test.xnet_messages().len()); + // Cycles deducted by `ic0.call_cycles_add128_up_to` are refunded. + assert_eq!( + initial_cycles - test.execution_cost(), + test.canister_state(canister_id).system_state.balance(), + ); +} + const MINT_CYCLES: &str = r#" (module (import "ic0" "msg_reply_data_append" 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 1c835587fca..d4818231d33 100644 --- a/rs/execution_environment/src/query_handler/query_cache/tests.rs +++ b/rs/execution_environment/src/query_handler/query_cache/tests.rs @@ -1466,6 +1466,7 @@ fn query_cache_future_proof_test() { SystemApiCallId::AcceptMessage | SystemApiCallId::CallCyclesAdd | SystemApiCallId::CallCyclesAdd128 + | SystemApiCallId::CallCyclesAdd128UpTo | SystemApiCallId::CallDataAppend | SystemApiCallId::CallNew | SystemApiCallId::CallOnCleanup diff --git a/rs/execution_environment/src/query_handler/tests.rs b/rs/execution_environment/src/query_handler/tests.rs index 3b9a92f707f..c3e860f9419 100644 --- a/rs/execution_environment/src/query_handler/tests.rs +++ b/rs/execution_environment/src/query_handler/tests.rs @@ -1155,6 +1155,10 @@ fn composite_query_syscalls_from_reply_reject_callback() { wasm().call_cycles_add128(0, 0).build(), "call_cycles_add128", ), + ( + wasm().call_cycles_add128_up_to(0, 0).build(), + "call_cycles_add128_up_to", + ), ( wasm().msg_cycles_available128().build(), "cycles_available128", diff --git a/rs/interfaces/src/execution_environment.rs b/rs/interfaces/src/execution_environment.rs index 342a3acf2ec..13c38ae5180 100644 --- a/rs/interfaces/src/execution_environment.rs +++ b/rs/interfaces/src/execution_environment.rs @@ -131,6 +131,8 @@ pub enum SystemApiCallId { CallCyclesAdd, /// Tracker for `ic0.call_cycles_add128()` CallCyclesAdd128, + /// Tracker for `ic0.call_cycles_add128_up_to()` + CallCyclesAdd128UpTo, /// Tracker for `ic0.call_data_append()` CallDataAppend, /// Tracker for `ic0.call_new()` @@ -801,6 +803,28 @@ pub trait SystemApi { /// balance of the canister. fn ic0_call_cycles_add128(&mut self, amount: Cycles) -> HypervisorResult<()>; + /// Adds cycles to a call by moving them from the canister's balance onto + /// the call under construction. The cycles are deducted immediately + /// from the canister's balance and moved back if the call cannot be + /// performed (e.g. if `ic0.call_perform` signals an error or if the + /// canister invokes `ic0.call_new` or returns without invoking + /// `ic0.call_perform`). + /// + /// The number of cycles added to the call will be `<= amount` and such that a + /// subsequent `ic0.call_perform` will not fail because of insufficient cycles + /// balance (assuming no `ic0.call_data_append` is called between + /// `ic0.call_cycles_add128_up_to` and `ic0.call_perform`). + /// + /// This system call also copies the actual amount of cycles that were moved + /// onto the call represented by a 128-bit value starting at the location + /// `dst` in the canister memory. + fn ic0_call_cycles_add128_up_to( + &mut self, + amount: Cycles, + dst: usize, + heap: &mut [u8], + ) -> HypervisorResult<()>; + /// This call concludes assembling the call. It queues the call message to /// the given destination, but does not actually act on it until the current /// WebAssembly function returns without trapping. diff --git a/rs/system_api/src/lib.rs b/rs/system_api/src/lib.rs index d5d958fcffc..ddcd575679c 100644 --- a/rs/system_api/src/lib.rs +++ b/rs/system_api/src/lib.rs @@ -32,7 +32,9 @@ use ic_types::{ }; use ic_utils::deterministic_operations::deterministic_copy_from_slice; use request_in_prep::{into_request, RequestInPrep}; -use sandbox_safe_system_state::{CanisterStatusView, SandboxSafeSystemState, SystemStateChanges}; +use sandbox_safe_system_state::{ + CanisterStatusView, CyclesAmountType, SandboxSafeSystemState, SystemStateChanges, +}; use serde::{Deserialize, Serialize}; use stable_memory::StableMemory; use std::{ @@ -1118,6 +1120,12 @@ impl SystemApiImpl { self.memory_usage.current_usage } + /// Note that this function is made public only for the tests + #[doc(hidden)] + pub fn get_compute_allocation(&self) -> ComputeAllocation { + self.execution_parameters.compute_allocation + } + /// Bytes allocated in the Wasm/stable memory. pub fn get_allocated_bytes(&self) -> NumBytes { self.memory_usage.allocated_execution_memory @@ -1231,8 +1239,8 @@ impl SystemApiImpl { fn ic0_call_cycles_add_helper( &mut self, method_name: &str, - amount: Cycles, - ) -> HypervisorResult<()> { + amount: CyclesAmountType, + ) -> HypervisorResult { match &mut self.api_type { ApiType::Start { .. } | ApiType::Init { .. } @@ -1267,15 +1275,17 @@ impl SystemApiImpl { ), }), Some(request) => { - self.sandbox_safe_system_state + let amount_withdrawn = self + .sandbox_safe_system_state .withdraw_cycles_for_transfer( + request.current_payload_size(), self.memory_usage.current_usage, self.memory_usage.current_message_usage, amount, false, // synchronous error => no need to reveal top up balance )?; - request.add_cycles(amount); - Ok(()) + request.add_cycles(amount_withdrawn); + Ok(amount_withdrawn) } } } @@ -2224,17 +2234,40 @@ impl SystemApi for SystemApiImpl { } fn ic0_call_cycles_add(&mut self, amount: u64) -> HypervisorResult<()> { - let result = self.ic0_call_cycles_add_helper("ic0_call_cycles_add", Cycles::from(amount)); + let result = self + .ic0_call_cycles_add_helper( + "ic0_call_cycles_add", + CyclesAmountType::Exact(Cycles::from(amount)), + ) + .map(|_| ()); trace_syscall!(self, CallCyclesAdd, result, amount); result } fn ic0_call_cycles_add128(&mut self, amount: Cycles) -> HypervisorResult<()> { - let result = self.ic0_call_cycles_add_helper("ic0_call_cycles_add128", amount); + let result = self + .ic0_call_cycles_add_helper("ic0_call_cycles_add128", CyclesAmountType::Exact(amount)) + .map(|_| ()); trace_syscall!(self, CallCyclesAdd128, result, amount); result } + fn ic0_call_cycles_add128_up_to( + &mut self, + amount: Cycles, + dst: usize, + heap: &mut [u8], + ) -> HypervisorResult<()> { + let result = self.ic0_call_cycles_add_helper( + "ic0_call_cycles_add128_up_to", + CyclesAmountType::UpTo(amount), + ); + trace_syscall!(self, CallCyclesAdd128UpTo, result, amount); + let withdrawn_cycles = result?; + copy_cycles_to_heap(withdrawn_cycles, dst, heap, "ic0_call_cycles_add128_up_to")?; + Ok(()) + } + // Note that if this function returns an error, then the canister will be // trapped and the state will be rolled back. Hence, we do not have to worry // about rolling back any modifications that previous calls like diff --git a/rs/system_api/src/request_in_prep.rs b/rs/system_api/src/request_in_prep.rs index 13efa7a6bf6..4f929bcd5ec 100644 --- a/rs/system_api/src/request_in_prep.rs +++ b/rs/system_api/src/request_in_prep.rs @@ -181,6 +181,10 @@ impl RequestInPrep { pub(crate) fn add_cycles(&mut self, cycles: Cycles) { self.cycles += cycles; } + + pub(crate) fn current_payload_size(&self) -> NumBytes { + ((self.method_payload.len() + self.method_name.len()) as u64).into() + } } pub(crate) struct RequestWithPrepayment { diff --git a/rs/system_api/src/sandbox_safe_system_state.rs b/rs/system_api/src/sandbox_safe_system_state.rs index 4e6436382b5..eba3c5eac48 100644 --- a/rs/system_api/src/sandbox_safe_system_state.rs +++ b/rs/system_api/src/sandbox_safe_system_state.rs @@ -543,6 +543,16 @@ impl SystemStateChanges { } } +/// Determines if a precise amount of cycles is requested +/// or if the provided number is only a limit. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CyclesAmountType { + /// Use exactly this many cycles or fail. + Exact(Cycles), + /// Use as many cycles as possible, up to this limit. + UpTo(Cycles), +} + /// A version of the `SystemState` that can be used in a sandboxed process. /// Changes are separately tracked so that we can verify the changes are valid /// before applying them to the actual system state. @@ -931,28 +941,46 @@ impl SandboxSafeSystemState { pub(super) fn withdraw_cycles_for_transfer( &mut self, + current_payload_size_bytes: NumBytes, canister_current_memory_usage: NumBytes, canister_current_message_memory_usage: NumBytes, - amount: Cycles, + amount: CyclesAmountType, reveal_top_up: bool, - ) -> HypervisorResult<()> { + ) -> HypervisorResult { let mut new_balance = self.cycles_balance(); - let result = self - .cycles_account_manager - .withdraw_cycles_for_transfer( - self.canister_id, - self.freeze_threshold, - self.memory_allocation, - canister_current_memory_usage, - canister_current_message_memory_usage, - self.compute_allocation, - &mut new_balance, - amount, - self.subnet_size, - self.reserved_balance(), - reveal_top_up, - ) - .map_err(HypervisorError::InsufficientCyclesBalance); + let result = match amount { + CyclesAmountType::Exact(amount) => self + .cycles_account_manager + .withdraw_cycles_for_transfer( + self.canister_id, + self.freeze_threshold, + self.memory_allocation, + canister_current_memory_usage, + canister_current_message_memory_usage, + self.compute_allocation, + &mut new_balance, + amount, + self.subnet_size, + self.reserved_balance(), + reveal_top_up, + ) + .map(|()| amount) + .map_err(HypervisorError::InsufficientCyclesBalance), + CyclesAmountType::UpTo(amount) => Ok(self + .cycles_account_manager + .withdraw_up_to_cycles_for_transfer( + self.freeze_threshold, + self.memory_allocation, + current_payload_size_bytes, + canister_current_memory_usage, + canister_current_message_memory_usage, + self.compute_allocation, + &mut new_balance, + amount, + self.subnet_size, + self.reserved_balance(), + )), + }; self.update_balance_change(new_balance); result } diff --git a/rs/system_api/tests/system_api.rs b/rs/system_api/tests/system_api.rs index 9776668d035..c600714bde7 100644 --- a/rs/system_api/tests/system_api.rs +++ b/rs/system_api/tests/system_api.rs @@ -27,8 +27,7 @@ use ic_test_utilities_types::{ use ic_types::{ messages::{CallbackId, RejectContext, RequestMetadata, MAX_RESPONSE_COUNT_BYTES, NO_DEADLINE}, methods::{Callback, WasmClosure}, - time, - time::UNIX_EPOCH, + time::{self, UNIX_EPOCH}, CanisterTimer, CountBytes, Cycles, NumInstructions, PrincipalId, Time, MAX_ALLOWED_CANISTER_LOG_BUFFER_SIZE, }; @@ -269,6 +268,7 @@ fn is_supported(api_type: SystemApiCallId, context: &str) -> bool { SystemApiCallId::CallDataAppend => vec!["U", "CQ", "Ry", "Rt", "CRy", "CRt", "T"], SystemApiCallId::CallCyclesAdd => vec!["U", "Ry", "Rt", "T"], SystemApiCallId::CallCyclesAdd128 => vec!["U", "Ry", "Rt", "T"], + SystemApiCallId::CallCyclesAdd128UpTo => vec!["U", "Ry", "Rt", "T"], SystemApiCallId::CallPerform => vec!["U", "CQ", "Ry", "Rt", "CRy", "CRt", "T"], SystemApiCallId::CallWithBestEffortResponse => vec!["U", "CQ", "Ry", "Rt", "CRy", "CRt", "T"], SystemApiCallId::StableSize => vec!["*", "s"], @@ -554,6 +554,19 @@ fn api_availability_test( context, ); } + SystemApiCallId::CallCyclesAdd128UpTo => { + assert_api_availability( + |mut api| { + let _ = api.ic0_call_new(0, 0, 0, 0, 0, 0, 0, 0, &[42; 128]); + api.ic0_call_cycles_add128_up_to(Cycles::new(0), 0, &mut [42; 128]) + }, + api_type, + &system_state, + cycles_account_manager, + api_type_enum, + context, + ); + } SystemApiCallId::CallPerform => { assert_api_availability( |mut api| { @@ -975,6 +988,105 @@ fn test_fail_adding_more_cycles_when_not_enough_balance() { ); } +fn call_cycles_add128_up_to_helper( + api: &mut SystemApiImpl, + amount: Cycles, +) -> Result { + let size = 16; + let mut buf = vec![0u8; size]; + api.ic0_call_cycles_add128_up_to(amount, 0, &mut buf)?; + let attached_cycles = u128::from_le_bytes(buf.try_into().unwrap()); + Ok(Cycles::from(attached_cycles)) +} + +#[test] +fn test_call_cycles_add_up_to() { + let cycles_amount = Cycles::from(1_000_000_000_000u128); + let max_num_instructions = NumInstructions::from(1 << 30); + let cycles_account_manager = CyclesAccountManagerBuilder::new() + .with_max_num_instructions(max_num_instructions) + .build(); + let system_state = get_system_state_with_cycles(cycles_amount); + let mut api = get_system_api( + ApiTypeBuilder::build_update_api(), + &system_state, + cycles_account_manager, + ); + + // Check ic0_canister_cycle_balance after first ic0_call_new. + assert_eq!(api.ic0_call_new(0, 0, 0, 0, 0, 0, 0, 0, &[]), Ok(())); + // Check cycles balance. + assert_eq!( + Cycles::from(api.ic0_canister_cycle_balance().unwrap()), + cycles_amount + ); + + // Add an available amount of cycles to call. + let amount1 = Cycles::new(49); + assert_eq!( + call_cycles_add128_up_to_helper(&mut api, amount1), + Ok(amount1) + ); + // Check cycles balance + assert_eq!( + Cycles::from(api.ic0_canister_cycle_balance().unwrap()), + cycles_amount - amount1 + ); + + // Adding more cycles than available to call means the rest of the available balance gets added + let untouched_cycles = cycles_account_manager.freeze_threshold_cycles( + system_state.freeze_threshold, + system_state.memory_allocation, + api.get_current_memory_usage(), + api.get_allocated_message_bytes() + (MAX_RESPONSE_COUNT_BYTES as u64).into(), + api.get_compute_allocation(), + SMALL_APP_SUBNET_MAX_SIZE, + system_state.reserved_balance(), + ) + cycles_account_manager + .xnet_call_performed_fee(SMALL_APP_SUBNET_MAX_SIZE) + + cycles_account_manager + .xnet_call_bytes_transmitted_fee(0.into(), SMALL_APP_SUBNET_MAX_SIZE) + + cycles_account_manager.prepayment_for_response_transmission(SMALL_APP_SUBNET_MAX_SIZE) + + cycles_account_manager.prepayment_for_response_execution(SMALL_APP_SUBNET_MAX_SIZE); + assert_eq!( + call_cycles_add128_up_to_helper(&mut api, Cycles::new(u128::MAX)), + Ok(cycles_amount - amount1 - untouched_cycles) + ); + // Check cycles balance + assert_eq!( + Cycles::from(api.ic0_canister_cycle_balance().unwrap()), + untouched_cycles + ); + + assert_eq!(api.ic0_call_new(0, 0, 0, 0, 0, 0, 0, 0, &[]), Ok(())); + // Check cycles balance. + assert_eq!( + Cycles::from(api.ic0_canister_cycle_balance().unwrap()), + cycles_amount + ); + + // With some allocated memory the freezing threshold is no longer at 0. + api.try_grow_wasm_memory(0, 1).unwrap(); + let untouched_cycles = cycles_account_manager.freeze_threshold_cycles( + system_state.freeze_threshold, + system_state.memory_allocation, + api.get_current_memory_usage(), + api.get_allocated_message_bytes() + (MAX_RESPONSE_COUNT_BYTES as u64).into(), + api.get_compute_allocation(), + SMALL_APP_SUBNET_MAX_SIZE, + system_state.reserved_balance(), + ) + cycles_account_manager + .xnet_call_performed_fee(SMALL_APP_SUBNET_MAX_SIZE) + + cycles_account_manager + .xnet_call_bytes_transmitted_fee(0.into(), SMALL_APP_SUBNET_MAX_SIZE) + + cycles_account_manager.prepayment_for_response_transmission(SMALL_APP_SUBNET_MAX_SIZE) + + cycles_account_manager.prepayment_for_response_execution(SMALL_APP_SUBNET_MAX_SIZE); + assert_eq!( + cycles_amount - untouched_cycles, + call_cycles_add128_up_to_helper(&mut api, Cycles::new(u128::MAX)).unwrap() + ); +} + #[test] fn test_canister_balance() { let cycles_amount = 100; diff --git a/rs/universal_canister/impl/src/api.rs b/rs/universal_canister/impl/src/api.rs index fc28d59d9ee..dff08d2ee5f 100644 --- a/rs/universal_canister/impl/src/api.rs +++ b/rs/universal_canister/impl/src/api.rs @@ -51,6 +51,7 @@ mod ic0 { pub fn msg_deadline() -> u64; pub fn call_cycles_add(amount: u64) -> (); pub fn call_cycles_add128(amount_high: u64, amount_low: u64) -> (); + pub fn call_cycles_add128_up_to(amount_high: u64, amount_low: u64, dst: u32) -> (); pub fn call_perform() -> u32; pub fn stable_size() -> u32; pub fn stable_grow(additional_pages: u32) -> u32; @@ -144,6 +145,12 @@ pub fn call_cycles_add128(amount_high: u64, amount_low: u64) { } } +pub fn call_cycles_add128_up_to(amount_high: u64, amount_low: u64) -> Vec { + let mut bytes = vec![0u8; CYCLES_SIZE]; + unsafe { ic0::call_cycles_add128_up_to(amount_high, amount_low, bytes.as_mut_ptr() as u32) } + bytes +} + pub fn call_perform() -> u32 { unsafe { ic0::call_perform() } } diff --git a/rs/universal_canister/impl/src/lib.rs b/rs/universal_canister/impl/src/lib.rs index 7da9bdfe43d..151e28eadb4 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, + CallCyclesAdd128UpTo = 85, } ); diff --git a/rs/universal_canister/impl/src/main.rs b/rs/universal_canister/impl/src/main.rs index 7cb318f65ce..7e3688477f3 100644 --- a/rs/universal_canister/impl/src/main.rs +++ b/rs/universal_canister/impl/src/main.rs @@ -436,6 +436,11 @@ fn eval(ops_bytes: OpsBytes) { } std::hint::black_box(a); } + Ops::CallCyclesAdd128UpTo => { + let amount_low = stack.pop_int64(); + let amount_high = stack.pop_int64(); + stack.push_blob(api::call_cycles_add128_up_to(amount_high, amount_low)) + } } } } diff --git a/rs/universal_canister/lib/src/lib.rs b/rs/universal_canister/lib/src/lib.rs index 5133de66693..ac157acaa91 100644 --- a/rs/universal_canister/lib/src/lib.rs +++ b/rs/universal_canister/lib/src/lib.rs @@ -17,7 +17,7 @@ use universal_canister::Ops; /// `rs/universal_canister`. pub const UNIVERSAL_CANISTER_WASM: &[u8] = include_bytes!("universal-canister.wasm.gz"); pub const UNIVERSAL_CANISTER_WASM_SHA256: [u8; 32] = - hex!("9c0b4ed1d729ffdd0e8d194df3be621f58a2fb86e1f1bbf556d89f43637de303"); + hex!("65b035adf05770b69d68be0a5bec39df4d2ac5fc1dccb8397766120270159e6b"); /// A succinct shortcut for creating a `PayloadBuilder`, which is used to encode /// instructions to be executed by the UC. @@ -365,6 +365,13 @@ impl PayloadBuilder { self } + pub fn call_cycles_add128_up_to(mut self, high_amount: u64, low_amount: u64) -> Self { + self = self.push_int64(high_amount); + self = self.push_int64(low_amount); + self.0.push(Ops::CallCyclesAdd128UpTo as u8); + self + } + pub fn call_cycles_add(mut self, amount: u64) -> Self { self = self.push_int64(amount); self.0.push(Ops::CallCyclesAdd as u8); diff --git a/rs/universal_canister/lib/src/universal-canister.wasm.gz b/rs/universal_canister/lib/src/universal-canister.wasm.gz index 8ea8ce25055..39f32dce87c 100755 Binary files a/rs/universal_canister/lib/src/universal-canister.wasm.gz and b/rs/universal_canister/lib/src/universal-canister.wasm.gz differ