Skip to content

Commit

Permalink
[FINAL] feat: Allow accepting and burning cycles in replicated queries (
Browse files Browse the repository at this point in the history
#3760)

* [FINAL] feat: Allow accepting and burning cycles in replicated queries

* changelog

---------

Co-authored-by: Björn Tackmann <[email protected]>
  • Loading branch information
mraszyk and Dfinity-Bjoern authored Jan 23, 2025
1 parent 8eb9424 commit 14cd3df
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 45 deletions.
3 changes: 3 additions & 0 deletions docs/references/_attachments/interface-spec-changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Changelog {#changelog}

### 0.32.0 (2025-01-23) {#0_32_0}
* Allow accepting and burning cycles in replicated queries.

### 0.31.0 (2025-01-09) {#0_31_0}
* Add support for Schnorr auxiliary inputs

Expand Down
94 changes: 49 additions & 45 deletions docs/references/ic-interface-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1441,13 +1441,13 @@ defaulting to `I = i32` if the canister declares no memory.
ic0.msg_reply : () -> (); // U RQ NRQ CQ Ry Rt CRy CRt
ic0.msg_reject : (src : I, size : I) -> (); // U RQ NRQ CQ Ry Rt CRy CRt
ic0.msg_cycles_available128 : (dst : I) -> (); // U Rt Ry
ic0.msg_cycles_available128 : (dst : I) -> (); // U RQ Rt Ry
ic0.msg_cycles_refunded128 : (dst : I) -> (); // Rt Ry
ic0.msg_cycles_accept128 : (max_amount_high : i64, max_amount_low: i64, dst : I)
-> (); // U Rt Ry
-> (); // U RQ Rt Ry
ic0.cycles_burn128 : (amount_high : i64, amount_low : i64, dst : I)
-> (); // I G U Ry Rt C T
-> (); // I G U RQ Ry Rt C T
ic0.canister_self_size : () -> I; // *
ic0.canister_self_copy : (dst : I, offset : I, size : I) -> (); // *
Expand Down Expand Up @@ -1498,9 +1498,9 @@ The following System API functions are only available if `I = i32`, i.e., if the
or if the canister declares no memory.

```
ic0.msg_cycles_available : () -> i64; // U Rt Ry
ic0.msg_cycles_available : () -> i64; // U RQ Rt Ry
ic0.msg_cycles_refunded : () -> i64; // Rt Ry
ic0.msg_cycles_accept : (max_amount : i64) -> (amount : i64); // U Rt Ry
ic0.msg_cycles_accept : (max_amount : i64) -> (amount : i64); // U RQ Rt Ry
ic0.canister_cycle_balance : () -> i64; // *
ic0.call_cycles_add : (amount : i64) -> (); // U Ry Rt T
ic0.stable_size : () -> (page_count : i32); // * s
Expand Down Expand Up @@ -1672,7 +1672,7 @@ This function allows a canister to find out if it is running, stopping or stoppe

### Canister version {#system-api-canister-version}

For each canister, the system maintains a *canister version*. Upon canister creation, it is set to 0, and it is **guaranteed** to be incremented upon every change of the canister's code, settings, running status (Running, Stopping, Stopped), and memory (WASM and stable memory), i.e., upon every successful management canister call of methods `update_settings`, `load_canister_snapshot`, `install_code`, `install_chunked_code`, `uninstall_code`, `start_canister`, and `stop_canister` on that canister, code uninstallation due to that canister running out of cycles, canister's running status transitioning from Stopping to Stopped, and successful execution of update methods, response callbacks, heartbeats, and global timers. The system can arbitrarily increment the canister version also if the canister's code, settings, running status, and memory do not change.
For each canister, the system maintains a *canister version*. Upon canister creation, it is set to 0, and it is **guaranteed** to be incremented upon every change of the canister's code, settings, running status (Running, Stopping, Stopped), cycles balance, and memory (WASM and stable memory), i.e., upon every successful management canister call of methods `update_settings`, `load_canister_snapshot`, `install_code`, `install_chunked_code`, `uninstall_code`, `start_canister`, and `stop_canister` on that canister, code uninstallation due to that canister running out of cycles, canister's running status transitioning from Stopping to Stopped, and successful execution of update methods, replicated query methods, response callbacks, heartbeats, and global timers. The system can arbitrarily increment the canister version also if the canister's code, settings, running status, and memory do not change.

- `ic0.canister_version : () → i64`

Expand Down Expand Up @@ -3212,6 +3212,7 @@ The [WebAssembly System API](#system-api) is relatively low-level, and some of i
}
QueryFunc = WasmState -> Trap { cycles_used : Nat; } | Return {
response : Response;
cycles_accepted : Nat;
cycles_used : Nat;
}
CompositeQueryFunc = WasmState -> Trap { cycles_used : Nat; } | Return {
Expand All @@ -3231,35 +3232,36 @@ The [WebAssembly System API](#system-api) is relatively low-level, and some of i
AvailableCycles = Nat
RefundedCycles = Nat
CanisterModule = {
init : (CanisterId, Arg, CallerId, Env) -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_certified_data : NoCertifiedData | Blob;
new_global_timer : NoGlobalTimer | Nat;
cycles_used : Nat;
}
pre_upgrade : (WasmState, Principal, Env) -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_certified_data : NoCertifiedData | Blob;
cycles_used : Nat;
}
post_upgrade : (WasmState, Arg, CallerId, Env) -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_certified_data : NoCertifiedData | Blob;
new_global_timer : NoGlobalTimer | Nat;
cycles_used : Nat;
}
update_methods : MethodName ↦ ((Arg, CallerId, Env, AvailableCycles) -> UpdateFunc)
query_methods : MethodName ↦ ((Arg, CallerId, Env) -> QueryFunc)
composite_query_methods : MethodName ↦ ((Arg, CallerId, Env) -> CompositeQueryFunc)
heartbeat : (Env) -> SystemTaskFunc
global_timer : (Env) -> SystemTaskFunc
callbacks : (Callback, Response, RefundedCycles, Env, AvailableCycles) -> UpdateFunc
composite_callbacks : (Callback, Response, Env) -> UpdateFunc
inspect_message : (MethodName, WasmState, Arg, CallerId, Env) -> Trap | Return {
status : Accept | Reject;
}
}
CanisterModule = {
init : (CanisterId, Arg, CallerId, Env) -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_certified_data : NoCertifiedData | Blob;
new_global_timer : NoGlobalTimer | Nat;
cycles_used : Nat;
}
pre_upgrade : (WasmState, Principal, Env) -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_certified_data : NoCertifiedData | Blob;
cycles_used : Nat;
}
post_upgrade : (WasmState, Arg, CallerId, Env) -> Trap { cycles_used : Nat; } | Return {
new_state : WasmState;
new_certified_data : NoCertifiedData | Blob;
new_global_timer : NoGlobalTimer | Nat;
cycles_used : Nat;
}
update_methods : MethodName ↦ ((Arg, CallerId, Env, AvailableCycles) -> UpdateFunc)
query_methods : MethodName ↦ ((Arg, CallerId, Env) -> QueryFunc)
composite_query_methods : MethodName ↦ ((Arg, CallerId, Env) -> CompositeQueryFunc)
heartbeat : (Env) -> SystemTaskFunc
global_timer : (Env) -> SystemTaskFunc
callbacks : (Callback, Response, RefundedCycles, Env, AvailableCycles) -> UpdateFunc
composite_callbacks : (Callback, Response, Env) -> UpdateFunc
inspect_message : (MethodName, WasmState, Arg, CallerId, Env) -> Trap | Return {
status : Accept | Reject;
}
}
```
This high-level interface presents a pure, mathematical model of a canister, and hides the bookkeeping required to provide the System API as seen in Section [Canister interface (System API)](#system-api).
Expand Down Expand Up @@ -4082,7 +4084,7 @@ Available = S.call_contexts[M.call_contexts].available_cycles
)
or
( M.entry_point = PublicMethod Name Caller Arg
F = query_as_update(Mod.query_methods[Name], Arg, Caller, Env)
F = query_as_update(Mod.query_methods[Name], Arg, Caller, Env, Available)
New_canister_version = S.canister_version[M.receiver]
Wasm_memory_limit = 0
)
Expand Down Expand Up @@ -4232,16 +4234,16 @@ validate_sender_canister_version(new_calls, canister_version_from_system) =

The functions `query_as_update` and `system_task_as_update` turns a query function (note that composite query methods cannot be called when executing a message during this transition) resp the heartbeat or global timer into an update function; this is merely a notational trick to simplify the rule:
```
query_as_update(f, arg, env) = λ wasm_state →
match f(arg, env)(wasm_state) with
query_as_update(f, arg, caller, env, available) = λ wasm_state →
match f(arg, caller, env, available)(wasm_state) with
Trap trap → Trap trap
Return res → Return {
new_state = wasm_state;
new_calls = [];
new_certified_data = NoCertifiedData;
new_global_timer = NoGlobalTimer;
response = res.response;
cycles_accepted = 0;
cycles_accepted = res.cycles_accepted;
cycles_used = res.cycles_used;
}
Expand Down Expand Up @@ -6863,16 +6865,18 @@ Finally, we can specify the abstract `CanisterModule` that models a concrete Web
- The partial map `query_methods` of the `CanisterModule` is defined for all method names `method` for which the WebAssembly program exports a function `func` named `canister_query <method>`, and has value
```
query_methods[method] = λ (arg, caller, sysenv) → λ wasm_state →
query_methods[method] = λ (arg, caller, sysenv, available) → λ wasm_state →
let es = ref {empty_execution_state with
wasm_state = wasm_state;
params = empty_params with { arg = arg; caller = caller; sysenv }
balance = sysenv.balance
cycles_available = available
context = Q
}
try func<es>() with Trap then Trap {cycles_used = es.cycles_used;}
Return {
response = es.response;
cycles_accepted = es.cycles_accepted;
cycles_used = es.cycles_used;
}
```
Expand Down Expand Up @@ -7180,13 +7184,13 @@ ic0.msg_reject<es>(src : I, size : I) =
es.cycles_available := 0

ic0.msg_cycles_available<es>() : i64 =
if es.context ∉ {U, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
if es.context ∉ {U, RQ, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
if es.cycles_available >= 2^64 then Trap {cycles_used = es.cycles_used;}
return es.cycles_available

I ∈ {i32, i64}
ic0.msg_cycles_available128<es>(dst : I) =
if es.context ∉ {U, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
if es.context ∉ {U, RQ, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
let amount = es.cycles_available
copy_cycles_to_canister<es>(dst, amount.to_little_endian_bytes())

Expand All @@ -7202,7 +7206,7 @@ ic0.msg_cycles_refunded128<es>(dst : I) =
copy_cycles_to_canister<es>(dst, amount.to_little_endian_bytes())

ic0.msg_cycles_accept<es>(max_amount : i64) : i64 =
if es.context ∉ {U, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
if es.context ∉ {U, RQ, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
let amount = min(max_amount, es.cycles_available)
es.cycles_available := es.cycles_available - amount
es.cycles_accepted := es.cycles_accepted + amount
Expand All @@ -7211,7 +7215,7 @@ ic0.msg_cycles_accept<es>(max_amount : i64) : i64 =

I ∈ {i32, i64}
ic0.msg_cycles_accept128<es>(max_amount_high : i64, max_amount_low : i64, dst : I) =
if es.context ∉ {U, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
if es.context ∉ {U, RQ, Rt, Ry} then Trap {cycles_used = es.cycles_used;}
let max_amount = max_amount_high * 2^64 + max_amount_low
let amount = min(max_amount, es.cycles_available)
es.cycles_available := es.cycles_available - amount
Expand All @@ -7221,7 +7225,7 @@ ic0.msg_cycles_accept128<es>(max_amount_high : i64, max_amount_low : i64, dst :

I ∈ {i32, i64}
ic0.cycles_burn128<es>(amount_high : i64, amount_low : i64, dst : I) =
if es.context ∉ {I, G, U, Ry, Rt, C, T} then Trap {cycles_used = es.cycles_used;}
if es.context ∉ {I, G, U, RQ, Ry, Rt, C, T} then Trap {cycles_used = es.cycles_used;}
let amount = amount_high * 2^64 + amount_low
let burned_amount = min(amount, liquid_balance(es))
es.balance := es.balance - burned_amount
Expand Down

0 comments on commit 14cd3df

Please sign in to comment.