From 99bc27cb00159f4413c320d68c76baec4b230644 Mon Sep 17 00:00:00 2001 From: Franz-Stefan Preiss Date: Tue, 23 Jul 2024 16:06:23 +0200 Subject: [PATCH 01/14] [FINAL] Management canister API for tSchnorr signatures (#288) * Management canister API for tSchnorr signatures * Update link to KD spec * Improve spec regarding call requirements, also for ECDSA * Specify Ed25519 key derivation within IC spec * restrictions * changelog * experimental * fix experimental note --------- Co-authored-by: Martin Raszyk --- spec/_attachments/ic.did | 30 +++++ spec/_attachments/interface-spec-changelog.md | 1 + spec/index.md | 124 +++++++++++++++++- 3 files changed, 152 insertions(+), 3 deletions(-) diff --git a/spec/_attachments/ic.did b/spec/_attachments/ic.did index 717fd680..702b7ef4 100644 --- a/spec/_attachments/ic.did +++ b/spec/_attachments/ic.did @@ -76,6 +76,11 @@ type ecdsa_curve = variant { secp256k1; }; +type schnorr_algorithm = variant { + bip340secp256k1; + ed25519; +}; + type satoshi = nat64; type bitcoin_network = variant { @@ -303,6 +308,27 @@ type sign_with_ecdsa_result = record { signature : blob; }; +type schnorr_public_key_args = record { + canister_id : opt canister_id; + derivation_path : vec blob; + key_id : record { algorithm : schnorr_algorithm; name : text }; +}; + +type schnorr_public_key_result = record { + public_key : blob; + chain_code : blob; +}; + +type sign_with_schnorr_args = record { + message : blob; + derivation_path : vec blob; + key_id : record { algorithm : schnorr_algorithm; name : text }; +}; + +type sign_with_schnorr_result = record { + signature : blob; +}; + type node_metrics_history_args = record { subnet_id : principal; start_at_timestamp_nanos : nat64; @@ -377,6 +403,10 @@ service ic : { ecdsa_public_key : (ecdsa_public_key_args) -> (ecdsa_public_key_result); sign_with_ecdsa : (sign_with_ecdsa_args) -> (sign_with_ecdsa_result); + // Threshold Schnorr signature + schnorr_public_key : (schnorr_public_key_args) -> (schnorr_public_key_result); + sign_with_schnorr : (sign_with_schnorr_args) -> (sign_with_schnorr_result); + // bitcoin interface bitcoin_get_balance : (bitcoin_get_balance_args) -> (bitcoin_get_balance_result); bitcoin_get_balance_query : (bitcoin_get_balance_query_args) -> (bitcoin_get_balance_query_result) query; diff --git a/spec/_attachments/interface-spec-changelog.md b/spec/_attachments/interface-spec-changelog.md index 2cd3fb9a..a2649853 100644 --- a/spec/_attachments/interface-spec-changelog.md +++ b/spec/_attachments/interface-spec-changelog.md @@ -1,6 +1,7 @@ ## Changelog {#changelog} ### ∞ (unreleased) +* EXPERIMENTAL: Management canister API for threshold Schnorr signatures. ### 0.25.0 (2024-06-14) {#0_25_0} * Query call statistics. diff --git a/spec/index.md b/spec/index.md index 65877f01..21655d68 100644 --- a/spec/index.md +++ b/spec/index.md @@ -2253,11 +2253,13 @@ This method takes no input and returns 32 pseudo-random bytes to the caller. The This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages. -This method returns a [SEC1](https://www.secg.org/sec1-v2.pdf) encoded ECDSA public key for the given canister using the given derivation path. If the `canister_id` is unspecified, it will default to the canister id of the caller. The `derivation_path` is a vector of variable length byte strings. Each byte string may be of arbitrary length, including empty. The total number of byte strings in the `derivation_path` must be at most 255. The `key_id` is a struct specifying both a curve and a name. The availability of a particular `key_id` depends on implementation. +This method returns a [SEC1](https://www.secg.org/sec1-v2.pdf) encoded ECDSA public key for the given canister using the given derivation path. If the `canister_id` is unspecified, it will default to the canister id of the caller. The `derivation_path` is a vector of variable length byte strings. Each byte string may be of arbitrary length, including empty. The total number of byte strings in the `derivation_path` must be at most 255. The `key_id` is a struct specifying both a curve and a name. The availability of a particular `key_id` depends on the implementation. For curve `secp256k1`, the public key is derived using a generalization of BIP32 (see [ia.cr/2021/1330, Appendix D](https://ia.cr/2021/1330)). To derive (non-hardened) [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)-compatible public keys, each byte string (`blob`) in the `derivation_path` must be a 4-byte big-endian encoding of an unsigned integer less than 231. If the `derivation_path` contains a byte string that is not a 4-byte big-endian encoding of an unsigned integer less than 231, then a derived public key will be returned, but that key derivation process will not be compatible with the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) standard. -The return result is an extended public key consisting of an ECDSA `public_key`, encoded in [SEC1](https://www.secg.org/sec1-v2.pdf) compressed form, and a `chain_code`, which can be used to deterministically derive child keys of the `public_key`. +The return value is an extended public key consisting of an ECDSA `public_key`, encoded in [SEC1](https://www.secg.org/sec1-v2.pdf) compressed form, and a `chain_code`, which can be used to deterministically derive child keys of the `public_key`. + +This call requires that an ECDSA key with ID `key_id` was generated by the IC. Otherwise, the call is rejected. ### IC method `sign_with_ecdsa` {#ic-sign_with_ecdsa} @@ -2267,7 +2269,123 @@ This method returns a new [ECDSA](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FI The signatures are encoded as the concatenation of the [SEC1](https://www.secg.org/sec1-v2.pdf) encodings of the two values r and s. For curve `secp256k1`, this corresponds to 32-byte big-endian encoding. -This call requires that the ECDSA feature is enabled, the caller is a canister, and `message_hash` is 32 bytes long. Otherwise it will be rejected. +This call requires that an ECDSA key with ID `key_id` was generated by the IC, the signing functionality for that key was enabled, and `message_hash` is 32 bytes long. Otherwise, the call is is rejected. + +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). + +### IC method `schnorr_public_key` {#ic-schnorr_public_key} + +:::note + +Threshold Schnorr API is EXPERIMENTAL and there might be breaking changes of the behavior in the future. Use at your own risk! + +::: + +This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages. + +This method returns a (derived) Schnorr public key for the given canister using the given derivation path. If the `canister_id` is unspecified, it will default to the canister id of the caller. The `derivation_path` is a vector of variable length byte strings. Each byte string may be of arbitrary length, including empty. The total number of byte strings in the `derivation_path` must be at most 255. The `key_id` is a struct specifying both an algorithm and a name. The availability of a particular `key_id` depends on the implementation. + +The return value is an extended Schnorr public key consisting of a Schnorr `public_key` and a `chain_code`. The chain code can be used to deterministically derive child keys of the `public_key`. Both the derivation and the encoding of the public key depends on the key ID's `algorithm`: + +- For algorithm `bip340secp256k1`, the public key is derived using the generalization of BIP32 defined in [ia.cr/2021/1330, Appendix D](https://ia.cr/2021/1330). To derive (non-hardened) [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)-compatible public keys, each byte string (`blob`) in the `derivation_path` must be a 4-byte big-endian encoding of an unsigned integer less than 231. If the `derivation_path` contains a byte string that is not a 4-byte big-endian encoding of an unsigned integer less than 231, then a derived public key will be returned, but that key derivation process will not be compatible with the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) standard. + + The public key is encoded in [SEC1](https://www.secg.org/sec1-v2.pdf) compressed form. To use BIP32 public keys to verify BIP340 Schnorr signatures, the first byte of the (33-byte) SEC1-encoded public key must be removed (see [BIP-340, Public Key Conversion](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#public-key-conversion)). + +- For algorithm `ed25519`, the public key is derived using the scheme specified in [Ed25519 hierarchical key derivation](#ed25519-key-derivation). + + The public key is encoded in standard 32-byte compressed form (see [RFC8032, 5.1.2 Encoding](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2)). + +This call requires that a Schnorr key with ID `key_id` was generated by the IC. Otherwise, the call is rejected. + +#### Ed25519 hierarchical key derivation {#ed25519-key-derivation} + +This section describes a child key derivation (CKD) function for computing child public keys from Ed25519 parent public keys. +The section is inspired by [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) and uses similar wording and structure. + +##### Motivation + +To support the Ed25519 variant of threshold Schnorr signatures on the Internet Computer, a key derivation scheme compatible with Ed25519 signatures is required. +For a respective signing service on the Internet Computer to be efficient, the signing subnet maintains only a single master key pair and _derives_ signing child keys for each canister. +Although there exist various hierarchical key derivation schemes (e.g., [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki), [SLIP10](https://github.com/satoshilabs/slips/blob/master/slip-0010.md), [BIP32-Ed25519](https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf), [Schnorrkel](https://github.com/w3f/schnorrkel)), all of the analyzed schemes are either incompatible in a threshold setting (e.g., use hardened key derivation only), comply with clamping which adds unnecessary complexity, or otherwise rely on non-standard primitives. +For these reasons, a new derivation scheme is specified here. +This scheme does not make use of _clamping_ (see [RFC8032, Section 5.1.5, Item 2](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.5)), because it is unnecessary in the given setting, and satisfies the following requirements: + +- Off-chain availability: New public keys can be computed off-chain from a master public key without requiring interaction with the IC. +- Hierarchical derivation: Derived keys are organized in a tree such that from any public key it is possible to derive new child keys. The first level is used to derive unique canister-specific keys from the master key. +- Simplicity: The scheme is simple to implement using existing libraries. + +##### Conventions + +We will assume the elliptic curve (EC) operations using the field and curve parameters as defined by Ed25519 (see [RFC8032, Section 5.1](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1)). Variables below are either: + +- Integers modulo the order of the curve's prime order subgroup (referred to as L). +- Points on the curve. +- Byte sequences. + +Addition (+) of two points is defined as application of the EC group operation. +Concatenation (||) is the operation of appending one byte sequence onto another. + +We assume the following functions: + +- point(p): returns the point resulting from EC point multiplication (repeated application of the EC group operation) of the Ed25519 base point with the integer p. +- serP(P): serializes the point to a byte sequence using standard 32-byte compressed form (see [RFC8032, 5.1.2 Encoding](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2)). +- utf8(s): returns the UTF-8 encoding of string s. +- parse512(p): interprets a 64-byte sequence as a 512-bit number, most significant byte first. +- HKDF(salt,IKM,info,N) -> OKM: HMAC-based key derivation function (see [RFC5869](https://datatracker.ietf.org/doc/html/rfc5869)) using HMAC-SHA512 (see [RFC4231](https://datatracker.ietf.org/doc/html/rfc4231)) calculating N-bytes long output key material (OKM) from (byte sequences) salt, input key material (IKM), and application specific information *info*. + +##### Extended keys + +Public keys are extended with an extra 32 bytes of entropy, which extension is called chain code. +An extended public key is represented as (K, c), with K = point(k) and c being the chain code, for some private key k. +Each extended key can have an arbitrary number of child keys. +The scheme does not support hardened derivation of child keys. + +##### Child key derivation (CKD) function + +Given a parent extended public key and an index i, it is possible to compute the corresponding child extended public key. +The function CKDpub computes a child extended public key from a parent extended public key and an index i, where i is a byte sequence of arbitrary length (including empty). + +CKDpub((Kpar, cpar), i) → (Ki, ci): +- let IKM = serP(Kpar) || i. +- let OKM = HKDF(cpar, IKM, utf8("Ed25519"), 96). +- Split OKM into a 64-byte and a 32-byte sequence, tweak and ci. +- let Ki = Kpar + point(parse512(tweak) mod L). +- return (Ki, ci). + +##### Key tree + +A key tree can be built by repeatedly applying CKDpub, starting with one root, called the master extended public key M. +Computing CKDpub(M, i) for different values of i results in a number of level-0 derived keys. +As each of these is again an extended key, CKDpub can be applied to those as well. +The sequence of indices used when repeatedly applying CKDpub is called the _derivation path_. + +The function KTpub computes a child extended public key from a parent extended public key and a derivation path d. + +KTpub((Kpar, cpar), d) → (Kd, cd): +- let (Kd, cd) = (Kpar, cpar) +- for all indices i in d: + (Kd, cd) = CKDpub((Kd, cd), i) +- return (Kd, cd). + +### IC method `sign_with_schnorr` {#ic-sign_with_schnorr} + +:::note + +Threshold Schnorr API is EXPERIMENTAL and there might be breaking changes of the behavior in the future. Use at your own risk! + +::: + +This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages. + +This method returns a Schnorr signature of the given `message` that can be verified against a (derived) public key obtained by calling `schnorr_public_key` using the caller's `canister_id` and the given `derivation_path` and `key_id`. + +The encoding of the signature depends on the key ID's `algorithm`: + +- For algorithm `bip340secp256k1`, the signature is encoded in 64 bytes according to [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). + +- For algorithm `ed25519`, the signature is encoded in 64 bytes according to [RFC8032, 5.1.6 Sign](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6). + +This call requires that a Schnorr key with ID `key_id` was generated by the IC and the signing functionality for that key was enabled. Otherwise, the call is is rejected. 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). From 2b479115c064b82f551815aac000dfd33d2beb2d Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:31:39 +0200 Subject: [PATCH 02/14] fix: simplify definition of single-variant Delegation type (#327) --- spec/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/index.md b/spec/index.md index 21655d68..45733c3b 100644 --- a/spec/index.md +++ b/spec/index.md @@ -2755,9 +2755,9 @@ A certificate by the root subnet does not have a delegation field. A certificate The certificate included in the delegation (if present) must not itself again contain a delegation. ::: + ``` -Delegation = - Delegation { +Delegation = { subnet_id : Principal; certificate : Certificate; } From da5caa432a1f843bb3e971da74a3121cb4520bef Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:11:22 +0200 Subject: [PATCH 03/14] feat: release v0.26.0 (#325) * feat: release v0.26.0 * add back unreleased --- spec/_attachments/interface-spec-changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/_attachments/interface-spec-changelog.md b/spec/_attachments/interface-spec-changelog.md index a2649853..3f874978 100644 --- a/spec/_attachments/interface-spec-changelog.md +++ b/spec/_attachments/interface-spec-changelog.md @@ -1,6 +1,8 @@ ## Changelog {#changelog} ### ∞ (unreleased) + +### 0.26.0 (2024-07-23) {#0_26_0} * EXPERIMENTAL: Management canister API for threshold Schnorr signatures. ### 0.25.0 (2024-06-14) {#0_25_0} From 22c6aa527a9c0e438c353b1f4eaaae5ee430c360 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 8 Aug 2024 12:52:19 -0400 Subject: [PATCH 04/14] Fix the anchor of canister_info (#330) Change from `ic-canister-info` to `ic-canister_info` to align with other management canister API anchors. --- spec/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/index.md b/spec/index.md index 45733c3b..4c561d7a 100644 --- a/spec/index.md +++ b/spec/index.md @@ -2185,7 +2185,7 @@ Indicates various information about the canister. It contains: Only the controllers of the canister or the canister itself can request its status. -### IC method `canister_info` {#ic-canister-info} +### IC method `canister_info` {#ic-canister_info} This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages. From 2b2c5766e4734f15857ab7be71a5e5acfd913b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Tackmann?= <54846571+Dfinity-Bjoern@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:03:57 +0200 Subject: [PATCH 05/14] chore: Move fetch_canister_logs to main management canister subsection (#328) * Move fetch_canister_logs to main management canister subsection * create IC Provisional API section --------- Co-authored-by: Martin Raszyk --- spec/index.md | 68 +++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/spec/index.md b/spec/index.md index 4c561d7a..27480409 100644 --- a/spec/index.md +++ b/spec/index.md @@ -2488,29 +2488,35 @@ A single metric entry is a record with the following fields: - `num_block_failures_total` (`nat64`): the number of failed block proposals by this node. -### IC method `provisional_create_canister_with_cycles` {#ic-provisional_create_canister_with_cycles} - -This method can be called by canisters as well as by external users via ingress messages. +### IC method `fetch_canister_logs` {#ic-fetch_canister_logs} -As a provisional method on development instances, the `provisional_create_canister_with_cycles` method is provided. It behaves as `create_canister`, but initializes the canister's balance with `amount` fresh cycles (using `DEFAULT_PROVISIONAL_CYCLES_BALANCE` if `amount = null`). If `specified_id` is provided, the canister is created under this id. Note that canister creation using `create_canister` or `provisional_create_canister_with_cycles` with `specified_id = null` can fail after calling `provisional_create_canister_with_cycles` with provided `specified_id`. In that case, canister creation should be retried. +This method can only be called by external users via non-replicated calls, i.e., it cannot be called by canisters, cannot be called via replicated calls, and cannot be called from composite query calls. -The optional `sender_canister_version` parameter can contain the caller's canister version. If provided, its value must be equal to `ic0.canister_version`. +:::note -Cycles added to this call via `ic0.call_cycles_add` and `ic0.call_cycles_add128` are returned to the caller. +The canister logs management canister API is considered EXPERIMENTAL. Canister developers must be aware that the API may evolve in a non-backward-compatible way. -This method is only available in local development instances. +::: -### IC method `provisional_top_up_canister` {#ic-provisional_top_up_canister} +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, CQ, CRy, CRt, CC, and F modes, as defined in [Overview of imports](#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. +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`: logs can be either public (visible to everyone) or only visible to the canister's controllers (by default). -This method can be called by canisters as well as by external users via ingress messages. +A single log is a record with the following fields: -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`. +- `idx` (`nat64`): the unique sequence number of the log for this particular canister; +- `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; -Cycles added to this call via `ic0.call_cycles_add` and `ic0.call_cycles_add128` are returned to the caller. +:::warning -Any user can top-up any canister this way. +The response of a query comes from a single replica, and is therefore not appropriate for security-sensitive applications. +Replica-signed queries may improve security because the recipient can verify the response comes from the correct subnet. -This method is only available in local development instances. +::: ## The IC Bitcoin API {#ic-bitcoin-api} @@ -2588,35 +2594,33 @@ This function returns fee percentiles, measured in millisatoshi/vbyte (1000 mill The [standard nearest-rank estimation method](https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method), inclusive, with the addition of a 0th percentile is used. Concretely, for any i from 1 to 100, the ith percentile is the fee with rank `⌈i * 100⌉`. The 0th percentile is defined as the smallest fee (excluding coinbase transactions). -### IC method `fetch_canister_logs` {#ic-fetch_canister_logs} +## The IC Provisional API {#ic-provisional-api} -This method can only be called by external users via non-replicated calls, i.e., it cannot be called by canisters, cannot be called via replicated calls, and cannot be called from composite query calls. +The IC Provisional API for creating canisters and topping up canisters out of thin air is only available in local development instances. -:::note +### IC method `provisional_create_canister_with_cycles` {#ic-provisional_create_canister_with_cycles} -The canister logs management canister API is considered EXPERIMENTAL. Canister developers must be aware that the API may evolve in a non-backward-compatible way. +This method can be called by canisters as well as by external users via ingress messages. -::: +As a provisional method on development instances, the `provisional_create_canister_with_cycles` method is provided. It behaves as `create_canister`, but initializes the canister's balance with `amount` fresh cycles (using `DEFAULT_PROVISIONAL_CYCLES_BALANCE` if `amount = null`). If `specified_id` is provided, the canister is created under this id. Note that canister creation using `create_canister` or `provisional_create_canister_with_cycles` with `specified_id = null` can fail after calling `provisional_create_canister_with_cycles` with provided `specified_id`. In that case, canister creation should be retried. -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, CQ, CRy, CRt, CC, and F modes, as defined in [Overview of imports](#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. -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`: logs can be either public (visible to everyone) or only visible to the canister's controllers (by default). +The optional `sender_canister_version` parameter can contain the caller's canister version. If provided, its value must be equal to `ic0.canister_version`. -A single log is a record with the following fields: +Cycles added to this call via `ic0.call_cycles_add` and `ic0.call_cycles_add128` are returned to the caller. -- `idx` (`nat64`): the unique sequence number of the log for this particular canister; -- `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; +This method is only available in local development instances. -:::warning +### IC method `provisional_top_up_canister` {#ic-provisional_top_up_canister} -The response of a query comes from a single replica, and is therefore not appropriate for security-sensitive applications. -Replica-signed queries may improve security because the recipient can verify the response comes from the correct subnet. +This method can be called by canisters as well as by external users via ingress messages. -::: +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. + +Any user can top-up any canister this way. + +This method is only available in local development instances. ## Certification {#certification} From 0a50e0c1eac44dbc0c02116ff110096a17c4c022 Mon Sep 17 00:00:00 2001 From: Thomas Locher Date: Tue, 20 Aug 2024 18:26:01 +0200 Subject: [PATCH 06/14] [FINAL] Adding an endpoint to fetch bitcoin block headers (#298) * Added the bitcoin block headers endpoint. * Updated to address comments. * Fixed a copy&paste error. * Added the description of an error case for bitcoin_get_block_headers. * Added the Bitcoin network parameter. --------- Co-authored-by: Martin Raszyk Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> --- spec/_attachments/ic.did | 58 +++++++++++++++++++++++++--------------- spec/index.md | 44 +++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/spec/_attachments/ic.did b/spec/_attachments/ic.did index 702b7ef4..2dc99cd8 100644 --- a/spec/_attachments/ic.did +++ b/spec/_attachments/ic.did @@ -90,7 +90,13 @@ type bitcoin_network = variant { type bitcoin_address = text; -type block_hash = blob; +type bitcoin_block_hash = blob; + +type bitcoin_block_header = blob; + +type millisatoshi_per_byte = nat64; + +type bitcoin_block_height = nat32; type outpoint = record { txid : blob; @@ -112,6 +118,13 @@ type bitcoin_get_utxos_args = record { }; }; +type bitcoin_get_utxos_result = record { + utxos : vec utxo; + tip_block_hash : bitcoin_block_hash; + tip_height : bitcoin_block_height; + next_page : opt blob; +}; + type bitcoin_get_utxos_query_args = record { address : bitcoin_address; network : bitcoin_network; @@ -121,21 +134,10 @@ type bitcoin_get_utxos_query_args = record { }; }; -type bitcoin_get_current_fee_percentiles_args = record { - network : bitcoin_network; -}; - -type bitcoin_get_utxos_result = record { - utxos : vec utxo; - tip_block_hash : block_hash; - tip_height : nat32; - next_page : opt blob; -}; - type bitcoin_get_utxos_query_result = record { utxos : vec utxo; - tip_block_hash : block_hash; - tip_height : nat32; + tip_block_hash : bitcoin_block_hash; + tip_height : bitcoin_block_height; next_page : opt blob; }; @@ -145,18 +147,37 @@ type bitcoin_get_balance_args = record { min_confirmations : opt nat32; }; +type bitcoin_get_balance_result = satoshi; + type bitcoin_get_balance_query_args = record { address : bitcoin_address; network : bitcoin_network; min_confirmations : opt nat32; }; +type bitcoin_get_balance_query_result = satoshi; + +type bitcoin_get_current_fee_percentiles_args = record { + network : bitcoin_network; +}; + +type bitcoin_get_current_fee_percentiles_result = vec millisatoshi_per_byte; + type bitcoin_send_transaction_args = record { transaction : blob; network : bitcoin_network; }; -type millisatoshi_per_byte = nat64; +type bitcoin_get_block_headers_args = record { + start_height : bitcoin_block_height; + end_height : opt bitcoin_block_height; + network: bitcoin_network; +}; + +type bitcoin_get_block_headers_result = record { + tip_height : bitcoin_block_height; + block_headers : vec bitcoin_block_header; +}; type node_metrics = record { node_id : principal; @@ -361,12 +382,6 @@ type stored_chunks_result = vec chunk_hash; type upload_chunk_result = chunk_hash; -type bitcoin_get_balance_result = satoshi; - -type bitcoin_get_balance_query_result = satoshi; - -type bitcoin_get_current_fee_percentiles_result = vec millisatoshi_per_byte; - type fetch_canister_logs_args = record { canister_id : canister_id; }; @@ -414,6 +429,7 @@ service ic : { bitcoin_get_utxos_query : (bitcoin_get_utxos_query_args) -> (bitcoin_get_utxos_query_result) query; bitcoin_send_transaction : (bitcoin_send_transaction_args) -> (); bitcoin_get_current_fee_percentiles : (bitcoin_get_current_fee_percentiles_args) -> (bitcoin_get_current_fee_percentiles_result); + bitcoin_get_block_headers : (bitcoin_get_block_headers_args) -> (bitcoin_get_block_headers_result); // metrics interface node_metrics_history : (node_metrics_history_args) -> (node_metrics_history_result); diff --git a/spec/index.md b/spec/index.md index 27480409..ce682f4d 100644 --- a/spec/index.md +++ b/spec/index.md @@ -76,7 +76,7 @@ The public entry points of canisters are called *methods*. Methods can be declar Methods can be *called*, from *caller* to *callee*, and will eventually incur a *response* which is either a *reply* or a *reject*. A method may have *parameters*, which are provided with concrete *arguments* in a method call. -External calls can be update calls, which can *only* call update and query methods, and query calls, which can *only* call query and composite query methods. Inter-canister calls issued while evaluating an update call can call update and query methods (just like update calls). Inter-canister calls issued while evaluating a query call (to a composite query method) can call query and composite query methods (just like query calls). Note that calls from a canister to itself also count as "inter-canister". Update and query call offer a security/efficiency trade-off. +External calls can be update calls, which can *only* call update and query methods, and query calls, which can *only* call query and composite query methods. Inter-canister calls issued while evaluating an update call can call update and query methods (just like update calls). Inter-canister calls issued while evaluating a query call (to a composite query method) can call query and composite query methods (just like query calls). Note that calls from a canister to itself also count as "inter-canister". Update and query call offer a security/efficiency trade-off. Update calls are executed in *replicated* mode, i.e. execution takes place in parallel on multiple replicas who need to arrive at a consensus on what the result of the call is. Query calls are fast but offer less guarantees since they are executed in *non-replicated* mode, by a single replica. Internally, a call or a response is transmitted as a *message* from a *sender* to a *receiver*. Messages do not have a response. @@ -109,11 +109,11 @@ This specification may refer to certain constants and limits without specifying - `CHUNK_STORE_SIZE` - Maximum number of chunks that can be stored within the chunk store of a canister. + Maximum number of chunks that can be stored within the chunk store of a canister. - `MAX_CHUNKS_IN_LARGE_WASM` - Maximum number of chunks that can comprise a large Wasm module. + Maximum number of chunks that can comprise a large Wasm module. - `DEFAULT_PROVISIONAL_CYCLES_BALANCE` @@ -448,7 +448,7 @@ The state tree contains information about all API boundary nodes (the source of Example: `api-bn1.example.com`. - `/api_boundary_nodes//ipv4_address` (text) - + Public IPv4 address of a node in the dotted-decimal notation. If no `ipv4_address` is available for the corresponding node, then this path does not exist. Example: `192.168.10.150`. @@ -479,12 +479,12 @@ The state tree contains information about the topology of the Internet Computer. - `/subnet//metrics` (blob) A collection of subnet-wide metrics related to this subnet's current resource usage and/or performance. The metrics are a CBOR map with the following fields: - + - `num_canisters` (`nat`): The number of canisters on this subnet. - `canister_state_bytes` (`nat`): The total size of the state in bytes taken by canisters on this subnet since this subnet was created. - `consumed_cycles_total` (`map`): The total number of cycles consumed by all current and deleted canisters on this subnet. It's a map of two values, a low part of type `nat` and a high part of type `opt nat`. - `update_transactions_total` (`nat`): The total number of transactions processed on this subnet since this subnet was created. - + :::note @@ -1884,11 +1884,11 @@ In the future, the IC might expose more performance counters. ### Replicated execution check {#system-api-replicated-execution-check} -The canister can check whether it is currently running in replicated or non replicated execution. +The canister can check whether it is currently running in replicated or non replicated execution. `ic0.in_replicated_execution : () -> (result: i32)` -Returns 1 if the canister is being run in replicated mode and 0 otherwise. +Returns 1 if the canister is being run in replicated mode and 0 otherwise. ### Controller check {#system-api-controller-check} @@ -2063,12 +2063,12 @@ The optional `sender_canister_version` parameter can contain the caller's canist This method can be called by canisters as well as by external users via ingress messages. Canisters have associated some storage space (hence forth chunk storage) where they can hold chunks of Wasm modules that are too lage to fit in a single message. This method allows the controllers of a canister (and the canister itself) to upload such chunks. The method returns the hash of the chunk that was stored. The size of each chunk must be at most 1MiB. The maximum number of chunks in the chunk store is `CHUNK_STORE_SIZE` chunks. The storage cost of each chunk is fixed and corresponds to storing 1MiB of data. - + ### IC method `clear_chunk_store` {#ic-clear_chunk_store} This method can be called by canisters as well as by external users via ingress messages. -Canister controllers (and the canister itself) can clear the entire chunk storage of a canister. +Canister controllers (and the canister itself) can clear the entire chunk storage of a canister. ### IC method `stored_chunks` {#ic-stored_chunks} @@ -2594,6 +2594,26 @@ This function returns fee percentiles, measured in millisatoshi/vbyte (1000 mill The [standard nearest-rank estimation method](https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method), inclusive, with the addition of a 0th percentile is used. Concretely, for any i from 1 to 100, the ith percentile is the fee with rank `⌈i * 100⌉`. The 0th percentile is defined as the smallest fee (excluding coinbase transactions). +### IC method `bitcoin_get_block_headers` {#ic-bitcoin_get_block_headers} + +:::note + +The `bitcoin_get_block_headers` endpoint is considered EXPERIMENTAL. Canister developers must be aware that this endpoint may evolve in a non-backward-compatible way. + +::: + +This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages. + +Given a start height, an optional end height, and a Bitcoin network (`mainnet` or `testnet`), the function returns the block headers in the provided range. The range is inclusive, i.e., the block headers at the start and end heights are returned as well. +An error is returned when an end height is specified that is greater than the tip height. + +If no end height is specified, all blocks until the tip height, i.e., the largest available height, are returned. However, if the range from the start height to the end height or the tip height is large, only a prefix of the requested block headers may be returned in order to bound the size of the response. + +The response is guaranteed to contain the block headers in order: if it contains any block headers, the first block header occurs at the start height, the second block header occurs at the start height plus one and so forth. + +The response is a record consisting of the tip height and the vector of block headers. +The block headers are 80-byte blobs in the [standard Bitcoin format](https://developer.bitcoin.org/reference/block_chain.html#block-headers). + ## The IC Provisional API {#ic-provisional-api} The IC Provisional API for creating canisters and topping up canisters out of thin air is only available in local development instances. @@ -4520,7 +4540,7 @@ S with #### IC Management Canister: Clear chunk store -The controller of a canister, or the canister itself can clear the chunk store of that canister. +The controller of a canister, or the canister itself can clear the chunk store of that canister. ```html @@ -6152,7 +6172,7 @@ Read response A record with - `{certificate: C}` - + The predicate `may_read_path_for_subnet` is defined as follows, implementing the access control outlined in [Request: Read state](#http-read-state): ``` From b53a7fc5d891e29b2e68970daf7518bfbc6b6f50 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:55:07 +0200 Subject: [PATCH 07/14] fix: update changelog (#331) --- spec/_attachments/interface-spec-changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/_attachments/interface-spec-changelog.md b/spec/_attachments/interface-spec-changelog.md index 3f874978..94e5d694 100644 --- a/spec/_attachments/interface-spec-changelog.md +++ b/spec/_attachments/interface-spec-changelog.md @@ -1,6 +1,7 @@ ## Changelog {#changelog} ### ∞ (unreleased) +* EXPERIMENTAL: Management canister API to fetch Bitcoin block headers. ### 0.26.0 (2024-07-23) {#0_26_0} * EXPERIMENTAL: Management canister API for threshold Schnorr signatures. From f2abc508044dc561bf6afd118c7001a3f58c6ae4 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:56:44 +0200 Subject: [PATCH 08/14] fix: chunk store of a deleted canister is dropped (#333) --- spec/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/index.md b/spec/index.md index ce682f4d..c70c0e19 100644 --- a/spec/index.md +++ b/spec/index.md @@ -5277,6 +5277,7 @@ S with canister_log_visibility[A.canister_id] = (deleted) canister_logs[A.canister_id] = (deleted) query_stats[A.canister_id] = (deleted) + chunk_store[A.canister_id] = (deleted) messages = Older_messages · Younger_messages · ResponseMessage { origin = M.origin From 3acb7956cd3baa62ef2dd41d7a05c29a25b19a62 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:57:09 +0200 Subject: [PATCH 09/14] chore(deps): bump webpack from 5.91.0 to 5.94.0 in /docusaurus (#334) Bumps [webpack](https://github.com/webpack/webpack) from 5.91.0 to 5.94.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.91.0...v5.94.0) --- updated-dependencies: - dependency-name: webpack dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docusaurus/package-lock.json | 43 ++++++++++-------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/docusaurus/package-lock.json b/docusaurus/package-lock.json index 001fe730..0bbc5e71 100644 --- a/docusaurus/package-lock.json +++ b/docusaurus/package-lock.json @@ -3569,24 +3569,6 @@ "@types/ms": "*" } }, - "node_modules/@types/eslint": { - "version": "8.56.7", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz", - "integrity": "sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -4058,10 +4040,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "peerDependencies": { "acorn": "^8" } @@ -5987,9 +5969,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -15231,20 +15213,19 @@ } }, "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", From 607b6ef162918b0d95235a813861f7a3ad0659e2 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:05:24 +0200 Subject: [PATCH 10/14] chore(deps): bump micromatch from 4.0.5 to 4.0.8 in /docusaurus (#336) --- docusaurus/package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docusaurus/package-lock.json b/docusaurus/package-lock.json index 0bbc5e71..9aaae4ab 100644 --- a/docusaurus/package-lock.json +++ b/docusaurus/package-lock.json @@ -11076,11 +11076,11 @@ ] }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { From 45a23a737ec37cb8c9b098e377af857b4b55abda Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:06:29 +0200 Subject: [PATCH 11/14] fix: remove bitcoin query API from candid file (#337) --- spec/_attachments/ic.did | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/spec/_attachments/ic.did b/spec/_attachments/ic.did index 2dc99cd8..60321cf5 100644 --- a/spec/_attachments/ic.did +++ b/spec/_attachments/ic.did @@ -125,22 +125,6 @@ type bitcoin_get_utxos_result = record { next_page : opt blob; }; -type bitcoin_get_utxos_query_args = record { - address : bitcoin_address; - network : bitcoin_network; - filter : opt variant { - min_confirmations : nat32; - page : blob; - }; -}; - -type bitcoin_get_utxos_query_result = record { - utxos : vec utxo; - tip_block_hash : bitcoin_block_hash; - tip_height : bitcoin_block_height; - next_page : opt blob; -}; - type bitcoin_get_balance_args = record { address : bitcoin_address; network : bitcoin_network; @@ -149,14 +133,6 @@ type bitcoin_get_balance_args = record { type bitcoin_get_balance_result = satoshi; -type bitcoin_get_balance_query_args = record { - address : bitcoin_address; - network : bitcoin_network; - min_confirmations : opt nat32; -}; - -type bitcoin_get_balance_query_result = satoshi; - type bitcoin_get_current_fee_percentiles_args = record { network : bitcoin_network; }; @@ -424,9 +400,7 @@ service ic : { // bitcoin interface bitcoin_get_balance : (bitcoin_get_balance_args) -> (bitcoin_get_balance_result); - bitcoin_get_balance_query : (bitcoin_get_balance_query_args) -> (bitcoin_get_balance_query_result) query; bitcoin_get_utxos : (bitcoin_get_utxos_args) -> (bitcoin_get_utxos_result); - bitcoin_get_utxos_query : (bitcoin_get_utxos_query_args) -> (bitcoin_get_utxos_query_result) query; bitcoin_send_transaction : (bitcoin_send_transaction_args) -> (); bitcoin_get_current_fee_percentiles : (bitcoin_get_current_fee_percentiles_args) -> (bitcoin_get_current_fee_percentiles_result); bitcoin_get_block_headers : (bitcoin_get_block_headers_args) -> (bitcoin_get_block_headers_result); From 72ed35be88ef5b791ffd34896d96874937ad7065 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:32:00 +0200 Subject: [PATCH 12/14] fix: canister memory usage (#338) --- spec/index.md | 316 ++++++++++++++------------------------------------ 1 file changed, 87 insertions(+), 229 deletions(-) diff --git a/spec/index.md b/spec/index.md index c70c0e19..c9ac1255 100644 --- a/spec/index.md +++ b/spec/index.md @@ -2986,6 +2986,7 @@ The [WebAssembly System API](#system-api) is relatively low-level, and some of i memory_allocation : Nat; memory_usage_raw_module : Nat; memory_usage_canister_history : Nat; + memory_usage_chunk_store : Nat; freezing_threshold : Nat; subnet_size : Nat; certificate : NoCertificate | Blob; @@ -3344,13 +3345,38 @@ The (unspecified) function `idle_cycles_burned_rate(compute_allocation, memory_a freezing_limit(compute_allocation, memory_allocation, freezing_threshold, memory_usage, subnet_size) = idle_cycles_burned_rate(compute_allocation, memory_allocation, memory_usage, subnet_size) * freezing_threshold / (24 * 60 * 60) ``` -The (unspecified) functions `memory_usage_wasm_state(wasm_state)`, `memory_usage_raw_module(raw_module)`, and `memory_usage_canister_history(canister_history)` determine the canister's memory usage in bytes consumed by its Wasm state, raw Wasm binary, and canister history, respectively. +The (unspecified) functions `memory_usage_wasm_state(wasm_state)`, `memory_usage_raw_module(raw_module)`, `memory_usage_canister_history(canister_history)`, and `memory_usage_chunk_store(chunk_store)` determine the canister's memory usage in bytes consumed by its Wasm state, raw Wasm binary, canister history, and chunk store, respectively. + +The freezing limit of canister `A` in state `S` can be obtained as follows: +``` +freezing_limit(S, A) = + freezing_limit( + S.compute_allocation[A], + S.memory_allocation[A], + S.freezing_threshold[A], + memory_usage_wasm_state(S.canisters[A].wasm_state) + + memory_usage_raw_module(S.canisters[A].raw_module) + + memory_usage_canister_history(S.canister_history[A]) + + memory_usage_chunk_store(S.chunk_store[A]), + S.canister_subnet[A].subnet_size, + ) +``` The amount of cycles that is available for spending in calls and execution is computed by the function `liquid_balance(balance, reserved_balance, freezing_limit)`: ``` liquid_balance(balance, reserved_balance, freezing_limit) = balance - max(freezing_limit - reserved_balance, 0) ``` +The "liquid" balance of canister `A` in state `S` can be obtained as follows: +``` +liquid_balance(S, A) = + liquid_balance( + S.balances[A], + S.reserved_balances[A], + freezing_limit(S, A), + ) +``` + The reasoning behind this is that resource payments first drain the reserved balance and only when the reserved balance gets to zero, they start draining the main balance. The amount of cycles that need to be reserved after operations that allocate resources is modeled with an unspecified function `cycles_to_reserve(S, CanisterId, compute_allocation, memory_allocation, CanState)` that depends on the old IC state, the id of the canister, the new allocations of the canister, and the new state of the canister. @@ -3517,25 +3543,14 @@ is_effective_canister_id(E.content, ECID) memory_allocation = S.memory_allocation[E.content.canister_id]; memory_usage_raw_module = memory_usage_raw_module(S.canisters[E.content.canister_id].raw_module); memory_usage_canister_history = memory_usage_canister_history(S.canister_history[E.content.canister_id]); + memory_usage_chunk_store = memory_usage_chunk_store(S.chunk_store[E.content.canister_id]); freezing_threshold = S.freezing_threshold[E.content.canister_id]; subnet_size = S.canister_subnet[E.content.canister_id].subnet_size; certificate = NoCertificate; status = simple_status(S.canister_status[E.content.canister_id]); canister_version = S.canister_version[E.content.canister_id]; } - liquid_balance( - S.balances[E.content.canister_id], - S.reserved_balances[E.content.canister_id], - freezing_limit( - S.compute_allocation[E.content.canister_id], - S.memory_allocation[E.content.canister_id], - S.freezing_threshold[E.content.canister_id], - memory_usage_wasm_state(S.canisters[E.content.canister_id].wasm_state) + - memory_usage_raw_module(S.canisters[E.content.canister_id].raw_module) + - memory_usage_canister_history(S.canister_history[E.content.canister_id]), - S.canister_subnet[E.content.canister_id].subnet_size, - ) - ) ≥ 0 + liquid_balance(S, E.content.canister_id) ≥ 0 S.canisters[E.content.canister_id].module.inspect_message (E.content.method_name, S.canisters[E.content.canister_id].wasm_state, E.content.arg, E.content.sender, Env) = Return {status = Accept;} ) @@ -3654,19 +3669,7 @@ Conditions S.messages = Older_messages · CallMessage CM · Younger_messages (CM.queue = Unordered) or (∀ CallMessage M' | FuncMessage M' ∈ Older_messages. M'.queue ≠ CM.queue) S.canisters[CM.callee] ≠ EmptyCanister -S.canister_status[CM.callee] = liquid_balance( - S.balances[CM.callee], - S.reserved_balances[CM.callee], - freezing_limit( - S.compute_allocation[CM.callee], - S.memory_allocation[CM.callee], - S.freezing_threshold[CM.callee], - memory_usage_wasm_state(S.canisters[CM.callee].wasm_state) + - memory_usage_raw_module(S.canisters[CM.callee].raw_module) + - memory_usage_canister_history(S.canister_history[CM.callee]), - S.canister_subnet[CM.callee].subnet_size, - ) -) < 0 +liquid_balance(S, CM.callee) < 0 ``` State after: @@ -3702,19 +3705,7 @@ S.messages = Older_messages · CallMessage CM · Younger_messages (CM.queue = Unordered) or (∀ CallMessage M' | FuncMessage M' ∈ Older_messages. M'.queue ≠ CM.queue) S.canisters[CM.callee] ≠ EmptyCanister S.canister_status[CM.callee] = Running -liquid_balance( - S.balances[CM.callee], - S.reserved_balances[CM.callee], - freezing_limit( - S.compute_allocation[CM.callee], - S.memory_allocation[CM.callee], - S.freezing_threshold[CM.callee], - memory_usage_wasm_state(S.canisters[CM.callee].wasm_state) + - memory_usage_raw_module(S.canisters[CM.callee].raw_module) + - memory_usage_canister_history(S.canister_history[CM.callee]), - S.canister_subnet[CM.callee].subnet_size, - ) -) ≥ MAX_CYCLES_PER_MESSAGE +liquid_balance(S, CM.callee) ≥ MAX_CYCLES_PER_MESSAGE Ctxt_id ∉ dom(S.call_contexts) ``` @@ -3754,19 +3745,7 @@ Conditions S.canisters[C] ≠ EmptyCanister S.canister_status[C] = Running -liquid_balance( - S.balances[C], - S.reserved_balance[C], - freezing_limit( - S.compute_allocation[C], - S.memory_allocation[C], - S.freezing_threshold[C], - memory_usage_wasm_state(S.canisters[C].wasm_state) + - memory_usage_raw_module(S.canisters[C].raw_module) + - memory_usage_canister_history(S.canister_history[C]), - S.canister_subnet[C].subnet_size, - ) -) ≥ MAX_CYCLES_PER_MESSAGE +liquid_balance(S, C) ≥ MAX_CYCLES_PER_MESSAGE Ctxt_id ∉ dom(S.call_contexts) ``` @@ -3807,19 +3786,7 @@ S.canisters[C] ≠ EmptyCanister S.canister_status[C] = Running S.global_timer[C] ≠ 0 S.time[C] ≥ S.global_timer[C] -liquid_balance( - S.balances[C], - S.reserved_balances[C], - freezing_limit( - S.compute_allocation[C], - S.memory_allocation[C], - S.freezing_threshold[C], - memory_usage_wasm_state(S.canisters[C].wasm_state) + - memory_usage_raw_module(S.canisters[C].raw_module) + - memory_usage_canister_history(S.canister_history[C]), - S.canister_subnet[C].subnet_size, - ) -) ≥ MAX_CYCLES_PER_MESSAGE +liquid_balance(S, C) ≥ MAX_CYCLES_PER_MESSAGE Ctxt_id ∉ dom(S.call_contexts) ``` @@ -3879,6 +3846,7 @@ Env = { memory_allocation = S.memory_allocation[M.receiver]; memory_usage_raw_module = memory_usage_raw_module(S.canisters[M.receiver].raw_module); memory_usage_canister_history = memory_usage_canister_history(S.canister_history[M.receiver]); + memory_usage_chunk_store = memory_usage_chunk_store(S.chunk_store[M.receiver]); freezing_threshold = S.freezing_threshold[M.receiver]; subnet_size = S.canister_subnet[M.receiver].subnet_size; certificate = NoCertificate; @@ -3944,7 +3912,8 @@ if S.freezing_threshold[M.receiver], memory_usage_wasm_state(res.new_state) + memory_usage_raw_module(S.canisters[M.receiver].raw_module) + - memory_usage_canister_history(S.canister_history[M.receiver]), + memory_usage_canister_history(S.canister_history[M.receiver]) + + memory_usage_chunk_store(S.chunk_store[M.receiver]), S.canister_subnet[M.receiver].subnet_size, ) New_reserved_balance ≤ S.reserved_balance_limits[M.receiver] @@ -3955,7 +3924,8 @@ if ) ≥ 0 (S.memory_allocation[M.receiver] = 0) or (memory_usage_wasm_state(res.new_state) + memory_usage_raw_module(S.canisters[M.receiver].raw_module) + - memory_usage_canister_history(S.canister_history[M.receiver]) ≤ S.memory_allocation[M.receiver]) + memory_usage_canister_history(S.canister_history[M.receiver]) + + memory_usage_chunk_store(S.chunk_store[M.receiver]) ≤ S.memory_allocation[M.receiver]) (Wasm_memory_limit = 0) or |res.new_state.store.mem| <= Wasm_memory_limit (res.response = NoResponse) or S.call_contexts[M.call_context].needs_to_respond then @@ -4200,18 +4170,7 @@ New_balance = M.transferred_cycles - Cycles_reserved New_reserved_balance = Cycles_reserved New_reserved_balance <= New_reserved_balance_limit if New_compute_allocation > 0 or New_memory_allocation > 0 or Cycles_reserved > 0: - liquid_balance( - New_balance, - New_reserved_balance, - freezing_limit( - New_compute_allocation, - New_memory_allocation, - New_freezing_threshold, - memory_usage_canister_history(New_canister_history), - SubnetSize, - ) - ) ≥ 0 - + liquid_balance(S', Canister_id) ≥ 0 New_canister_history = { total_num_changes = 1 @@ -4236,7 +4195,7 @@ State after ```html -S with +S' = S with canisters[Canister_id] = EmptyCanister time[Canister_id] = CurrentTime global_timer[Canister_id] = 0 @@ -4331,19 +4290,7 @@ New_balance = S.balances[A.canister_id] - Cycles_reserved New_reserved_balance = S.reserved_balances[A.canister_id] + Cycles_reserved New_reserved_balance ≤ New_reserved_balance_limit if New_compute_allocation > S.compute_allocation[A.canister_id] or New_memory_allocation > S.memory_allocation[A.canister_id] or Cycles_reserved > 0: - liquid_balance( - New_balance, - New_reserved_balance, - freezing_limit( - New_compute_allocation, - New_memory_allocation, - New_freezing_threshold, - memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) + - memory_usage_raw_module(S.canisters[A.canister_id].raw_module) + - memory_usage_canister_history(New_canister_history), - S.canister_subnet[A.canister_id].subnet_size, - ) - ) ≥ 0 + liquid_balance(S', A.canister_id) ≥ 0 S.canister_history[A.canister_id] = { total_num_changes = N; @@ -4370,7 +4317,7 @@ State after ```html -S with +S' = S with if A.settings.controllers is not null: controllers[A.canister_id] = A.settings.controllers canister_history[A.canister_id] = New_canister_history @@ -4444,7 +4391,8 @@ S with S.memory_allocation[A.canister_id], memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) + memory_usage_raw_module(S.canisters[A.canister_id].raw_module) + - memory_usage_canister_history(S.canister_history[A.canister_id]), + memory_usage_canister_history(S.canister_history[A.canister_id]) + + memory_usage_chunk_store(S.chunk_store[A.canister_id]), S.freezing_threshold[A.canister_id], S.canister_subnet[A.canister_id].subnet_size, ); @@ -4633,6 +4581,7 @@ Env = { memory_allocation = S.memory_allocation[A.canister_id]; memory_usage_raw_module = memory_usage_raw_module(A.wasm_module); memory_usage_canister_history = memory_usage_canister_history(New_canister_history); + memory_usage_chunk_store = memory_usage_chunk_store(New_chunk_store); freezing_threshold = S.freezing_threshold[A.canister_id]; subnet_size = S.canister_subnet[A.canister_id].subnet_size; certificate = NoCertificate; @@ -4645,38 +4594,15 @@ New_balance = S.balances[A.canister_id] - Cycles_used - Cycles_reserved New_reserved_balance = S.reserved_balances[A.canister_id] + Cycles_reserved New_reserved_balance ≤ S.reserved_balance_limits[A.canister_id] -liquid_balance( - S.balances[A.canister_id], - S.reserved_balances[A.canister_id], - freezing_limit( - S.compute_allocation[A.canister_id], - S.memory_allocation[A.canister_id], - S.freezing_threshold[A.canister_id], - memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) + - memory_usage_raw_module(S.canisters[A.canister_id].raw_module) + - memory_usage_canister_history(S.canister_history[A.canister_id]), - S.canister_subnet[A.canister_id].subnet_size, - ) -) ≥ MAX_CYCLES_PER_MESSAGE +liquid_balance(S, A.canister_id) ≥ MAX_CYCLES_PER_MESSAGE -liquid_balance( - New_balance, - New_reserved_balance, - freezing_limit( - S.compute_allocation[A.canister_id], - S.memory_allocation[A.canister_id], - S.freezing_threshold[A.canister_id], - memory_usage_wasm_state(New_state) + - memory_usage_raw_module(A.wasm_module) + - memory_usage_canister_history(New_canister_history), - S.canister_subnet[A.canister_id].subnet_size, - ) -) ≥ 0 +liquid_balance(S', A.canister_id) ≥ 0 if S.memory_allocation[A.canister_id] > 0: memory_usage_wasm_state(New_state) + memory_usage_raw_module(A.wasm_module) + - memory_usage_canister_history(New_canister_history) ≤ S.memory_allocation[A.canister_id] + memory_usage_canister_history(New_canister_history) + + memory_usage_chunk_store(New_chunk_store) ≤ S.memory_allocation[A.canister_id] (S.wasm_memory_limit[A.canister_id] = 0) or |New_state.store.mem| <= S.wasm_memory_limit[A.canister_id] @@ -4703,7 +4629,7 @@ State after ```html -S with +S' = S with canisters[A.canister_id] = { wasm_state = New_state; module = Mod; @@ -4764,6 +4690,7 @@ dom(Mod.query_methods) ∩ dom(Mod.composite_query_methods) = ∅ Env = { time = S.time[A.canister_id]; controllers = S.controllers[A.canister_id]; + global_timer = S.global_timer[A.canister_id]; balance = S.balances[A.canister_id]; reserved_balance = S.reserved_balances[A.canister_id]; reserved_balance_limit = S.reserved_balance_limits[A.canister_id]; @@ -4771,10 +4698,12 @@ Env = { memory_allocation = S.memory_allocation[A.canister_id]; memory_usage_raw_module = memory_usage_raw_module(S.canisters[A.canister_id].raw_module); memory_usage_canister_history = memory_usage_canister_history(S.canister_history[A.canister_id]); + memory_usage_chunk_store = memory_usage_chunk_store(S.chunk_store[A.canister_id]); freezing_threshold = S.freezing_threshold[A.canister_id]; subnet_size = S.canister_subnet[A.canister_id].subnet_size; certificate = NoCertificate; status = simple_status(S.canister_status[A.canister_id]); + canister_version = S.canister_version[A.canister_id]; } ( @@ -4825,38 +4754,15 @@ New_balance = S.balances[A.canister_id] - Cycles_used - Cycles_used' - Cycles_re New_reserved_balance = S.reserved_balances[A.canister_id] + Cycles_reserved New_reserved_balance ≤ S.reserved_balance_limits[A.canister_id] -liquid_balance( - S.balances[A.canister_id], - S.reserved_balances[A.canister_id], - freezing_limit( - S.compute_allocation[A.canister_id], - S.memory_allocation[A.canister_id], - S.freezing_threshold[A.canister_id], - memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) + - memory_usage_raw_module(S.canisters[A.canister_id].raw_module) + - memory_usage_canister_history(S.canister_history[A.canister_id]), - S.canister_subnet[A.canister_id].subnet_size, - ) -) ≥ MAX_CYCLES_PER_MESSAGE +liquid_balance(S, A.canister_id) ≥ MAX_CYCLES_PER_MESSAGE -liquid_balance( - New_balance, - New_reserved_balance, - freezing_limit( - S.compute_allocation[A.canister_id], - S.memory_allocation[A.canister_id], - S.freezing_threshold[A.canister_id], - memory_usage_wasm_state(New_state) + - memory_usage_raw_module(A.wasm_module) + - memory_usage_canister_history(New_canister_history), - S.canister_subnet[A.canister_id].subnet_size, - ) -) ≥ 0 +liquid_balance(S', A.canister_id) ≥ 0 if S.memory_allocation[A.canister_id] > 0: memory_usage_wasm_state(New_state) + memory_usage_raw_module(A.wasm_module) + - memory_usage_canister_history(New_canister_history) ≤ S.memory_allocation[A.canister_id] + memory_usage_canister_history(New_canister_history) + + memory_usage_chunk_store(S[A.canister_id].chunk_store) ≤ S.memory_allocation[A.canister_id] (S.wasm_memory_limit[A.canister_id] = 0) or |New_state.store.mem| <= S.wasm_memory_limit[A.canister_id] @@ -4882,7 +4788,7 @@ State after ```html -S with +S' = S with canisters[A.canister_id] = { wasm_state = New_state; module = Mod; @@ -5443,18 +5349,7 @@ else: New_reserved_balance = Cycles_reserved New_reserved_balance ≤ New_reserved_balance_limit if New_compute_allocation > 0 or New_memory_allocation > 0 or Cycles_reserved > 0: - liquid_balance( - New_balance, - New_reserved_balance, - freezing_limit( - New_compute_allocation, - New_memory_allocation, - New_freezing_threshold, - memory_usage_canister_history(New_canister_history), - SubnetSize, - ) - ) ≥ 0 - + liquid_balance(S', Canister_id) ≥ 0 New_canister_history { total_num_changes = 1 @@ -5479,7 +5374,7 @@ State after ```html -S with +S' = S with canisters[Canister_id] = EmptyCanister time[Canister_id] = CurrentTime global_timer[Canister_id] = 0 @@ -5951,6 +5846,7 @@ composite_query_helper(S, Cycles, Depth, Root_canister_id, Caller, Canister_id, then Cert := NoCertificate // no certificate available in query and composite query methods evaluated on canisters other than the target canister of the query call let Env = { time = S.time[Canister_id]; + controllers = S.controllers[Canister_id]; global_timer = S.global_timer[Canister_id]; balance = S.balances[Canister_id]; reserved_balance = S.reserved_balances[Canister_id]; @@ -5959,6 +5855,7 @@ composite_query_helper(S, Cycles, Depth, Root_canister_id, Caller, Canister_id, memory_allocation = S.memory_allocation[Canister_id]; memory_usage_raw_module = memory_usage_raw_module(S.canisters[Canister_id].raw_module); memory_usage_canister_history = memory_usage_canister_history(S.canister_history[Canister_id]); + memory_usage_chunk_store = memory_usage_chunk_store(S.chunk_store[Canister_id]); freezing_threshold = S.freezing_threshold[Canister_id]; subnet_size = S.canister_subnet[Canister_id].subnet_size; certificate = Cert; @@ -5972,19 +5869,7 @@ composite_query_helper(S, Cycles, Depth, Root_canister_id, Caller, Canister_id, then let W = S.canisters[Canister_id].wasm_state let F = if Method_name ∈ dom(Mod.query_methods) then Mod.query_methods[Method_name] else Mod.composite_query_methods[Method_name] - if liquid_balance( - S.balances[Canister_id], - S.reserved_balances[Canister_id], - freezing_limit( - S.compute_allocation[Canister_id], - S.memory_allocation[Canister_id], - S.freezing_threshold[Canister_id], - memory_usage_wasm_state(S.canisters[Canister_id].wasm_state) + - memory_usage_raw_module(S.canisters[Canister_id].raw_module) + - memory_usage_canister_history(S.canister_history[Canister_id]), - S.canister_subnet[Canister_id].subnet_size, - ) - ) < 0 + if liquid_balance(S, Canister_id) < 0 then Return (Reject (SYS_TRANSIENT, ), Cycles, S) let R = F(Arg, Caller, Env)(W) @@ -6290,6 +6175,22 @@ It is nonsensical to pass to an execution function a WebAssembly store `S` that ::: +The "liquid" balance of a canister with a given `ExecutionState` can be obtained as follows: +``` +liquid_balance(es) = + 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.memory_usage_chunk_store, + es.params.sysenv.subnet_size, + ) + ) +``` + - For more convenience when creating a new `ExecutionState`, we define the following partial records: ``` empty_params = { @@ -6838,20 +6739,7 @@ ic0.msg_cycles_accept128(max_amount_high : i64, max_amount_low : i64, dst : ic0.cycles_burn128(amount_high : i64, amount_low : i64, dst : i32) = if es.context ∉ {I, G, U, 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.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, - ) - ) - ) + let burned_amount = min(amount, liquid_balance(es)) es.balance := es.balance - burned_amount copy_cycles_to_canister(dst, burned_amount.to_little_endian_bytes()) @@ -6940,36 +6828,16 @@ ic0.call_data_append (src : i32, size : i32) = ic0.call_cycles_add(amount : i64) = 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;} - if 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, - ) - ) < amount then Trap {cycles_used = es.cycles_used;} + if liquid_balance(es) < amount then Trap {cycles_used = es.cycles_used;} es.balance := es.balance - amount es.pending_call.transferred_cycles := es.pending_call.transferred_cycles + amount - ic0.call_cycles_add128(amount_high : i64, amount_low : i64) = - 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 amount = amount_high * 2^64 + amount_low - if 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, - ) - ) < amount then Trap {cycles_used = es.cycles_used;} +ic0.call_cycles_add128(amount_high : i64, amount_low : i64) = + 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 amount = amount_high * 2^64 + amount_low + if liquid_balance(es) < amount then Trap {cycles_used = es.cycles_used;} es.balance := es.balance - amount es.pending_call.transferred_cycles := es.pending_call.transferred_cycles + amount @@ -6982,17 +6850,7 @@ ic0.call_peform() : ( err_code : i32 ) = // are we below the freezing threshold? // Or maybe the system has other reasons to not perform this - if 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, - ) - ) < 0 or system_cannot_do_this_call_now() + if liquid_balance(es) < 0 or system_cannot_do_this_call_now() then discard_pending_call() return From c28afe4269b14f102c78b69225b7c380b826cf0f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi <40335219+DSharifi@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:37:21 +0200 Subject: [PATCH 13/14] feat(sync-call): [IC-1666] Endpoint for synchronous call requests (#265) * . * First draft completed * . * . * . * . * . * . * Apply suggestions from code review Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> * Added need for polling for non happy path flows * Removed note * Apply suggestions from code review Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> Co-authored-by: Alin Sinpalean <58422065+alin-at-dfinity@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> * Removed enumeration of steps, and made reply/reject share `200` status code * Move back note to async call * . * Added `status` field and updated where "/call" is found * Moved request status to the reply section. * Removed "asynchronous" categorization for call requests * reformulate * . * Changed deadline to 1 execution round * . * Renamed to v3 for api path * . * . * , * . * . * . * . * . * . * . * . * . * . * . * . * Apply suggestions from code review Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> * Removed too specific status for pre-execution errors * Changed state to status for transition * 202 empty body, and no certificate * clarify * changelog --------- Co-authored-by: mraszyk <31483726+mraszyk@users.noreply.github.com> Co-authored-by: Alin Sinpalean <58422065+alin-at-dfinity@users.noreply.github.com> Co-authored-by: Martin Raszyk --- spec/_attachments/interface-spec-changelog.md | 1 + spec/index.md | 80 ++++++++++++++++--- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/spec/_attachments/interface-spec-changelog.md b/spec/_attachments/interface-spec-changelog.md index 94e5d694..aa6890b0 100644 --- a/spec/_attachments/interface-spec-changelog.md +++ b/spec/_attachments/interface-spec-changelog.md @@ -2,6 +2,7 @@ ### ∞ (unreleased) * EXPERIMENTAL: Management canister API to fetch Bitcoin block headers. +* Synchronous update call API at `/api/v3/canister/.../call`. ### 0.26.0 (2024-07-23) {#0_26_0} * EXPERIMENTAL: Management canister API for threshold Schnorr signatures. diff --git a/spec/index.md b/spec/index.md index c9ac1255..6c9f8c44 100644 --- a/spec/index.md +++ b/spec/index.md @@ -498,7 +498,7 @@ Because this uses the lexicographic ordering of princpials, and the byte disting ### Request status {#state-tree-request-status} -For each asynchronous request known to the Internet Computer, its status is in a subtree at `/request_status/`. Please see [Overview of canister calling](#http-call-overview) for more details on how asynchronous requests work. +For each update call request known to the Internet Computer, its status is in a subtree at `/request_status/`. Please see [Overview of canister calling](#http-call-overview) for more details on how update call requests work. - `/request_status//status` (text) @@ -558,9 +558,11 @@ Users have the ability to learn about the hash of the canister's module, its cur ## HTTPS Interface {#http-interface} -The concrete mechanism that users use to send requests to the Internet Computer is via an HTTPS API, which exposes three endpoints to handle interactions, plus one for diagnostics: +The concrete mechanism that users use to send requests to the Internet Computer is via an HTTPS API, which exposes four endpoints to handle interactions, plus one for diagnostics: -- At `/api/v2/canister//call` the user can submit (asynchronous, potentially state-changing) calls. +- At `/api/v2/canister//call` the user can submit update calls that are asynchronous and might change the IC state. + +- At `/api/v3/canister//call` the user can submit update calls and get a synchronous HTTPS response with a certificate for the call status. - At `/api/v2/canister//read_state` or `/api/v2/subnet//read_state` the user can read various information about the state of the Internet Computer. In particular, they can poll for the status of a call here. @@ -570,7 +572,7 @@ The concrete mechanism that users use to send requests to the Internet Computer In these paths, the `` is the [textual representation](#textual-ids) of the [*effective* canister id](#http-effective-canister-id). -Requests to `/api/v2/canister//call`, `/api/v2/canister//read_state`, `/api/v2/subnet//read_state`, and `/api/v2/canister//query` are POST requests with a CBOR-encoded request body, which consists of a authentication envelope (as per [Authentication](#authentication)) and request-specific content as described below. +Requests to `/api/v2/canister//call`, `/api/v3/canister//call`, `/api/v2/canister//read_state`, `/api/v2/subnet//read_state`, and `/api/v2/canister//query` are POST requests with a CBOR-encoded request body, which consists of a authentication envelope (as per [Authentication](#authentication)) and request-specific content as described below. :::note @@ -580,7 +582,13 @@ This document does not yet explain how to find the location and port of the Inte ### Overview of canister calling {#http-call-overview} -Users interact with the Internet Computer by calling canisters. By the very nature of a blockchain protocol, they cannot be acted upon immediately, but only with a delay. Moreover, the actual node that the user talks to may not be honest or, for other reasons, may fail to get the request on the way. This implies the following high-level workflow: +Users interact with the Internet Computer by calling canisters. By the very nature of a blockchain protocol, they cannot be acted upon immediately, but only with a delay. Moreover, the actual node that the user talks to may not be honest or, for other reasons, may fail to get the request on the way. + +The Internet Computer has two HTTPS APIs for canister calling: +- [*Asynchronous*](#http-async-call-overview) canister calling, where the user must poll the Internet Computer for the status of the canister call by _separate_ HTTPS requests. +- [*Synchronous*](#http-sync-call-overview) canister calling, where the status of the canister call is in the response of the original HTTPS request. + +#### Asynchronous canister calling {#http-async-call-overview} 1. A user submits a call via the [HTTPS Interface](#http-interface). No useful information is returned in the immediate response (as such information cannot be trustworthy anyways). @@ -639,8 +647,60 @@ Calls must stay in `replied` or `rejected` long enough for polling users to catc When asking the IC about the state or call of a request, the user uses the request id (see [Request ids](#request-id)) to read the request status (see [Request status](#state-tree-request-status)) from the state tree (see [Request: Read state](#http-read-state)). +#### Synchronous canister calling {#http-sync-call-overview} + +A synchronous update call, also known as a "call and await", is a type of update call where the replica will attempt to respond to the HTTPS request with a certificate of the call status. If the returned certificate indicates that the update call is in a terminal state (`replied`, `rejected`, or `done`), then the user __does not need to poll__ (using [`read_state`](#http-read-state) requests) to determine the result of the call. A terminal state means the call has completed its execution. + +The synchronous call endpoint is useful for users as it removes the networking overhead of polling the IC to determine the status of their call. + +The replica will maintain the HTTPS connection for the request and will respond once the call status transitions to a terminal state. + +If an implementation specific timeout for the request is reached while the replica waits for the terminal state, then the replica will reply with an empty body and a 202 HTTP status code. In such cases, the user should use [`read_state`](#http-read-state) to determine the status of the call. + ### Request: Call {#http-call} +In order to call a canister, the user makes a POST request to `/api/v3/canister//call`. The request body consists of an authentication envelope with a `content` map with the following fields: + +- `request_type` (`text`): Always `call` + +- `sender`, `nonce`, `ingress_expiry`: See [Authentication](#authentication) + +- `canister_id` (`blob`): The principal of the canister to call. + +- `method_name` (`text`): Name of the canister method to call. + +- `arg` (`blob`): Argument to pass to the canister method. + +The HTTP response to this request can have the following forms: + +- 200 HTTP status with a non-empty body. This status is returned if the canister call completed within an implementation-specific timeout or was rejected within an implementation-specific timeout. + + - If the update call completed, a certificate for the state of the update call is produced, and returned in a CBOR (see [CBOR](#cbor)) map with the fields specified below: + + - `status` (`text`): `"certified_state"` + + - `reply` (`blob`): A certificate (see [Certification](#certification)) with subtrees at `/request_status/` and `/time`, where `` is the [request ID](#request-id) of the update call. See [Request status](#state-tree-request-status) for more details on the request status. + + - If a non-replicated pre-processing error occurred (e.g., due to the [canister inspect message](#system-api-inspect-message)), then a body with information about the IC specific error encountered is returned. The body is a CBOR map with the following fields: + + - `status` (`text`): `"non_replicated_rejection"` + + - `reject_code` (`nat`): The reject code (see [Reject codes](#reject-codes)). + + - `reject_message` (`text`): a textual diagnostic message. + + - `error_code` (`text`): an optional implementation-specific textual error code (see [Error codes](#error-codes)). + +- 202 HTTP status with an empty body. This status is returned if an implementation-specific timeout is reached before the canister call completes. Users should use [`read_state`](#http-read-state) to determine the status of the call. + +- 4xx HTTP status for client errors (e.g. malformed request). Except for 429 HTTP status, retrying the request will likely have the same outcome. + +- 5xx HTTP status when the server has encountered an error or is otherwise incapable of performing the request. The request might succeed if retried at a later time. + +This request type can *also* be used to call a query method (but not a composite query method). A user may choose to go this way, instead of via the faster and cheaper [Request: Query call](#http-query) below, if they want to get a *certified* response. Note that the canister state will not be changed by sending a call request type for a query method (except for cycle balance change due to message execution). + +### Request: Asynchronous Call {#http-async-call} + In order to call a canister, the user makes a POST request to `/api/v2/canister//call`. The request body consists of an authentication envelope with a `content` map with the following fields: - `request_type` (`text`): Always `call` @@ -899,7 +959,7 @@ All requests coming in via the HTTPS interface need to be either *anonymous* or - `nonce` (`blob`, optional): Arbitrary user-provided data of length at most 32 bytes, typically randomly generated. This can be used to create distinct requests with otherwise identical fields. -- `ingress_expiry` (`nat`, required): An upper limit on the validity of the request, expressed in nanoseconds since 1970-01-01 (like [ic0.time()](#system-api-time)). This avoids replay attacks: The IC will not accept requests, or transition requests from status `received` to status `processing`, if their expiry date is in the past. The IC may refuse to accept requests with an ingress expiry date too far in the future. This applies to synchronous and asynchronous requests alike (and could have been called `request_expiry`). +- `ingress_expiry` (`nat`, required): An upper limit on the validity of the request, expressed in nanoseconds since 1970-01-01 (like [ic0.time()](#system-api-time)). This avoids replay attacks: The IC will not accept requests, or transition requests from status `received` to status `processing`, if their expiry date is in the past. The IC may refuse to accept requests with an ingress expiry date too far in the future. This applies not only to update calls, but all requests alike (and could have been called `request_expiry`). - `sender` (`Principal`, required): The user who issued the request. @@ -3152,9 +3212,9 @@ A reference implementation would likely maintain a separate list of `messages` f #### API requests -We distinguish between the *asynchronous* API requests (type `Request`) passed to `/api/v2/…/call`, which may be present in the IC state, and the *synchronous* API requests passed to `/api/v2/…/read_state` and `/api/v2/…/query`, which are only ephemeral. +We distinguish between API requests (type `Request`) passed to `/api/v2/…/call` and `/api/v3/…/call`, which may be present in the IC state, and the *read-only* API requests passed to `/api/v2/…/read_state` and `/api/v2/…/query`, which are only ephemeral. -These are the synchronous read messages: +These are the read-only messages: ``` Path = List(Blob) APIReadRequest @@ -3453,7 +3513,7 @@ The following is an incomplete list of invariants that should hold for the abstr Based on this abstract notion of the state, we can describe the behavior of the IC. There are three classes of behaviors: -- Asynchronous API requests that are submitted via `/api/v2/…/call`. These transitions describe checks that the request must pass to be considered received. +- Potentially state changing API requests that are submitted via `/api/v2/…/call` and `/api/v3/…/call`. These transitions describe checks that the request must pass to be considered received. - Spontaneous transitions that model the internal behavior of the IC, by describing conditions on the state that allow the transition to happen, and the state after. @@ -3497,7 +3557,7 @@ is_effective_canister_id(Request {canister_id = p, …}, p), if p ≠ ic_princip ``` #### API Request submission -After a node accepts a request via `/api/v2/canister//call`, the request gets added to the IC state as `Received`. +After a node accepts a request via `/api/v2/canister//call` or `/api/v3/canister//call`, the request gets added to the IC state as `Received`. This may only happen if the signature is valid and is created with a correct key. Due to this check, the envelope is discarded after this point. From 737838e2d9f7445aa67b8878ddfa2af8b587b8d0 Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:43:38 +0200 Subject: [PATCH 14/14] chore(deps): bump express from 4.19.2 to 4.21.0 in /docusaurus (#341) Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.21.0. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docusaurus/package-lock.json | 100 ++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/docusaurus/package-lock.json b/docusaurus/package-lock.json index 9aaae4ab..bd61ca59 100644 --- a/docusaurus/package-lock.json +++ b/docusaurus/package-lock.json @@ -4429,9 +4429,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -4441,7 +4441,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -5961,9 +5961,9 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -6279,36 +6279,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -6344,9 +6344,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/express/node_modules/range-parser": { "version": "1.2.1", @@ -6503,12 +6503,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -9188,9 +9188,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -11331,9 +11334,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12476,11 +12482,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -13670,9 +13676,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -13705,6 +13711,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -13817,14 +13831,14 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0"