diff --git a/docs/references/ic-interface-spec/abstract-behavior.md b/docs/references/ic-interface-spec/abstract-behavior.md index f5070a79..13bdd847 100644 --- a/docs/references/ic-interface-spec/abstract-behavior.md +++ b/docs/references/ic-interface-spec/abstract-behavior.md @@ -5711,6 +5711,10 @@ ic0.subnet_self_copy(dst : I, offset : I, size : I) = if es.context = s then Trap {cycles_used = es.cycles_used;} copy_to_canister(dst, offset, size, es.params.sysenv.subnet_id) +ic0.subnet_self_node_count() : i32 = + if es.context = s then Trap {cycles_used = es.cycles_used;} + return es.params.sysenv.subnet_size + ic0.canister_cycle_balance() : i64 = if es.context = s then Trap {cycles_used = es.cycles_used;} if es.balance >= 2^64 then Trap {cycles_used = es.cycles_used;} @@ -5972,6 +5976,13 @@ I ∈ {i32, i64} ic0.cost_http_request(request_size: i64, max_res_bytes: i64, dst: I) : () = copy_cycles_to_canister(dst, arbitrary()) +I ∈ {i32, i64} +ic0.cost_http_request_v2(params_src : I, params_size : I, dst : I) : ()= + params = copy_from_canister(params_src, params_size) + if params is not a valid Candid encoding of an HTTP outcall cost parameter record then + Trap {cycles_used = es.cycles_used;} + copy_cycles_to_canister(dst, arbitrary()) + I ∈ {i32, i64} ic0.cost_sign_with_ecdsa(src: I, size: I, ecdsa_curve: i32, dst: I) : i32 = known_keys = arbitrary() diff --git a/docs/references/ic-interface-spec/canister-interface.md b/docs/references/ic-interface-spec/canister-interface.md index 4617b98a..59756a98 100644 --- a/docs/references/ic-interface-spec/canister-interface.md +++ b/docs/references/ic-interface-spec/canister-interface.md @@ -217,6 +217,12 @@ The 32-bit stable memory System API (`ic0.stable_size`, `ic0.stable_grow`, `ic0. ::: +:::note + +The `ic0.cost_http_request` System API call is DEPRECATED. Canister developers are advised to use the `ic0.cost_http_request_v2` call instead. + +::: + The following sections describe various System API functions, also referred to as system calls, which we summarize here. All the following functions belong to the `ic0` module (denoted by the prefix `ic0.`). @@ -261,6 +267,7 @@ defaulting to `I = i32` if the canister declares no memory. ic0.subnet_self_size : () -> I; // * ic0.subnet_self_copy : (dst : I, offset : I, size : I) -> (); // * + ic0.subnet_self_node_count : () -> i32; // * ic0.msg_method_name_size : () -> I; // F ic0.msg_method_name_copy : (dst : I, offset : I, size : I) -> (); // F @@ -303,6 +310,7 @@ defaulting to `I = i32` if the canister declares no memory. ic0.cost_call : (method_name_size: i64, payload_size : i64, dst : I) -> (); // * s ic0.cost_create_canister : (dst : I) -> (); // * s ic0.cost_http_request : (request_size : i64, max_res_bytes : i64, dst : I) -> (); // * s + ic0.cost_http_request_v2 : (params_src : I, params_size : I, dst : I) -> (); // * s ic0.cost_sign_with_ecdsa : (src : I, size : I, ecdsa_curve: i32, dst : I) -> i32; // * s ic0.cost_sign_with_schnorr : (src : I, size : I, algorithm: i32, dst : I) -> i32; // * s ic0.cost_vetkd_derive_key : (src : I, size : I, vetkd_curve: i32, dst : I) -> i32; // * s @@ -505,9 +513,9 @@ A canister can learn about its own identity: A canister can learn about the subnet it is running on: -- `ic0.subnet_self_size : () → I` and `ic0.subnet_self_copy: (dst : I, offset : I, size : I) → ()`; `I ∈ {i32, i64}` +- `ic0.subnet_self_size : () → I`, `ic0.subnet_self_copy: (dst : I, offset : I, size : I) → ()`; `I ∈ {i32, i64}`, and `ic0.subnet_self_node_count : () -> i32` - These functions allow the canister to query the subnet id (as a blob) of the subnet on which the canister is running. + These functions allow the canister to query the subnet id (as a blob) of the subnet on which the canister is running, and to retrieve the number of nodes that are currently on the subnet. ### Canister status {#system-api-canister-status} @@ -912,7 +920,13 @@ These system calls return costs in Cycles, represented by 128 bits, which will b - `ic0.cost_http_request(request_size : i64, max_res_bytes : i64, dst : I) -> ()`; `I ∈ {i32, i64}` - The cost of a canister http outcall via [`http_request`](./management-canister.md#ic-http_request). `request_size` is the sum of the byte lengths of the following components of an http request: + :::note + + The `ic0.cost_http_request` System API call is DEPRECATED. Canister developers are advised to use the `ic0.cost_http_request_v2` call instead. + + ::: + + The cost of a canister HTTP outcall via [`http_request`](./management-canister.md#ic-http_request) with the pricing version set to `1` (currently the default). `request_size` is the sum of the byte lengths of the following components of an http request: - url - headers - i.e., the sum of the lengths of all keys and values - body @@ -920,6 +934,47 @@ These system calls return costs in Cycles, represented by 128 bits, which will b `max_res_bytes` is the maximum response length the caller wishes to accept (the caller should provide the default value of `2,000,000` if no maximum response length is provided in the actual request to the management canister). +- `ic0.cost_http_request_v2(params_src: I, params_size: I, dst : I) -> (); I ∈ {i32, i64}` + + The cost of a canister HTTP outcall, either via [`http_request`](./management-canister.md#ic-http_request) with the pricing version set to `2`, or via [`flexible_http_request`](./management-canister.md#ic-flexible_http_request), which takes no pricing version argument and is priced this way. The blob described by `params_src` and `params_size` must be a valid Candid encoding of a value of the following type: + ``` + record { + request_bytes : nat64; + http_roundtrip_time_ms : nat64; + raw_response_bytes : nat64; + transformed_response_bytes : nat64; + transform_instructions: nat64; + outcall_type : opt variant { + fully_replicated: reserved; + non_replicated: reserved; + flexible: opt record { + min_responses: nat32; + max_responses: nat32; + total_requests: nat32; + } + } + } + ``` + + The function traps if `params_src` and `params_size` do not describe a valid Candid encoding of a value of the above type. Beyond that type, decoding may skip only a very small, fixed amount of data, so the payload of the `fully_replicated` and `non_replicated` variants must be encoded as `null` and the encoding must not carry record fields other than the ones above; an encoding that violates either of these may trap. Similarly, the function also traps if the given blob is too large. + + The function returns the amount of cycles to attach to an HTTP outcall in which every participating node consumes exactly the amount of resources specified by the individual fields. Part of this amount is a _reservation_ rather than a charge: every node the outcall is assigned to is assumed to attempt it, and enough is reserved to fund whichever result ends up being delivered, including a reject delivered in place of the response that was asked for. Whatever is not spent is refunded (see [`http_request`](./management-canister.md#ic-http_request)), so the actual cost of such an outcall may be less than this system call predicts, but assuming parameters are accurate, it cannot be more. The individual fields are: + - `request_bytes` is the sum of the byte lengths of the following components of an HTTP request: + - `url` + - `headers` - i.e., the sum of the lengths of all keys and values + - `body` + - `transform` - i.e., the sum of the transform method name length and the length of the transform context. + + - `http_roundtrip_time_ms` is the amount of time between the time when the HTTP request starts being sent to the remote server and the time that the HTTP response is fully received (in milliseconds). + + - `raw_response_bytes` is the length of the HTTP response. + + - `transformed_response_bytes` is the length of the HTTP response after transformation. + + - `transform_instructions` is the number of instructions the transform function takes. + + - `outcall_type` is the type of HTTP outcall issued: a fully replicated call (made through the `http_request` endpoint with `is_replicated` set to `null` or `opt true`), non-replicated (made through `http_request` with `is_replicated` set to `opt false`), or flexible (made through the [`flexible_http_request`](./management-canister.md#ic-flexible_http_request) endpoint). If `outcall_type` is absent, the cost of a fully replicated call is returned. When the `flexible` outcall variant is selected, it can optionally be supplemented with the `min_responses`, `max_responses`, and `total_requests` parameters provided to the endpoint; if that record is omitted, the endpoint's own defaults of `floor(2 / 3 * N) + 1`, `N` and `N` are used, where `N` is the number of the nodes on the caller's subnet. Unlike the endpoint, this System API call does not validate the counts: a combination that `flexible_http_request` would reject simply yields a price that no outcall will ever be charged. + - `ic0.cost_sign_with_ecdsa(src : I, size : I, ecdsa_curve: i32, dst : I) -> i32`; `I ∈ {i32, i64}` - `ic0.cost_sign_with_schnorr(src : I, size : I, algorithm: i32, dst : I) -> i32`; `I ∈ {i32, i64}` diff --git a/docs/references/ic-interface-spec/management-canister.md b/docs/references/ic-interface-spec/management-canister.md index 7fa8980f..d55a4451 100644 --- a/docs/references/ic-interface-spec/management-canister.md +++ b/docs/references/ic-interface-spec/management-canister.md @@ -691,7 +691,7 @@ The following parameters should be supplied for the call: - `url` - the requested URL. The URL must be valid according to [RFC-3986](https://www.ietf.org/rfc/rfc3986.txt), it might contain non-ASCII characters according to [RFC-3987](https://www.ietf.org/rfc/rfc3987.txt), and its length must not exceed `8192`. The URL may specify a custom port number. -- `max_response_bytes` - optional, specifies the maximal size of the response in bytes. If provided, the value must not exceed `2MB` (`2,000,000B`). The call will be charged based on this parameter. If not provided, the maximum of `2MB` will be used. +- `max_response_bytes` - optional, specifies the maximal size of the response in bytes. If provided, the value must not exceed `2MB` (`2,000,000B`). If not provided, the maximum of `2MB` will be used. The limit applies for both pricing versions, and is enforced on the response received from the remote server as well as on the response produced by the `transform` function. Only pricing version `1` ("legacy") also charges the call upfront based on this parameter; with pricing version `2` ("pay-as-you-go"), the call is charged only for the resources it actually consumes. - `method` - currently, `GET`, `HEAD`, and `POST` are supported. Additionally, `PUT`, `DELETE`, and `PATCH` are supported in non-replicated mode only. @@ -701,15 +701,23 @@ The following parameters should be supplied for the call: - `transform` - an optional record that includes a function that transforms raw responses to sanitized responses, and a byte-encoded context that is provided to the function upon invocation, along with the response to be sanitized. If provided, the calling canister itself must export this function -- `is_replicated` - optional, selecting between replicated and non-replicated modes. +- `is_replicated` - optional, selecting between replicated and non-replicated modes. Setting the field to `opt false` selects the non-replicated mode, in which a single node chosen by the system performs the request. Setting it to `opt true`, or omitting it, selects the replicated mode. -:::note + :::note -The `is_replicated` field is considered EXPERIMENTAL. + The `is_replicated` field is considered EXPERIMENTAL. -::: + ::: -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). +- `pricing_version` - optional, the version of the pricing mechanism for HTTP outcalls that should be applied to this call; it can be either `1` ("legacy") or `2` ("pay-as-you-go"). For compatibility reasons, the default is `1`; however, version `1` is deprecated. If the field is omitted, set to a version the subnet does not support, or set to any other value, the call is priced with version `1` and no error is reported. Note that pricing version `1` does not take the replication mode into account, so a non-replicated call is charged the same as a replicated one with the same request size and `max_response_bytes`; only version `2` prices a call according to its replication mode. + +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). How many cycles must be attached, and what is refunded, depends on the pricing version: + +- with pricing version `1`, the call is rejected unless the attached cycles cover the cost returned by the `ic0.cost_http_request` API with the appropriate parameters; the difference between the attached cycles and that cost is refunded. + +- with pricing version `2`, the call is rejected unless the attached cycles cover a base fee that depends on the request and is charged when the call is accepted. Any attached cycles exceeding those used by the outcall execution are refunded. + +The cycles attached beyond the base fee of a pricing version `2` call are not merely a payment: They are withheld and split evenly into a budget for each node performing the outcall, and each node's remaining budget bounds the response it may download, the time it may wait for it, and the number of instructions its execution of the `transform` function may use. A call that covers the base fee but is funded below the amount reported by the `ic0.cost_http_request_v2` API for the resources it will use, is therefore not rejected up front: it runs with reduced limits, and a node that exhausts its budget produces a `CANISTER_REJECT` response instead of the response it was asked for. If what the nodes leave unspent no longer covers delivering any response at all, the call is answered with a `SYS_TRANSIENT` reject; since the cost of delivering a response depends on its size, this can happen after the remote server has already been contacted. The unspent part of the per-node budgets is credited to the caller's cycles balance asynchronously, separately from the refund that accompanies the response. The returned response (and the response provided to the `transform` function, if specified) contains the following fields: @@ -744,10 +752,64 @@ The Internet Computer mainnet supports requests to both IPv6 and IPv4 destinatio :::warning -If you do not specify the `max_response_bytes` parameter, the maximum of a `2MB` response will be charged for, which is expensive in terms of cycles. Always set the parameter to a reasonable upper bound of the expected (network and transformed) response size to not incur unnecessary cycles costs for your request. +With pricing version `1`, if you do not specify the `max_response_bytes` parameter, the maximum of a `2MB` response will be charged for, which is expensive in terms of cycles. Always set the parameter to a reasonable upper bound of the expected (network and transformed) response size to not incur unnecessary cycles costs for your request. ::: +### IC method `flexible_http_request` {#ic-flexible_http_request} + +This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages. + +This is a variant of the [`http_request`](#ic-http_request) method where nodes return their individual HTTP responses to the caller instead of trying to reach consensus on the response, letting the caller do its own HTTP response processing. Use cases include calling HTTP endpoints that provide rapidly changing information (where achieving consensus is unlikely) and letting the user pick a trade-off between cheaper calls (fewer replicas requesting/responding) and stronger integrity guarantees (more replicas requesting/responding). + +Flexible outcalls have no `pricing_version` argument; on subnets that charge for HTTP outcalls they are always priced with pricing version `2` ("pay-as-you-go"). + +The arguments of the call are as for `http_request`, except that: + +- there is an additional optional argument `replication`. When set, the caller can specify how many nodes should issue an HTTP outcall (`total_requests`), the minimum number of HTTP responses from nodes in order for the outcall to succeed (`min_responses`), and the maximum number of HTTP responses the caller is willing to receive as the result of the outcall (`max_responses`). That is, a successful HTTP outcall is guaranteed to return between `min_responses` and `max_responses` responses. If `replication` is set, then the caller must ensure that `0 <= min_responses <= max_responses <= total_requests` and `1 <= total_requests <= N`, where `N` is the number of the nodes on the caller's subnet, otherwise the call will fail. The caller may use the `ic0.subnet_self_node_count` System API call to determine `N`. If `replication` is not provided, the defaults of `floor(2 / 3 * N) + 1`, `N` and `N` are used for `min_responses`, `max_responses` and `total_requests`. + + It is `min_responses` that determines when the outcall returns: the result is delivered as soon as `min_responses` responses are available, and further responses are included only if they have arrived by then, fit into the total result limit below, and are covered by the attached cycles. A successful outcall may therefore return as few as `min_responses` responses even when every node responded, so callers must handle any count in the permitted range. Setting `min_responses` and `max_responses` to `0` expresses a fire-and-forget outcall: the requests are issued, and the call replies with an empty vector as soon as the first node has reported back, regardless of the request's outcome. + +- the optional `max_response_bytes` argument bounds the size of the response, but it does not determine the cost of the call: flexible outcalls are always charged for the resources they actually consume. If provided, the value must not exceed `2MB` (`2,000,000B`), otherwise the call will fail. If not provided, the limit of `2MB` is used. Each node enforces the limit individually, both on the response received from the remote server and on the response produced by the `transform` function. The limit a node actually applies is the smaller of `max_response_bytes` and the response size its share of the attached cycles pays for, so a node may fail on a response that is within `max_response_bytes` if too few cycles were attached. + +The other arguments, `url`, `method`, `headers`, `body`, and `transform` are the same as for `http_request`. The result is a vector of responses, with each individual response having the same structure as a `http_request` response, providing `status`, `headers`, and `body` fields. Each response comes from a different node, but the responses do not identify the node that produced them, identical responses from different nodes are not merged, and the order of the responses in the vector is not specified. When fewer responses are returned than the nodes produced, which of them are returned is up to the system, so the returned responses must not be assumed to be a uniform sample. + +As for `http_request`, the endpoint specified by the provided `url` should be idempotent. The one exception is when `total_requests` is set to 1 in `replication`. The request restrictions are also the same as for the `http_request` method: + +- The total number of bytes in the request must not exceed `2MB` (`2,000,000`) bytes. + +- The `GET`, `HEAD`, and `POST` methods are always supported. The `PUT`, `DELETE`, and `PATCH` methods are supported only when the replication counts are deterministic, i.e., when `min_responses`, `max_responses`, and `total_requests` are all equal; otherwise the call will fail. + +- The number of headers must not exceed `64`. + +- The number of bytes representing a header name or value must not exceed `8KiB`. + +- The total number of bytes representing the header names and values must not exceed `48KiB`. + +The response from the remote server must not exceed `max_response_bytes`, if provided, and `2MB` otherwise. Moreover, the responses returned by the different nodes (possibly after the transform function) are delivered together and must jointly fit into a total result limit of `2MiB` (`2,097,152B`), which applies to their encoded sizes plus a small per-response overhead. If they do not all fit, fewer responses are returned, down to `min_responses`; only when even the smallest `min_responses` responses exceed that limit does the call fail. Since up to `max_responses` responses are returned, choosing a `max_response_bytes` of at most `2MB / max_responses` keeps the result within the limit. + +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). As for `http_request` with pricing version `2`, a base fee is charged when the call is accepted and the remaining attached cycles bound what the nodes may spend on the outcall; the unused cycles are then refunded to the caller. + +The result of the call is a variant with an `ok` and an `err` arm, and both arms are delivered as a reply rather than as a reject: an outcall that cannot meet the requested replication requirements, including one that times out, replies with an `err` of the `flexible_http_request_err` type. That error includes a textual error message, an optional global error code, and a vector of per-node details. Failures detected before the requests are issued, such as invalid arguments, invalid `replication` counts, too few attached cycles, or the method not being available on the subnet, are delivered as a reject instead. + +The `global_error` field describes why the aggregate call failed to meet the requirements: + +- `timeout`, meaning that less than `min_responses` from the nodes have been collected before some system-defined timeout. + +- `out_of_cycles`, indicating that what the nodes left unspent of the attached cycles no longer covers delivering any result the call could still produce, including a `too_many_rejects` result. Since the cost of delivering a result depends on the sizes of the responses, this can be reported after the nodes have already completed their HTTP requests. + +- `responses_too_large`: indicating that no combination of at least `min_responses` available responses could fit into the total 2MiB result limit. + +- `too_many_rejects`: indicating that more than `total_requests - min_responses` nodes returned reject responses, so at least `min_responses` successful responses can never be collected. A response, or a transform output, that exceeds the size limit a node enforces is rejected by that node, so exceeding that limit surfaces as `too_many_rejects` rather than as `responses_too_large`. + +The `node_details` vector provides visibility into the execution on specific nodes; it may be empty, and it is not guaranteed to list every node the outcall was issued to. A `timeout` carries no entries; `too_many_rejects` lists rejecting nodes; `responses_too_large` and `out_of_cycles` list nodes whose responses the system has seen, whether those responses succeeded or were rejected. Each node appears at most once, and a successful outcall carries no per-node details at all. Each entry contains: + +- `node_id`. + +- `report`: An accounting of resources (bytes, instructions, time, and cycles) used by the node. Every field is optional: a field is absent when the corresponding resource is not reported, `used` with the amount consumed, or `exceeded` if the node failed because that resource ran over its budget. An implementation may leave the whole report empty, so callers must not rely on it to diagnose a failure. + +- `error`: An optional record containing a `code` and `message`. Its presence does not by itself indicate that the node failed: depending on the global error it is reported for every listed node, including nodes that responded successfully, in which case the `code` conveys the observed outcome and the `message` carries a size or a cycles figure. The `code` values are diagnostic strings and are not a fixed enumeration. + ### IC method `node_metrics_history` {#ic-node_metrics_history} This method can only be called by canisters, i.e., it cannot be called by external users via ingress messages. diff --git a/public/references/ic.did b/public/references/ic.did index 23c9e10b..87fa0f76 100644 --- a/public/references/ic.did +++ b/public/references/ic.did @@ -122,6 +122,34 @@ type http_request_result = record { body : blob; }; +type http_request_resource_report = record { + raw_response_bytes: opt variant { used: nat64; exceeded: reserved }; + http_roundtrip_time_ms: opt variant { used: nat64; exceeded: reserved }; + transform_instructions: opt variant { used: nat64; exceeded: reserved }; + transformed_response_bytes: opt variant { used: nat64; exceeded: reserved }; + cycles: opt variant { used: nat; exceeded: reserved }; +}; + +type flexible_http_request_err = record { + global_error: opt variant { + timeout : reserved; + out_of_cycles : reserved; + responses_too_large : reserved; + too_many_rejects : reserved; + }; + node_details : vec record { + node_id: principal; + report: http_request_resource_report; + error: opt record { code: text; message: text }; + }; + message: text; +}; + +type flexible_http_request_result = variant { + ok: vec http_request_result; + err: flexible_http_request_err; +}; + type ecdsa_curve = variant { secp256k1; }; @@ -362,6 +390,24 @@ type http_request_args = record { context : blob; }; is_replicated : opt bool; + pricing_version : opt nat32; +}; + +type flexible_http_request_args = record { + url : text; + max_response_bytes : opt nat64; + method : variant { get; head; post; put; delete; patch }; + headers : vec http_header; + body : opt blob; + transform : opt record { + function : func(record { response : http_request_result; context : blob }) -> (http_request_result) query; + context : blob; + }; + replication: opt record { + min_responses: nat32; + max_responses: nat32; + total_requests: nat32; + }; }; type ecdsa_public_key_args = record { @@ -682,6 +728,7 @@ service ic : { deposit_cycles : (deposit_cycles_args) -> (); raw_rand : () -> (raw_rand_result); http_request : (http_request_args) -> (http_request_result); + flexible_http_request : (flexible_http_request_args) -> (flexible_http_request_result); // Public canister data canister_info : (canister_info_args) -> (canister_info_result);