From d2a74e912bdfd77b537b88d2687a81be853b8da9 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Fri, 14 Jun 2024 08:44:38 +0200 Subject: [PATCH 1/3] feat: new System API call ic0.call_cycles_add128_up_to --- spec/index.md | 104 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 73 insertions(+), 31 deletions(-) diff --git a/spec/index.md b/spec/index.md index 17cb2bfe..3a9d1d64 100644 --- a/spec/index.md +++ b/spec/index.md @@ -1378,6 +1378,8 @@ The following sections describe various System API functions, also referred to a ic0.call_data_append : (src : i32, size : i32) -> (); // U CQ Ry Rt CRy CRt T ic0.call_cycles_add : (amount : i64) -> (); // U Ry Rt T ic0.call_cycles_add128 : (amount_high : i64, amount_low: i64) -> (); // U Ry Rt T + ic0.call_cycles_add128_up_to : (max_amount_high : i64, max_amount_low: i64, dst: i32) + -> (); // U Ry Rt T ic0.call_perform : () -> ( err_code : i32 ); // U CQ Ry Rt CRy CRt T ic0.stable_size : () -> (page_count : i32); // * s @@ -1612,15 +1614,51 @@ There must be at most one call to `ic0.call_on_cleanup` between `ic0.call_new` a - `ic0.call_cycles_add : (amount : i64) -> ()` - This adds cycles onto a call. See [Cycles](#system-api-cycles). + This system call moves cycles from the canister balance onto the call under construction, to be transferred with that call. - This may be called multiple times between `ic0.call_new` and `ic0.call_perform`. + The cycles are deducted from the balance as shown by `ic0.canister_cycle_balance128` immediately, and moved back if the call cannot be performed (e.g. if `ic0.call_perform` signals an error, if the canister invokes `ic0.call_new`, or returns without calling `ic0.call_perform`). + + This system call may be called multiple times between `ic0.call_new` and `ic0.call_perform`. + + This system call traps if there is no call under construction, i.e., if not called between `ic0.call_new` and `ic0.call_perform`. + + This system call traps if trying to transfer more cycles than are in the current balance of the canister. + + This system call traps if the cycle balance of the canister after transferring cycles decreases below the canister's freezing limit. - `ic0.call_cycles_add128 : (amount_high : i64, amount_low : i64) -> ()` - This adds cycles onto a call. See [Cycles](#system-api-cycles). + This system call moves cycles from the canister balance onto the call under construction, to be transferred with that call. - This may be called multiple times between `ic0.call_new` and `ic0.call_perform`. + The amount of cycles it moves is represented by a 128-bit value which can be obtained by combining the `amount_high` and `amount_low` parameters. + + The cycles are deducted from the balance as shown by `ic0.canister_cycles_balance128` immediately, and moved back if the call cannot be performed (e.g. if `ic0.call_perform` signals an error, if the canister invokes `ic0.call_new`, or returns without calling `ic0.call_perform`). + + This system call may be called multiple times between `ic0.call_new` and `ic0.call_perform`. + + This system call traps if there is no call under construction, i.e., if not called between `ic0.call_new` and `ic0.call_perform`. + + This system call traps if trying to transfer more cycles than are in the current balance of the canister. + + This system call traps if the cycle balance of the canister after transferring cycles decreases below the canister's freezing limit. + +- `ic0.call_cycles_add128_up_to : (max_amount_high : i64, max_amount_low : i64, dst: i32) -> ()` + + This system call moves cycles from the canister balance onto the call under construction, to be transferred with that call. + + This moves the maximum possible amount of cycles onto the call, up to these constraints: + + - It moves no more cycles than represented by a 128-bit value which can be obtained by combining the `max_amount_high` and `max_amount_low` parameters. + + - The cycle balance of the canister after transferring cycles does not decrease below the canister's freezing limit. + + The cycles are deducted from the balance as shown by `ic0.canister_cycles_balance128` immediately, and moved back if the call cannot be performed (e.g. if `ic0.call_perform` signals an error, if the canister invokes `ic0.call_new`, or returns without calling `ic0.call_perform`). + + This may be called multiple times between `ic0.call_new` and `ic0.call_perform`, each time possibly moving more cycles onto the call. + + This system call traps if there is no call under construction, i.e., if not called between `ic0.call_new` 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. - `ic0.call_perform : () -> ( err_code : i32 )` @@ -1646,7 +1684,7 @@ This specification currently does not go into details about which actions cost h - `ic0.canister_cycle_balance : () → i64` - Indicates the current cycle balance of the canister. It is the canister balance before the execution of the current message, minus a reserve to pay for the execution of the current message, minus any cycles queued up to be sent via `ic0.call_cycles_add`. After execution of the message, the IC may add unused cycles from the reserve back to the balance. + Indicates the current cycle balance of the canister. It is the canister balance before the execution of the current message, minus a reserve to pay for the execution of the current message, minus any cycles queued up to be sent via `ic0.call_cycles_add`, `ic0.call_cycles_add128`, and `ic0.call_cycles_add128_up_to`. After execution of the message, the IC may add unused cycles from the reserve back to the balance. :::note @@ -1656,7 +1694,7 @@ This call traps if the current balance does not fit into a 64-bit value. Caniste - `ic0.canister_cycle_balance128 : (dst : i32) → ()` - Indicates the current cycle balance of the canister by copying the value at the location `dst` in the canister memory. It is the canister balance before the execution of the current message, minus a reserve to pay for the execution of the current message, minus any cycles queued up to be sent via `ic0.call_cycles_add128`. After execution of the message, the IC may add unused cycles from the reserve back to the balance. + Indicates the current cycle balance of the canister by copying the value at the location `dst` in the canister memory. It is the canister balance before the execution of the current message, minus a reserve to pay for the execution of the current message, minus any cycles queued up to be sent via `ic0.call_cycles_add`, `ic0.call_cycles_add128`, and `ic0.call_cycles_add128_up_to`. After execution of the message, the IC may add unused cycles from the reserve back to the balance. - `ic0.msg_cycles_available : () → i64` @@ -1724,28 +1762,6 @@ Example: To accept all cycles provided in a call, invoke `ic0.msg_cycles_accept( This system call does not trap. -- `ic0.call_cycles_add : (amount : i64) → ()` - - This function moves cycles from the canister balance onto the call under construction, to be transferred with that call. - - The cycles are deducted from the balance as shown by `ic0.canister_cycle_balance` immediately, 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 calling `ic0.call_perform`). - - This system call traps if trying to transfer more cycles than are in the current balance of the canister. - - This system call traps if the cycle balance of the canister after transferring cycles decreases below the canister's freezing limit. - -- `ic0.call_cycles_add128 : (amount_high : i64, amount_low : i64) → ()` - - This function moves cycles from the canister balance onto the call under construction, to be transferred with that call. - - The amount of cycles it moves is represented by a 128-bit value which can be obtained by combining the `amount_high` and `amount_low` parameters. - - The cycles are deducted from the balance as shown by `ic0.canister_cycles_balance128` immediately, 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 calling `ic0.call_perform`). - - This traps if trying to transfer more cycles than are in the current balance of the canister. - - This system call traps if the cycle balance of the canister after transferring cycles decreases below the canister's freezing limit. - - `ic0.msg_cycles_refunded : () → i64` This function can only be used in a callback handler (reply or reject), and indicates the amount of cycles that came back with the response as a refund. The refund has already been added to the canister balance automatically. @@ -2332,7 +2348,7 @@ As a provisional method on development instances, the `provisional_create_canist The optional `sender_canister_version` parameter can contain the caller's canister version. If provided, its value must be equal to `ic0.canister_version`. -Cycles added to this call via `ic0.call_cycles_add128` are returned to the caller. +Cycles added to this call via `ic0.call_cycles_add`, `ic0.call_cycles_add128`, and `ic0.call_cycles_add128_up_to` are returned to the caller. This method is only available in local development instances. @@ -2340,7 +2356,7 @@ This method is only available in local development instances. As a provisional method on development instances, the `provisional_top_up_canister` method is provided. It adds `amount` cycles to the balance of canister identified by `amount`. -Cycles added to this call via `ic0.call_cycles_add` and `ic0.call_cycles_add128` are returned to the caller. +Cycles added to this call via `ic0.call_cycles_add`, `ic0.call_cycles_add128`, and `ic0.call_cycles_add128_up_to` are returned to the caller. Any user can top-up any canister this way. @@ -6252,6 +6268,7 @@ Finally we can specify the abstract `CanisterModule` that models a concrete WebA context = U } try func() with Trap then Trap {cycles_used = es.cycles_used;} + discard_pending_call() Return { new_state = es.wasm_state; new_calls = es.calls; @@ -6289,6 +6306,7 @@ Finally we can specify the abstract `CanisterModule` that models a concrete WebA context = CQ } try func() with Trap then Trap {cycles_used = es.cycles_used;} + discard_pending_call() Return { new_state = es.wasm_state; new_calls = es.calls; @@ -6306,6 +6324,7 @@ Finally we can specify the abstract `CanisterModule` that models a concrete WebA context = T } try func() with Trap then Trap {cycles_used = es.cycles_used;} + discard_pending_call() Return { new_state = es.wasm_state; new_calls = es.calls; @@ -6332,6 +6351,7 @@ heartbeat = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} context = T } try func() with Trap then Trap {cycles_used = es.cycles_used;} + discard_pending_call() Return { new_state = es.wasm_state; new_calls = es.calls; @@ -6375,6 +6395,7 @@ global_timer = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} if typeof(func) ≠ func (i32) -> () then Trap func(env) + discard_pending_call() Return { new_state = es.wasm_state; new_calls = es.calls; @@ -6432,6 +6453,7 @@ global_timer = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} if typeof(func) ≠ func (i32) -> () then Trap func(env) + discard_pending_call() Return { new_state = es.wasm_state; new_calls = es.calls; @@ -6721,8 +6743,8 @@ The pseudo-code below does *not* explicitly enforce the restrictions of which im ic0.call_cycles_add128(amount_high : i64, amount_low : i64) = if es.context ∉ {U, Ry, Rt, T} then Trap {cycles_used = es.cycles_used;} - let amount = amount_high * 2^64 + amount_low if es.pending_call = NoPendingCall then Trap {cycles_used = es.cycles_used;} + let amount = amount_high * 2^64 + amount_low if liquid_balance( es.balance, es.params.sysenv.reserved_balance, @@ -6738,6 +6760,26 @@ The pseudo-code below does *not* explicitly enforce the restrictions of which im es.balance := es.balance - amount es.pending_call.transferred_cycles := es.pending_call.transferred_cycles + amount + ic0.call_cycles_add128_up_to(max_amount_high : i64, max_amount_low : i64, dst: i32) = + if es.context ∉ {U, Ry, Rt, T} then Trap {cycles_used = es.cycles_used;} + if es.pending_call = NoPendingCall then Trap {cycles_used = es.cycles_used;} + let max_amount = max_amount_high * 2^64 + max_amount_low + let amount = min(max_amount, liquid_balance( + es.balance, + es.params.sysenv.reserved_balance, + freezing_limit( + es.params.sysenv.compute_allocation, + es.params.sysenv.memory_allocation, + es.params.sysenv.freezing_threshold, + memory_usage_wasm_state(es.wasm_state) + es.params.sysenv.memory_usage_raw_module + es.params.sysenv.memory_usage_canister_history, + es.params.sysenv.subnet_size, + ) + )) + + es.balance := es.balance - amount + es.pending_call.transferred_cycles := es.pending_call.transferred_cycles + amount + copy_cycles_to_canister(dst, amount.to_little_endian_bytes()) + ic0.call_peform() : ( err_code : i32 ) = if es.context ∉ {U, CQ, Ry, Rt, CRy, CRt, T} then Trap {cycles_used = es.cycles_used;} if es.pending_call = NoPendingCall then Trap {cycles_used = es.cycles_used;} From f8be36cb20d8f385b28f948c6ed3c0cf6d48acae Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Thu, 4 Jul 2024 14:26:41 +0200 Subject: [PATCH 2/3] remark --- spec/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/index.md b/spec/index.md index ee40bbb7..c8ade342 100644 --- a/spec/index.md +++ b/spec/index.md @@ -1717,6 +1717,7 @@ There must be at most one call to `ic0.call_on_cleanup` between `ic0.call_new` a This system call traps if there is no call under construction, i.e., if not called between `ic0.call_new` 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. + This amount might be lower than the 128-bit value obtained by combining the `max_amount_high` and `max_amount_low` parameters. - `ic0.call_perform : () -> ( err_code : i32 )` From d692008cc315bd8576e2c4522d8afabf9a55af89 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Mon, 22 Jul 2024 10:51:22 +0200 Subject: [PATCH 3/3] stronger guarantee on subsequent ic0.call_perform --- spec/index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/index.md b/spec/index.md index 61f23c2d..547510c3 100644 --- a/spec/index.md +++ b/spec/index.md @@ -1662,7 +1662,8 @@ There must be at most one call to `ic0.call_on_cleanup` between `ic0.call_new` a - It moves no more cycles than represented by a 128-bit value which can be obtained by combining the `max_amount_high` and `max_amount_low` parameters. - - The cycle balance of the canister after transferring cycles does not decrease below the canister's freezing limit. + - A subsequent `ic0.call_perform` does not fail because of insufficient cycles balance + (if no extra bytes were added to the message via `ic0.call_data_append` between `ic0.call_cycles_add128_up_to` and `ic0.call_perform`) The cycles are deducted from the balance as shown by `ic0.canister_cycles_balance128` immediately, and moved back if the call cannot be performed (e.g. if `ic0.call_perform` signals an error, if the canister invokes `ic0.call_new`, or returns without calling `ic0.call_perform`). @@ -6857,7 +6858,7 @@ ic0.call_cycles_add128_up_to(max_amount_high : i64, max_amount_low : i64, ds if es.pending_call = NoPendingCall then Trap {cycles_used = es.cycles_used;} let max_amount = max_amount_high * 2^64 + max_amount_low let amount = min(max_amount, liquid_balance( - es.balance, + es.balance - MAX_CYCLES_PER_RESPONSE, es.params.sysenv.reserved_balance, freezing_limit( es.params.sysenv.compute_allocation,