From 14cd3dfba891bd08c56b4bbe57046a15a18923cd Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:54:09 +0100 Subject: [PATCH] [FINAL] feat: Allow accepting and burning cycles in replicated queries (#3760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FINAL] feat: Allow accepting and burning cycles in replicated queries * changelog --------- Co-authored-by: Björn Tackmann <54846571+Dfinity-Bjoern@users.noreply.github.com> --- .../_attachments/interface-spec-changelog.md | 3 + docs/references/ic-interface-spec.md | 94 ++++++++++--------- 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/docs/references/_attachments/interface-spec-changelog.md b/docs/references/_attachments/interface-spec-changelog.md index 5f465b0a9f..2d3c0fc653 100644 --- a/docs/references/_attachments/interface-spec-changelog.md +++ b/docs/references/_attachments/interface-spec-changelog.md @@ -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 diff --git a/docs/references/ic-interface-spec.md b/docs/references/ic-interface-spec.md index 4461b8bf64..b011081f80 100644 --- a/docs/references/ic-interface-spec.md +++ b/docs/references/ic-interface-spec.md @@ -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) -> (); // * @@ -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 @@ -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` @@ -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 { @@ -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). @@ -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 ) @@ -4232,8 +4234,8 @@ 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; @@ -4241,7 +4243,7 @@ query_as_update(f, arg, env) = λ wasm_state → new_certified_data = NoCertifiedData; new_global_timer = NoGlobalTimer; response = res.response; - cycles_accepted = 0; + cycles_accepted = res.cycles_accepted; cycles_used = res.cycles_used; } @@ -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 `, 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() with Trap then Trap {cycles_used = es.cycles_used;} Return { response = es.response; + cycles_accepted = es.cycles_accepted; cycles_used = es.cycles_used; } ``` @@ -7180,13 +7184,13 @@ ic0.msg_reject(src : I, size : I) = es.cycles_available := 0 ic0.msg_cycles_available() : 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(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(dst, amount.to_little_endian_bytes()) @@ -7202,7 +7206,7 @@ ic0.msg_cycles_refunded128(dst : I) = copy_cycles_to_canister(dst, amount.to_little_endian_bytes()) ic0.msg_cycles_accept(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 @@ -7211,7 +7215,7 @@ ic0.msg_cycles_accept(max_amount : i64) : i64 = I ∈ {i32, i64} ic0.msg_cycles_accept128(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 @@ -7221,7 +7225,7 @@ ic0.msg_cycles_accept128(max_amount_high : i64, max_amount_low : i64, dst : I ∈ {i32, i64} ic0.cycles_burn128(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