From 5115b63f7fbeeb9003e6f7b04449fa76a9ff7a1f Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Wed, 13 May 2026 11:26:29 +0000 Subject: [PATCH 1/9] docs: add EdDSA/Ed25519 to allowed WebAuthn signature schemes --- docs/references/ic-interface-spec/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/references/ic-interface-spec/index.md b/docs/references/ic-interface-spec/index.md index d4e23614..c54ba669 100644 --- a/docs/references/ic-interface-spec/index.md +++ b/docs/references/ic-interface-spec/index.md @@ -353,6 +353,8 @@ The allowed signature schemes for web authentication are - [**RSA PKCS\#1v1.5 (RSASSA-PKCS1-v1\_5)**](https://datatracker.ietf.org/doc/html/rfc8017#section-8.2), using SHA-256 as hash function. +- [**EdDSA**](https://datatracker.ietf.org/doc/html/rfc8032) on curve Ed25519. + The signature is calculated by using the payload as the challenge in the web authentication assertion. The signature is checked by verifying that the `challenge` field contains the [base64url encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-5) of the payload, and that `signature` verifies on `authenticatorData · SHA-256(utf8(clientDataJSON))`, as specified in the [WebAuthn w3c recommendation](https://www.w3.org/TR/webauthn/#op-get-assertion). @@ -385,7 +387,7 @@ You can also view the wrapping in [an online ASN.1 JavaScript decoder](https://l - `client_data_json` (`text`): WebAuthn client data in JSON representation. - - `signature` (`blob`): Signature as specified in the [WebAuthn w3c recommendation](https://www.w3.org/TR/webauthn/#signature-attestation-types), which means DER encoding in the case of an ECDSA signature. + - `signature` (`blob`): Signature as specified in the [WebAuthn w3c recommendation](https://www.w3.org/TR/webauthn/#signature-attestation-types), which means DER encoding in the case of an ECDSA signature, and the 64-byte concatenation `R || s` as defined in [RFC 8032, Section 5.1.6](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6) in the case of an EdDSA signature on curve Ed25519. #### Canister signatures {#canister-signatures} From 603c136b31e292404cf6ac0eceb470f06f9fcafa Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Wed, 13 May 2026 13:13:25 +0000 Subject: [PATCH 2/9] feat: canister logs extensions --- docs/guides/canister-management/settings.mdx | 2 - .../ic-interface-spec/abstract-behavior.md | 185 ++++++++++++------ .../ic-interface-spec/management-canister.md | 19 +- public/references/ic.did | 7 + 4 files changed, 144 insertions(+), 69 deletions(-) diff --git a/docs/guides/canister-management/settings.mdx b/docs/guides/canister-management/settings.mdx index 79bdf837..68906394 100644 --- a/docs/guides/canister-management/settings.mdx +++ b/docs/guides/canister-management/settings.mdx @@ -197,8 +197,6 @@ settings: ### Log memory limit -{/* Needs human verification: log_memory_limit is exposed by icp-cli but is absent from the canonical ic.did: verify whether this is a management canister setting or an icp-cli layer setting */} - Maximum memory for storing canister logs. Oldest logs are purged when usage exceeds this value. | Property | Value | diff --git a/docs/references/ic-interface-spec/abstract-behavior.md b/docs/references/ic-interface-spec/abstract-behavior.md index fc268f0f..cf2c7dff 100644 --- a/docs/references/ic-interface-spec/abstract-behavior.md +++ b/docs/references/ic-interface-spec/abstract-behavior.md @@ -515,6 +515,7 @@ S = { certified_data: CanisterId ↦ Blob; canister_history: CanisterId ↦ CanisterHistory; canister_log_visibility: CanisterId ↦ CanisterLogVisibility; + canister_log_memory_limit: CanisterId ↦ Nat; canister_snapshot_visibility: CanisterId ↦ CanisterSnapshotVisibility; canister_logs: CanisterId ↦ [CanisterLog]; query_stats: CanisterId ↦ [QueryStats]; @@ -624,6 +625,7 @@ The initial state of the IC is certified_data = (); canister_history = (); canister_log_visibility = (); + canister_log_memory_limit = (); canister_snapshot_visibility = (); canister_logs = (); query_stats = (); @@ -1710,6 +1712,11 @@ if A.settings.log_visibility is not null: else: New_canister_log_visibility = Controllers +if A.settings.log_memory_limit is not null: + New_canister_log_memory_limit = A.settings.log_memory_limit +else: + New_canister_log_memory_limit = 4096 + if A.settings.snapshot_visibility is not null: New_canister_snapshot_visibility = A.settings.snapshot_visibility else: @@ -1741,6 +1748,7 @@ S' = S with query_stats[Canister_id] = [] canister_history[Canister_id] = New_canister_history canister_log_visibility[Canister_id] = New_canister_log_visibility + canister_log_memory_limit[Canister_id] = New_canister_log_memory_limit canister_snapshot_visibility[Canister_id] = New_canister_snapshot_visibility canister_logs[Canister_id] = [] messages = Older_messages · Younger_messages · @@ -1877,6 +1885,8 @@ S' = S with canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1 if A.settings.log_visibility is not null: canister_log_visibility[A.canister_id] = A.settings.log_visibility + if A.settings.log_memory_limit is not null: + canister_log_memory_limit[A.canister_id] = A.settings.log_memory_limit if A.settings.snapshot_visibility is not null: canister_snapshot_visibility[A.canister_id] = A.settings.snapshot_visibility messages = Older_messages · Younger_messages · @@ -2832,7 +2842,8 @@ S with certified_data[A.canister_id] = (deleted) canister_history[A.canister_id] = (deleted) canister_log_visibility[A.canister_id] = (deleted) - canister_snapshot_visibility[A.canister_id] = (deleted) + canister_log_memory_limit[A.canister_id] = (deleted) + canister_snapshot_visibility[A.canister_id] = (deleted) canister_logs[A.canister_id] = (deleted) query_stats[A.canister_id] = (deleted) chunk_store[A.canister_id] = (deleted) @@ -3065,6 +3076,11 @@ if A.settings.log_visibility is not null: else: New_canister_log_visibility = Controllers +if A.settings.log_memory_limit is not null: + New_canister_log_memory_limit = A.settings.log_memory_limit +else: + New_canister_log_memory_limit = 4096 + if A.settings.snapshot_visibility is not null: New_canister_snapshot_visibility = A.settings.snapshot_visibility else: @@ -3094,6 +3110,7 @@ S' = S with certified_data[Canister_id] = "" canister_history[Canister_id] = New_canister_history canister_log_visibility[Canister_id] = New_canister_log_visibility + canister_log_memory_limit[Canister_id] = New_canister_log_memory_limit canister_snapshot_visibility[Canister_id] = New_canister_snapshot_visibility canister_logs[Canister_id] = [] query_stats[CanisterId] = [] @@ -3735,6 +3752,101 @@ S with ``` +#### IC Management Canister: Canister logs {#ic-mgmt-canister-fetch-canister-logs} + +Given a state `S`, `Canister_id`, `filter`, and `Sender`, we define + +```html + +canister_logs(S, Canister_id, filter) = + if filter = by_idx Range: + { Log | Log ∈ S.canister_logs[Canister_id] ∧ Range.start <= Log.idx ∧ Log.idx < Range.end } + else if filter = by_timestamp_nanos Range: + { Log | Log ∈ S.canister_logs[Canister_id] ∧ Range.start <= Log.timestamp_nanos ∧ Log.timestamp_nanos < Range.end } + else: + S.canister_logs[Canister_id] +fetch_canister_logs_cost(S, Canister_id) = +is_sender_authorized(S, Canister_id, Sender) = + (S[Canister_id].canister_log_visibility = Public) + or + (S[Canister_id].canister_log_visibility = Controllers and Sender in S[Canister_id].controllers) + or + (S[Canister_id].canister_log_visibility = AllowedViewers Principals and (Sender in S[Canister_id].controllers or Sender in Principals)) + +``` + +Conditions + +```html + +S.messages = Older_messages · CallMessage M · Younger_messages +(M.queue = Unordered) or (∀ CallMessage M' | FuncMessage M' ∈ Older_messages. M'.queue ≠ M.queue) +M.callee = ic_principal +M.method_name = 'fetch_canister_logs' +M.arg = candid(A) +fetch_canister_logs_cost(S, A.canister_id) <= M.transferred_cycles +is_sender_authorized(S, A.canister_id, M.caller) + +``` + +State after + +```html + +S with + messages = Older_messages · Younger_messages · + ResponseMessage { + origin = M.origin + response = candid(canister_logs(S, A.canister_id, A.filter)) + refunded_cycles = M.transferred_cycles - fetch_canister_logs_cost(S, A.canister_id) + } + +``` + +The IC method `fetch_canister_logs` can also be invoked via management canister query calls. +They are calls to `/api/v3/canister//query` +with CBOR content `Q` such that `Q.canister_id = ic_principal`. + +Submitted request to `/api/v3/canister//query` + +```html + +E : Envelope + +``` + +Conditions + +```html + +E.content = CanisterQuery Q +Q.canister_id = ic_principal +Q.method_name = 'fetch_canister_logs' +|Q.nonce| <= 32 +is_effective_canister_id(E.content, ECID) +S.system_time <= Q.ingress_expiry or Q.sender = anonymous_id +Q.arg = candid(A) +A.canister_id ∈ verify_envelope(E, Q.sender, S.system_time) +is_sender_authorized(S, A.canister_id, Q.sender) + +``` + +Query response `R`: + +```html + +{status: "replied"; reply: {arg: candid(canister_logs(S, A.canister_id, A.filter))}, signatures: Sigs} + +``` + +where the query `Q`, the response `R`, and a certificate `Cert` that is obtained by requesting the path `/subnet` in a **separate** read state request to `/api/v3/canister//read_state` satisfy the following: + +```html + +verify_response(Q, R, Cert) ∧ lookup(["time"], Cert) = Found S.system_time // or "recent enough" + +``` + #### Callback invocation When an inter-canister call has been responded to, we can queue the call to the callback. @@ -4073,6 +4185,8 @@ S with canister_history[Canister_id] = (deleted) canister_log_visibility[New_canister_id] = S.canister_log_visibility[Canister_id] canister_log_visibility[Canister_id] = (deleted) + canister_log_memory_limit[New_canister_id] = S.canister_log_memory_limit[Canister_id] + canister_log_memory_limit[Canister_id] = (deleted) canister_snapshot_visibility[New_canister_id] = S.canister_snapshot_visibility[Canister_id] canister_snapshot_visibility[Canister_id] = (deleted) canister_logs[New_canister_id] = S.canister_logs[Canister_id] @@ -4236,16 +4350,19 @@ S with ``` -#### Trimming canister logs +#### Purging canister logs -Canister logs can be trimmed if their total length exceeds 4KiB. +Oldest canister logs are purged if the total memory used for canister logs exceeds the value `log_memory_limit` in canister settings. + +The (unspecified) function `canister_log_memory_usage(logs)` models the total memory used by `logs`. Conditions ```html S.canister_logs[CanisterId] = Older_logs · Newer_logs -SUM { |l| | l <- Older_logs } > 4KiB +canister_log_memory_usage(Older_logs · Newer_logs) > S.canister_log_memory_limit[CanisterId] +canister_log_memory_usage(Newer_logs) ≤ S.canister_log_memory_limit[CanisterId] ``` @@ -4258,66 +4375,6 @@ S with ``` -#### IC Management Canister: Canister logs (query call) {#ic-mgmt-canister-fetch-canister-logs} - -This section specifies management canister query calls. -They are calls to `/api/v3/canister//query` -with CBOR content `Q` such that `Q.canister_id = ic_principal`. - -The management canister offers the method `fetch_canister_logs` -that can be called as a query call and -returns logs of a requested canister. - -Submitted request to `/api/v3/canister//query` - -```html - -E : Envelope - -``` - -Conditions - -```html - -E.content = CanisterQuery Q -Q.canister_id = ic_principal -Q.method_name = 'fetch_canister_logs' -|Q.nonce| <= 32 -is_effective_canister_id(E.content, ECID) -S.system_time <= Q.ingress_expiry or Q.sender = anonymous_id -Q.arg = candid(A) -A.canister_id ∈ verify_envelope(E, Q.sender, S.system_time) -if E.sender_pubkey = canister_signature_pk Signing_canister_id Seed: - if not (Q.sender_info = null): - verify_signature E.sender_pubkey Q.sender_info.sig ("\x0Eic-sender-info" · Q.sender_info.info) - Q.sender_info.signer = Signing_canister_id -else: - Q.sender_info = null -(S[A.canister_id].canister_log_visibility = Public) - or - (S[A.canister_id].canister_log_visibility = Controllers and Q.sender in S[A.canister_id].controllers) - or - (S[A.canister_id].canister_log_visibility = AllowedViewers Principals and (Q.sender in S[A.canister_id].controllers or Q.sender in Principals)) - -``` - -Query response `R`: - -```html - -{status: "replied"; reply: {arg: candid(S.canister_logs[A.canister_id])}, signatures: Sigs} - -``` - -where the query `Q`, the response `R`, and a certificate `Cert` that is obtained by requesting the path `/subnet` in a **separate** read state request to `/api/v3/canister//read_state` satisfy the following: - -```html - -verify_response(Q, R, Cert) ∧ lookup(["time"], Cert) = Found S.system_time // or "recent enough" - -``` - #### IC Management Canister: List canisters (query call) {#ic-mgmt-canister-list-canisters} This section specifies the `list_canisters` management canister query call. diff --git a/docs/references/ic-interface-spec/management-canister.md b/docs/references/ic-interface-spec/management-canister.md index 861022c1..e62058e9 100644 --- a/docs/references/ic-interface-spec/management-canister.md +++ b/docs/references/ic-interface-spec/management-canister.md @@ -99,6 +99,13 @@ The optional `settings` parameter can be used to set the following settings: Default value: `controllers`. +- `log_memory_limit` (`nat`) + + Must be at most `2097152` (`2 MiB`) and indicates the maximum amount of memory used for canister logs. + Oldest canister logs are purged if the total memory used for canister logs exceeds this value. + + Default value: `4096`. + - `snapshot_visibility` (`snapshot_visibility`) Controls who can access the canister's snapshots through the following endpoints of the management canister: @@ -304,6 +311,8 @@ Only the controllers of the canister or the canister itself or subnet admins can * `snapshots_size`: Represents the memory consumed by all snapshots that belong to this canister. + * `log_memory_store_size`: Represents the memory used by canister logs of the canister. + All sizes are expressed in bytes. ### IC method `canister_info` {#ic-canister_info} @@ -905,12 +914,12 @@ A snapshot may be deleted only by the controllers of the canister that the snaps ### IC method `fetch_canister_logs` {#ic-fetch_canister_logs} -This method can only be called by external users via non-replicated (query) calls, i.e., it cannot be called by canisters, cannot be called via replicated calls, and cannot be called from composite query calls. +This method can only be called by external users via non-replicated (query) calls or by canisters (via replicated calls), i.e., it cannot be called by external users via replicated (update) calls and it cannot be called from composite query calls. Given a canister ID as input, this method returns a vector of logs of that canister including its trap messages. The canister logs are *not* collected in canister methods running in non-replicated mode (NRQ, TQ, CQ, CRy, CRt, CC, and F modes, as defined in [Overview of imports](./canister-interface.md#system-api-imports)) and the canister logs are *purged* when the canister is reinstalled or uninstalled. -The total size of all returned logs does not exceed 4KiB. -If new logs are added resulting in exceeding the maximum total log size of 4KiB, the oldest logs will be removed. +The total size of all returned logs does not exceed the value `log_memory_limit` in canister settings. +Oldest canister logs are purged if the total memory used for canister logs exceeds the value `log_memory_limit` in canister settings. Logs persist across canister upgrades and they are deleted if the canister is reinstalled or uninstalled. The log visibility is defined in the `log_visibility` field of `canister_settings` and can be one of the following variants: @@ -925,6 +934,10 @@ A single log is a record with the following fields: - `timestamp_nanos` (`nat64`): the timestamp as nanoseconds since 1970-01-01 at which the log was recorded; - `content` (`blob`): the actual content of the log; +To filter canister logs, an optional filter can be provided and have one of the following variants: +- `by_idx` (`record { start : nat64; end : nat64 }`): only logs are returned whose `idx` is within the provided range (`start` is inclusive, but `end` is exclusive); +- `by_timestamp_nanos` (`record { start : nat64; end : nat64 }`): only logs are returned whose `timestamp_nanos` is within the provided range (`start` is inclusive, but `end` is exclusive). + :::warning The response of a query comes from a single replica, and is therefore not appropriate for security-sensitive applications. diff --git a/public/references/ic.did b/public/references/ic.did index cad1d2b4..0ddad626 100644 --- a/public/references/ic.did +++ b/public/references/ic.did @@ -26,6 +26,7 @@ type canister_settings = record { freezing_threshold : opt nat; reserved_cycles_limit : opt nat; log_visibility : opt log_visibility; + log_memory_limit : opt nat; snapshot_visibility : opt snapshot_visibility; wasm_memory_limit : opt nat; wasm_memory_threshold : opt nat; @@ -39,6 +40,7 @@ type definite_canister_settings = record { freezing_threshold : nat; reserved_cycles_limit : nat; log_visibility : log_visibility; + log_memory_limit : nat; snapshot_visibility : snapshot_visibility; wasm_memory_limit : nat; wasm_memory_threshold : nat; @@ -300,6 +302,7 @@ type canister_status_result = record { canister_history_size : nat; wasm_chunk_store_size : nat; snapshots_size : nat; + log_memory_store_size : nat; }; cycles : nat; reserved_cycles : nat; @@ -499,6 +502,10 @@ type delete_canister_snapshot_args = record { type fetch_canister_logs_args = record { canister_id : canister_id; + filter : opt variant { + by_idx : record { start : nat64; end : nat64 }; + by_timestamp_nanos : record { start : nat64; end : nat64 }; + } }; type canister_log_record = record { From e31e334f7ea134c7e8e2796826222e1e2955458f Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Wed, 13 May 2026 13:16:20 +0000 Subject: [PATCH 3/9] Revert "docs: add EdDSA/Ed25519 to allowed WebAuthn signature schemes" This reverts commit 5115b63f7fbeeb9003e6f7b04449fa76a9ff7a1f. --- docs/references/ic-interface-spec/index.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/references/ic-interface-spec/index.md b/docs/references/ic-interface-spec/index.md index c54ba669..d4e23614 100644 --- a/docs/references/ic-interface-spec/index.md +++ b/docs/references/ic-interface-spec/index.md @@ -353,8 +353,6 @@ The allowed signature schemes for web authentication are - [**RSA PKCS\#1v1.5 (RSASSA-PKCS1-v1\_5)**](https://datatracker.ietf.org/doc/html/rfc8017#section-8.2), using SHA-256 as hash function. -- [**EdDSA**](https://datatracker.ietf.org/doc/html/rfc8032) on curve Ed25519. - The signature is calculated by using the payload as the challenge in the web authentication assertion. The signature is checked by verifying that the `challenge` field contains the [base64url encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-5) of the payload, and that `signature` verifies on `authenticatorData · SHA-256(utf8(clientDataJSON))`, as specified in the [WebAuthn w3c recommendation](https://www.w3.org/TR/webauthn/#op-get-assertion). @@ -387,7 +385,7 @@ You can also view the wrapping in [an online ASN.1 JavaScript decoder](https://l - `client_data_json` (`text`): WebAuthn client data in JSON representation. - - `signature` (`blob`): Signature as specified in the [WebAuthn w3c recommendation](https://www.w3.org/TR/webauthn/#signature-attestation-types), which means DER encoding in the case of an ECDSA signature, and the 64-byte concatenation `R || s` as defined in [RFC 8032, Section 5.1.6](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6) in the case of an EdDSA signature on curve Ed25519. + - `signature` (`blob`): Signature as specified in the [WebAuthn w3c recommendation](https://www.w3.org/TR/webauthn/#signature-attestation-types), which means DER encoding in the case of an ECDSA signature. #### Canister signatures {#canister-signatures} From 9d5b4756f3a9b7368b5d452ce6ef53ac337a7896 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Wed, 13 May 2026 13:54:43 +0000 Subject: [PATCH 4/9] note --- docs/references/ic-interface-spec/management-canister.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/references/ic-interface-spec/management-canister.md b/docs/references/ic-interface-spec/management-canister.md index e62058e9..29863b57 100644 --- a/docs/references/ic-interface-spec/management-canister.md +++ b/docs/references/ic-interface-spec/management-canister.md @@ -938,6 +938,8 @@ To filter canister logs, an optional filter can be provided and have one of the - `by_idx` (`record { start : nat64; end : nat64 }`): only logs are returned whose `idx` is within the provided range (`start` is inclusive, but `end` is exclusive); - `by_timestamp_nanos` (`record { start : nat64; end : nat64 }`): only logs are returned whose `timestamp_nanos` is within the provided range (`start` is inclusive, but `end` is exclusive). +Cycles to pay for the call must be explicitly transferred with the call, i.e., they are not automatically deducted from the caller's balance implicitly (e.g., as for inter-canister calls). + :::warning The response of a query comes from a single replica, and is therefore not appropriate for security-sensitive applications. From bfa60a59b088121a95f9bd77aa1ad89b9599baf4 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Wed, 3 Jun 2026 15:50:38 +0200 Subject: [PATCH 5/9] reset log_memory_limit if canister out of cycles --- docs/references/ic-interface-spec/abstract-behavior.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/references/ic-interface-spec/abstract-behavior.md b/docs/references/ic-interface-spec/abstract-behavior.md index 6633fe09..93c1214b 100644 --- a/docs/references/ic-interface-spec/abstract-behavior.md +++ b/docs/references/ic-interface-spec/abstract-behavior.md @@ -4128,6 +4128,7 @@ S with global_timer[CanisterId] = 0 compute_allocation[Canister_id] = 0 memory_allocation[Canister_id] = 0 + log_memory_limit[Canister_id] = 0 messages = S.messages · [ ResponseMessage { From 3665ee0b5d690aadb57371d1c37233fd0aabced4 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Fri, 17 Jul 2026 08:45:02 +0000 Subject: [PATCH 6/9] docs(logs): clarify canister log trimming direction Emphasize that an unfiltered fetch_canister_logs read trims the oldest records (response ends with the newest record) while a filtered read trims the newest records (response starts with the oldest matching record). Bound the trimming by an unspecified max response size. Also drop the stale duplicate "Canister logs (query call)" section re-introduced by the merge from main, which carried a duplicate anchor and pre-filter semantics. --- docs/guides/canister-management/logs.md | 2 + .../ic-interface-spec/abstract-behavior.md | 80 +++++-------------- .../ic-interface-spec/management-canister.md | 4 + 3 files changed, 26 insertions(+), 60 deletions(-) diff --git a/docs/guides/canister-management/logs.md b/docs/guides/canister-management/logs.md index ea2972b0..0f588743 100644 --- a/docs/guides/canister-management/logs.md +++ b/docs/guides/canister-management/logs.md @@ -102,6 +102,8 @@ icp canister logs -e ic --since-index 100 --until-index 200 Timestamp and index filters cannot be combined with `--follow`. +Filtering also changes which records are dropped when the selected logs exceed the response size. An unfiltered read trims the oldest records, so it returns the most recent logs (the output ends with the newest record). A filtered read trims the newest records instead, so it returns the oldest records that match the filter (the output starts with the oldest matching record), which lets you page forward through a range by advancing the start of the filter. + To output logs as JSON for programmatic processing: ```bash diff --git a/docs/references/ic-interface-spec/abstract-behavior.md b/docs/references/ic-interface-spec/abstract-behavior.md index 93c1214b..daf2a69c 100644 --- a/docs/references/ic-interface-spec/abstract-behavior.md +++ b/docs/references/ic-interface-spec/abstract-behavior.md @@ -3823,12 +3823,21 @@ Given a state `S`, `Canister_id`, `filter`, and `Sender`, we define ```html canister_logs(S, Canister_id, filter) = - if filter = by_idx Range: - { Log | Log ∈ S.canister_logs[Canister_id] ∧ Range.start <= Log.idx ∧ Log.idx < Range.end } - else if filter = by_timestamp_nanos Range: - { Log | Log ∈ S.canister_logs[Canister_id] ∧ Range.start <= Log.timestamp_nanos ∧ Log.timestamp_nanos < Range.end } - else: - S.canister_logs[Canister_id] + let Selected_logs = the sublist of S.canister_logs[Canister_id] (preserving order) such that + if filter = by_idx Range: + Log ∈ Selected_logs ⟺ Log ∈ S.canister_logs[Canister_id] ∧ Range.start <= Log.idx ∧ Log.idx < Range.end + else if filter = by_timestamp_nanos Range: + Log ∈ Selected_logs ⟺ Log ∈ S.canister_logs[Canister_id] ∧ Range.start <= Log.timestamp_nanos ∧ Log.timestamp_nanos < Range.end + else: + Log ∈ Selected_logs ⟺ Log ∈ S.canister_logs[Canister_id] + in + if filter = null: + the longest suffix Younger_logs of Selected_logs + such that canister_log_memory_usage(Younger_logs) ≤ max_response_size + else: + the longest prefix Older_logs of Selected_logs + such that canister_log_memory_usage(Older_logs) ≤ max_response_size +max_response_size = fetch_canister_logs_cost(S, Canister_id) = is_sender_authorized(S, Canister_id, Sender) = (S[Canister_id].canister_log_visibility = Public) @@ -3839,6 +3848,11 @@ is_sender_authorized(S, Canister_id, Sender) = ``` +When the selected logs do not all fit within a single response, they are trimmed to fit and the direction of trimming depends on whether a filter is provided. +An unfiltered read trims the oldest log records (keeps the longest suffix), so the response ends with the newest log record. +A filtered read trims the newest log records (keeps the longest prefix), so the response starts with the oldest log record satisfying the filter. +Thus an unfiltered read surfaces the most recent activity, while a filtered read can page forward through logs starting from the beginning of the requested range. + Conditions ```html @@ -4440,60 +4454,6 @@ S with ``` -#### IC Management Canister: Canister logs (query call) {#ic-mgmt-canister-fetch-canister-logs} - -This section specifies management canister query calls. -They are calls to `/api/v3/canister//query` -with CBOR content `Q` such that `Q.canister_id = ic_principal`. - -The management canister offers the method `fetch_canister_logs` -that can be called as a query call and -returns logs of a requested canister. - -Submitted request to `/api/v3/canister//query` - -```html - -E : Envelope - -``` - -Conditions - -```html - -E.content = CanisterQuery Q -Q.canister_id = ic_principal -Q.method_name = 'fetch_canister_logs' -|Q.nonce| <= 32 -is_effective_canister_id(E.content, ECID) -S.system_time <= Q.ingress_expiry or Q.sender = anonymous_id -Q.arg = candid(A) -A.canister_id ∈ verify_envelope(E, Q.sender, S.system_time) -(S[A.canister_id].canister_log_visibility = Public) - or - (S[A.canister_id].canister_log_visibility = Controllers and Q.sender in S[A.canister_id].controllers) - or - (S[A.canister_id].canister_log_visibility = AllowedViewers Principals and (Q.sender in S[A.canister_id].controllers or Q.sender in Principals)) - -``` - -Query response `R`: - -```html - -{status: "replied"; reply: {arg: candid(S.canister_logs[A.canister_id])}, signatures: Sigs} - -``` - -where the query `Q`, the response `R`, and a certificate `Cert` that is obtained by requesting the path `/subnet` in a **separate** read state request to `/api/v3/canister//read_state` satisfy the following: - -```html - -verify_response(Q, R, Cert) ∧ lookup(["time"], Cert) = Found S.system_time // or "recent enough" - -``` - #### IC Management Canister: List canisters (query call) {#ic-mgmt-canister-list-canisters} This section specifies the `list_canisters` management canister query call. diff --git a/docs/references/ic-interface-spec/management-canister.md b/docs/references/ic-interface-spec/management-canister.md index ca81d18a..9925cf18 100644 --- a/docs/references/ic-interface-spec/management-canister.md +++ b/docs/references/ic-interface-spec/management-canister.md @@ -954,6 +954,10 @@ To filter canister logs, an optional filter can be provided and have one of the - `by_idx` (`record { start : nat64; end : nat64 }`): only logs are returned whose `idx` is within the provided range (`start` is inclusive, but `end` is exclusive); - `by_timestamp_nanos` (`record { start : nat64; end : nat64 }`): only logs are returned whose `timestamp_nanos` is within the provided range (`start` is inclusive, but `end` is exclusive). +When the logs selected for the response do not all fit within a single response, they are trimmed to fit, and the direction of trimming differs between filtered and unfiltered reads: +- An **unfiltered** read trims the **oldest** log records, so the response ends with the newest log record. This surfaces the most recent activity. +- A **filtered** read trims the **newest** log records, so the response starts with the oldest log record satisfying the filter. This lets a filtered read page forward through logs starting from the beginning of the requested range. + Cycles to pay for the call must be explicitly transferred with the call, i.e., they are not automatically deducted from the caller's balance implicitly (e.g., as for inter-canister calls). :::warning From 92bd88e97bc154b760a17a525322d888c3ab4eb6 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Fri, 17 Jul 2026 08:52:32 +0000 Subject: [PATCH 7/9] docs(logs): drop fetch_canister_logs_cost (calls do not require cycles) Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/references/ic-interface-spec/abstract-behavior.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/references/ic-interface-spec/abstract-behavior.md b/docs/references/ic-interface-spec/abstract-behavior.md index daf2a69c..4870bd87 100644 --- a/docs/references/ic-interface-spec/abstract-behavior.md +++ b/docs/references/ic-interface-spec/abstract-behavior.md @@ -3838,7 +3838,6 @@ canister_logs(S, Canister_id, filter) = the longest prefix Older_logs of Selected_logs such that canister_log_memory_usage(Older_logs) ≤ max_response_size max_response_size = -fetch_canister_logs_cost(S, Canister_id) = is_sender_authorized(S, Canister_id, Sender) = (S[Canister_id].canister_log_visibility = Public) or @@ -3862,7 +3861,6 @@ S.messages = Older_messages · CallMessage M · Younger_messages M.callee = ic_principal M.method_name = 'fetch_canister_logs' M.arg = candid(A) -fetch_canister_logs_cost(S, A.canister_id) <= M.transferred_cycles is_sender_authorized(S, A.canister_id, M.caller) ``` @@ -3876,7 +3874,7 @@ S with ResponseMessage { origin = M.origin response = candid(canister_logs(S, A.canister_id, A.filter)) - refunded_cycles = M.transferred_cycles - fetch_canister_logs_cost(S, A.canister_id) + refunded_cycles = M.transferred_cycles } ``` From 9eb29f6fb9386f1d25ef3449b27f3544e3fcf5eb Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Fri, 17 Jul 2026 08:53:28 +0000 Subject: [PATCH 8/9] docs(logs): remove cycles-transfer note from fetch_canister_logs Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/references/ic-interface-spec/management-canister.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/references/ic-interface-spec/management-canister.md b/docs/references/ic-interface-spec/management-canister.md index 9925cf18..2a277715 100644 --- a/docs/references/ic-interface-spec/management-canister.md +++ b/docs/references/ic-interface-spec/management-canister.md @@ -958,8 +958,6 @@ When the logs selected for the response do not all fit within a single response, - An **unfiltered** read trims the **oldest** log records, so the response ends with the newest log record. This surfaces the most recent activity. - A **filtered** read trims the **newest** log records, so the response starts with the oldest log record satisfying the filter. This lets a filtered read page forward through logs starting from the beginning of the requested range. -Cycles to pay for the call must be explicitly transferred with the call, i.e., they are not automatically deducted from the caller's balance implicitly (e.g., as for inter-canister calls). - :::warning The response of a query comes from a single replica, and is therefore not appropriate for security-sensitive applications. From b3b765b2d85d33ef81c8db41999c2c073933d057 Mon Sep 17 00:00:00 2001 From: Martin Raszyk Date: Fri, 17 Jul 2026 08:55:19 +0000 Subject: [PATCH 9/9] docs(logs): clarify returned-log size bound is the max response size Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/references/ic-interface-spec/management-canister.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/ic-interface-spec/management-canister.md b/docs/references/ic-interface-spec/management-canister.md index 2a277715..b993c15d 100644 --- a/docs/references/ic-interface-spec/management-canister.md +++ b/docs/references/ic-interface-spec/management-canister.md @@ -934,7 +934,7 @@ This method can only be called by external users via non-replicated (query) call Given a canister ID as input, this method returns a vector of logs of that canister including its trap messages. The canister logs are *not* collected in canister methods running in non-replicated mode (NRQ, TQ, CQ, CRy, CRt, CC, and F modes, as defined in [Overview of imports](./canister-interface.md#system-api-imports)) and the canister logs are *purged* when the canister is reinstalled or uninstalled. -The total size of all returned logs does not exceed the value `log_memory_limit` in canister settings. +The total size of all returned logs does not exceed an implementation-defined constant chosen so as not to exceed the maximum response size. Oldest canister logs are purged if the total memory used for canister logs exceeds the value `log_memory_limit` in canister settings. Logs persist across canister upgrades and they are deleted if the canister is reinstalled or uninstalled.