diff --git a/docs/concepts/https-outcalls.md b/docs/concepts/https-outcalls.md index a2e0fbf2..89455c4f 100644 --- a/docs/concepts/https-outcalls.md +++ b/docs/concepts/https-outcalls.md @@ -9,14 +9,18 @@ Canisters on the Internet Computer can make HTTP requests to any public web serv ICP runs every canister on a subnet where all replicas execute the same code independently and must reach consensus. Outbound HTTP requests are non-trivial in this model: each replica independently contacts the server and typically receives a slightly different response: timestamps, headers, or field ordering vary, which would cause replicas to diverge. The traditional workaround is **oracles**: third-party services that fetch external data and relay it to the network, at the cost of extra complexity, fees, and a trust assumption. HTTPS outcalls solve the problem directly: the subnet reaches consensus over the response internally, so canisters call external APIs without a middleman. -## Replicated and non-replicated mode +## Outcall modes -HTTPS outcalls have two modes controlled by the `is_replicated` field: +HTTPS outcalls come in three modes. Two are selected by the `is_replicated` field of the `http_request` method; the third is a separate management canister method, `flexible_http_request`. **Replicated mode** (default) is what the consensus mechanism below describes: all replicas independently fetch the URL, a transform function normalizes the responses, and the subnet agrees on a single result. This provides the strongest integrity guarantee: the response is confirmed by a supermajority of nodes, making it extremely difficult for any single party to tamper with it. The tradeoff is that all replicas (typically 13) send the same request to the external server within milliseconds of each other, which can trigger API rate limits. **Non-replicated mode** (`is_replicated = false`) has a single replica make the request. No consensus is needed, so there is no transform function requirement and no rate-limit pressure on the external server. The tradeoff is trust: the single replica that handles the request could theoretically observe or modify the response before returning it to the canister. This mode is appropriate when the endpoint is idempotent, rate limits are a concern, or you're making POST requests where duplicate submissions would cause problems. +**Flexible mode** (`flexible_http_request`) has a committee of nodes make the request and hands the canister their individual responses rather than one agreed result. The canister decides what to make of them. For example: take a median, require that some of them match, or use the first that parses. The caller sizes the committee and states how many responses it needs and is willing to receive. A smaller committee costs less, a larger one is harder for any single node to influence. This suits endpoints whose data changes too fast for replicas to ever agree, such as live prices or feeds that stamp every response, where replicated mode would simply fail to reach consensus. The tradeoff is that reconciling the responses becomes your canister's job. + +Flexible outcalls are always priced with pay-as-you-go pricing (version 2), described under [Cycle costs](#cycle-costs) below. + ## How outcalls reach consensus When a canister calls the management canister's `http_request` method, the following happens: @@ -33,6 +37,8 @@ When a canister calls the management canister's `http_request` method, the follo The transform function is critical. Without it, even minor differences between responses (a header timestamp off by a millisecond) prevent consensus. If consensus cannot be reached, the call eventually times out: this is the most common failure mode when developing outcalls. +Flexible outcalls follow the same path, with two differences. In step 2 only the committee the caller sized issues the request, not every replica. And in step 5 the subnet agrees on which responses to deliver rather than on what the response says, so responses that disagree are returned instead of failing the call. The transform still runs, on each node's own response. + > **Local testing caveat:** The local replica runs a single node, so all responses pass consensus automatically: even without a transform function. Transform and consensus issues only surface when you deploy to a multi-node subnet. For practical guidance on writing transform functions, see the [HTTPS outcalls guide](../guides/backends/https-outcalls.md). @@ -53,7 +59,7 @@ A common pattern is stripping all response headers (they frequently contain time ## Request types and idempotency -HTTPS outcalls support `GET`, `HEAD`, and `POST` methods. +HTTPS outcalls support `GET`, `HEAD`, and `POST` in every mode. `PUT`, `DELETE`, and `PATCH` are restricted to the modes where the number of requests and responses is fixed and known: non-replicated mode, and flexible mode when the committee size and the required and accepted response counts are all equal. The restriction exists because replicated outcalls with `is_replicated = true` do not wait for every request to finish, so one mutating request could land after a later one and undo it. **GET and HEAD** requests are straightforward: they're inherently idempotent (repeating them doesn't change server state), so having 13 replicas send the same GET is harmless. `HEAD` is particularly useful for determining a resource's response size before making the actual request, which helps you set `max_response_bytes` accurately. @@ -67,21 +73,33 @@ Not all servers support idempotency keys, so evaluate this on a case-by-case bas ## Cycle costs -HTTPS outcalls are not free. The calling canister must attach cycles to cover the cost. Both the Motoko `ic` mops package and the Rust `ic-cdk` provide wrappers that automatically compute and attach the required amount using the `ic0.cost_http_request` system API. +HTTPS outcalls are not free. The calling canister must attach cycles to cover the cost. The system API reports what to attach, so a canister never has to hard-code a price: `ic0.cost_http_request_v2` for pay-as-you-go pricing, and the older `ic0.cost_http_request` for deprecated legacy pricing (charged in advance). + +There are two pricing models, chosen per call by the `pricing_version` field. + +:::caution[Version 1 is deprecated] + +Version 1 is still the default, and is what a call gets unless it asks for version 2. It is nonetheless deprecated: version 2 is to become the default, after which version 1 will be removed. New canisters should select version 2, and existing canisters should plan to migrate. -The cost depends on two factors: +::: + +Both the Motoko `ic` mops package and the Rust `ic-cdk-management-canister` crate provide wrappers that automatically compute and attach the required amount using the `ic0.cost_http_request` system API (version 1). Neither exposes `pricing_version` or `flexible_http_request` yet, so a canister that wants version 2 or flexible mode must build the management canister call itself. + +**Version 1** charges for the number of bytes you reserve. The cost depends on two factors: - **Request size**: the combined byte length of the URL, headers, body, transform function name, and transform context. - **`max_response_bytes`**: the maximum response size you declare. This is what you're charged for, not the actual response size. If you omit `max_response_bytes`, the system assumes the maximum of 2 MB and charges accordingly: roughly 20.85 billion cycles on a 13-node subnet. Always set this to a reasonable upper bound for your expected response to avoid overpaying. Unused cycles are refunded. -For exact pricing formulas, see the [cycles costs reference](../references/cycle-costs.md). +**Version 2** charges for what the call actually consumes: the bytes that arrive, the time the request takes, and the instructions the transform function runs. `max_response_bytes` still bounds the response, but it no longer sets the price. A generous cap therefore adds nothing to the charge; it only reserves more cycles for the duration of the call, which limits how many outcalls the canister can have in flight. The tradeoff is that the attached cycles double as the call's resource budget. A call that does not cover the base fee is rejected up front. Beyond that, attaching less than the call needs is accepted: it runs with proportionally smaller limits on response size, response time, and transform instructions, and fails partway through rather than up front. Use `ic0.cost_http_request_v2` to compute a recommendation of what to attach. + +For exact pricing formulas for both versions, see the [cycles costs reference](../references/cycle-costs.md). ## Limitations - **HTTPS only.** Plain HTTP is not supported. The target server must have a valid TLS certificate. -- **2 MB response limit.** The maximum is 2,000,000 bytes (decimal, not 2^21). The limit covers the response's header names and values plus the body, not the body alone, and it is enforced twice: on the raw response as it arrives from the server, and again on the output of the transform function. A transform therefore cannot rescue a response that already exceeded the cap, because the first check runs before the transform does. Size `max_response_bytes` for the headers and body as they arrive from the server. +- **2 MB response limit.** The maximum is 2,000,000 bytes (decimal, not 2^21). The limit covers the response's header names and values plus the body, not the body alone, and it is enforced twice: on the raw response as it arrives from the server, and again on the output of the transform function. A transform therefore cannot rescue a response that already exceeded the cap, because the first check runs before the transform does. Size `max_response_bytes` for the headers and body as they arrive from the server. In flexible mode the responses delivered together must additionally fit a 2 MiB total. - **Public endpoints only.** Canisters cannot reach localhost, private IP ranges (10.x.x.x, 192.168.x.x), or other non-routable addresses. - **No streaming or WebSocket.** Outcalls are single request-response pairs. Long-lived connections are not supported. - **Two timeouts.** If the external server does not respond within 30 seconds, or the subnet does not produce a response within 60 seconds, the call is rejected. It does not trap, so handle the error case rather than relying on a trap. @@ -100,12 +118,6 @@ For exact pricing formulas, see the [cycles costs reference](../references/cycle HTTPS outcalls can replace oracles for most use cases: price feeds, API queries, webhook notifications, and data verification. Oracles may still be useful if you need features like aggregated multi-source data feeds or historical data caching that an oracle provider maintains as a service. -## Future extensions - -One extension is under consideration that may affect architecture decisions: - -- **Multiple responses:** Instead of consensus on a single response, the canister could receive all individual replica responses and resolve differences in application logic: useful for fast-moving data like price feeds. - ## Next steps - [HTTPS outcalls guide](../guides/backends/https-outcalls.md): practical how-to with code examples in Motoko and Rust diff --git a/docs/concepts/index.md b/docs/concepts/index.md index ec5ae471..380fc4d5 100644 --- a/docs/concepts/index.md +++ b/docs/concepts/index.md @@ -33,7 +33,7 @@ Understand the ideas behind the Internet Computer before you build on it. These - **[Orthogonal persistence](orthogonal-persistence.md)**: How canister memory survives across executions and upgrades without databases. - **[Timers](timers.md)**: Periodic and one-shot scheduled tasks via the global timer mechanism. - **[Verifiable randomness](verifiable-randomness.md)**: Cryptographically secure random numbers using threshold VRF. -- **[HTTPS outcalls](https-outcalls.md)**: How canisters make HTTP requests to external services with consensus on responses. +- **[HTTPS outcalls](https-outcalls.md)**: How canisters make HTTP requests to external services, with or without consensus on the response. ## Cryptography diff --git a/docs/guides/backends/https-outcalls.mdx b/docs/guides/backends/https-outcalls.mdx index 3e63ab50..e0c7b7d4 100644 --- a/docs/guides/backends/https-outcalls.mdx +++ b/docs/guides/backends/https-outcalls.mdx @@ -10,7 +10,7 @@ import CodeExample from '../../../src/components/CodeExample.astro'; [Canisters](../../concepts/canisters.md) can make HTTP requests to external web services using HTTPS outcalls. This lets your canister call REST APIs or send notifications: all from canister code. -HTTPS outcalls are available through the [IC management canister](../../references/management-canister.md) (`aaaaa-aa`) via the `http_request` method. The `GET`, `HEAD`, and `POST` methods are supported. `HEAD` works identically to `GET` but returns only headers: useful for checking resource availability without downloading the body. Only HTTPS (not plain HTTP) is supported. +HTTPS outcalls are available through the [IC management canister](../../references/management-canister.md) (`aaaaa-aa`) via the `http_request` method, and via `flexible_http_request` for the flexible mode described below. `GET`, `HEAD`, and `POST` are supported in every mode. `PUT`, `DELETE`, and `PATCH` are only supported where the number of requests and responses is fixed and known, i.e. non-replicated outcalls and flexible outcalls where the number of total requests, min responses, and max responses are equal. `HEAD` works identically to `GET` but returns only headers: useful for checking resource availability without downloading the body. Only HTTPS (not plain HTTP) is supported. For how the consensus mechanism works for outcalls, see [Concepts: HTTPS Outcalls](../../concepts/https-outcalls.md). @@ -18,21 +18,23 @@ For how the consensus mechanism works for outcalls, see [Concepts: HTTPS Outcall By default, every replica node in the subnet independently makes the same HTTP request: called **replicated mode**. All nodes must agree on the response before execution continues. Two constraints apply regardless of mode: -- [Cycles](../../concepts/cycles.md) to cover the request cost **must be attached** at call time. Both languages provide a wrapper that computes the exact amount and attaches it: `ic_cdk::management_canister::http_request` in Rust, and `Call.httpRequest` from the `ic` package in Motoko. Prefer these over a hand-picked figure: attached cycles are held for the duration of the call, so an arbitrary margin caps how many outcalls the canister can have in flight. -- The **maximum response size is 2MB** (2,000,000 bytes). This covers the response's header names and values plus the body, so size a cap against both: a response can carry 1–2 KB of headers before any body. Requests exceeding this limit fail. Always set `max_response_bytes` to a tight upper bound: omitting it defaults to 2MB and charges cycles accordingly. +- [Cycles](../../concepts/cycles.md) to cover the request cost **must be attached** at call time. For `http_request` under the default pricing, both languages provide a wrapper that computes the exact amount and attaches it: `ic_cdk_management_canister::http_request` in Rust (the `ic-cdk-management-canister` crate), and `Call.httpRequest` from the `ic` package in Motoko. Prefer these over a hand-picked figure: attached cycles are held for the duration of the call, so an arbitrary margin caps how many outcalls the canister can have in flight. +- The **maximum response size is 2MB** (2,000,000 bytes). This covers the response's header names and values plus the body, so size a cap against both: a response can carry 1 to 2 KB of headers before any body. Requests exceeding this limit fail. Under legacy pricing (version 1), always set `max_response_bytes` to a tight upper bound: omitting it defaults to 2MB and charges cycles accordingly. In flexible mode the same 2MB cap applies to each node's response, and the responses delivered together (at least `min_responses` many) must additionally fit a **2 MiB total** (2,097,152 bytes). -In replicated mode, a transform function is strongly recommended: without one, responses across nodes will likely differ and consensus will fail. In non-replicated mode (`is_replicated = false`), a transform is unnecessary because only one node makes the request. See [Replicated vs non-replicated mode](#replicated-vs-non-replicated-mode) below. +In replicated mode, a transform function is strongly recommended: without one, responses across nodes will likely differ and consensus will fail. In non-replicated (`is_replicated = false`) and flexible mode it is optional, because the nodes' individual responses are returned rather than reconciled. See [Outcall modes](#outcall-modes) below. -## Replicated vs non-replicated mode +## Outcall modes -HTTPS outcalls have two modes, controlled by the `is_replicated` field: +HTTPS outcalls have three modes. Replicated and non-replicated are selected by the `is_replicated` field of `http_request`; flexible is a separate management canister method, `flexible_http_request`. -| | Replicated (default) | Non-replicated (`is_replicated = false`) | -|---|---|---| -| Who sends the request | All N nodes on the subnet | One node | -| Consensus on response | Yes | No | -| Transform needed | Strongly recommended | No | -| Risk | API rate limits (N simultaneous requests) | Response could be tampered with | +| | Replicated (default) | Non-replicated (`is_replicated = false`) | Flexible (`flexible_http_request`) | +|---|---|---|---| +| Who sends the request | All N nodes on the subnet | One node | A committee of `total_requests` nodes | +| What the canister gets | One agreed response | The one node's response | Between `min_responses` and `max_responses` individual responses | +| Consensus on response | Yes | No | No: consensus is on which responses to deliver | +| Transform needed | Strongly recommended | Optional | Optional | +| Pricing | Version 1 (deprecated) or 2 | Version 1 (deprecated) or 2 | Always version 2 | +| Risk | API rate limits (N simultaneous requests) | Response could be tampered with | Reconciling the responses is your canister's job | **Rate limit risk in replicated mode:** On a 13-node subnet, 13 identical requests hit the external API within milliseconds. Many APIs enforce per-second or per-IP rate limits that this will trigger. If the API you're calling has rate limits, prefer `is_replicated = false`. @@ -42,6 +44,8 @@ HTTPS outcalls have two modes, controlled by the `is_replicated` field: The tradeoff with non-replicated mode: the single node that makes the request could theoretically observe and modify the response before returning it to the canister. +**Use flexible mode** when the data changes too fast for replicas to ever agree, so replicated mode would fail consensus, and you would rather compare several independent answers than trust one. The tradeoff is between cost and trust: a committee of three costs less than one of 13 and is correspondingly easier for a single node to skew. Your canister must handle any count between `min_responses` and `max_responses`, including responses that disagree with each other. See [`flexible_http_request`](../../references/ic-interface-spec/management-canister.md#ic-flexible_http_request) in the interface specification for the argument and result types. + ## GET request A minimal example that sends a GET request to an echo service. The response body is deterministic, so this uses replicated mode for strong integrity guarantees: @@ -124,9 +128,17 @@ POST requests work the same way, with two additional considerations: +## Flexible request + +Flexible outcalls go through the `flexible_http_request` management canister method, which hands the canister each node's own response instead of one the subnet agreed on. + +Handle any count between `min_responses` and `max_responses`: fewer than `max_responses` is a normal success, not a degraded one. The responses do not say which node produced them and their order is not specified, so treat them as an unordered multiset. On failure, `err.global_error` distinguishes a `timeout` from `out_of_cycles`, `responses_too_large`, and `too_many_rejects`, and `err.node_details` reports what the individual nodes did. + +**The delivered responses must fit 2 MiB between them.** The limit applies to the combined encoded size of every response returned after the transform, and not to each response on its own. When they do not all fit, fewer are returned, down to `min_responses`. The call fails only when even the smallest `min_responses` responses exceed the limit together. + ## Transform functions -In replicated mode, a transform function is strongly recommended (without one, responses across nodes will likely differ and consensus will fail. In non-replicated mode it is unnecessary. The transform runs on each replica before consensus and must be a `query` method. At minimum, strip all HTTP response headers) they contain non-deterministic fields like `Date`, `Set-Cookie`, and tracking IDs: +In replicated mode, a transform function is strongly recommended: without one, responses across nodes will likely differ and consensus will fail. In non-replicated and flexible mode it is optional; each node runs it on its own response. The transform runs on each replica before consensus and must be a `query` method. At minimum, strip all HTTP response headers, which carry non-deterministic fields like `Date`, `Set-Cookie`, and tracking IDs: - In Motoko: `{ response with headers = [] }` - In Rust: `HttpRequestResult { headers: vec![], ..raw.response }` @@ -139,16 +151,41 @@ If the response body also contains dynamic fields (timestamps, per-request IDs, ## Cycle costs -HTTPS outcall costs are based on `max_response_bytes`, not the actual response size. If you omit `max_response_bytes`, the system assumes 2MB and charges approximately **20.85 billion cycles**: even for a 1KB response. Always set a tight upper bound. Unused cycles are refunded, but you still pay for the declared maximum. +HTTPS outcalls are not free, and there are two pricing versions, chosen per call by the `pricing_version` field of `http_request`. Flexible outcalls have no such field and are always priced with version 2. + +:::caution[Version 1 is deprecated] + +Version 1 is still the default, so a call that does not set `pricing_version` gets it. Version 2 is to become the default, after which version 1 will be removed. -In Rust, `ic_cdk::management_canister::http_request` computes and attaches the exact cost automatically using the `ic0.cost_http_request` system API. In Motoko, `Call.httpRequest` from the `ic` package does the same. Attaching a hand-picked amount instead is counterproductive: the cycles are held for the duration of the call, so a margin caps how many outcalls the canister can have in flight. +::: + +| | Version 1 (default, deprecated) | Version 2 (pay-as-you-go) | +|---|---|---| +| Charged for | `max_response_bytes`, whether you use it or not | the bytes that arrive, the round-trip time, the transform instructions | +| Role of `max_response_bytes` | sets both the limit and the price | sets the limit and affects the cycles reservation, not the price | +| Amount to attach | the exact charge, computable from the request | a cycles budget, based on expected resource consumption | +| Cost system API | `ic0.cost_http_request` | `ic0.cost_http_request_v2` | +| Attaching too little | rejected up front | rejected up front if it misses the base fee; otherwise runs with tighter per-node limits and may fail partway | +| Flexible outcalls | not available | required | + +### Version 1 + +Costs are based on `max_response_bytes`, not the actual response size. If you omit `max_response_bytes`, the system assumes 2MB and charges approximately **20.85 billion cycles**: even for a 1KB response. Always set a tight upper bound. Unused cycles are refunded, but you still pay for the declared maximum. + +In Rust, `ic_cdk_management_canister::http_request` computes and attaches the exact cost automatically using the `ic0.cost_http_request` system API. In Motoko, `Call.httpRequest` from the `ic` package does the same. Attaching a hand-picked amount instead is counterproductive: the cycles are held for the duration of the call, so a margin caps how many outcalls the canister can have in flight. For reference, on a 13-node subnet: - Base cost: ~49 million cycles - Per request byte: 5,200 cycles - Per `max_response_bytes` byte: 10,400 cycles -See [Cycles costs](../../references/cycle-costs.md#https-outcalls) for the full pricing table. +### Version 2 + +Setting `pricing_version = 2` prices the resources the call actually consumes: the bytes that arrive, the time the request takes, and the instructions the transform runs. A conservative `max_response_bytes` therefore costs nothing extra. It still bounds the response, and it still affects how many cycles are held while the call runs, but it no longer sets the price. + +Compute the recommended cycles amount to attach with `ic0.cost_http_request_v2`, passing the resources you expect the call to use. Passing the maximum of each instead gives you an amount the call cannot exhaust, but reserves far more for the duration of the call, since the maxima include a 60-second round trip and the full query instruction limit for the transform. + +See [Cycles costs](../../references/cycle-costs.md#https-outcalls) for the full pricing formulas for both versions. ## Limitations and pitfalls @@ -166,6 +203,7 @@ Use the "Full example in ICP Ninja" links above to deploy and test directly in t - [Concepts: HTTPS Outcalls](../../concepts/https-outcalls.md): how consensus works for outcalls - [Management canister reference](../../references/management-canister.md#http_request): full `http_request` parameter reference including all fields +- [`flexible_http_request` reference](../../references/management-canister.md#flexible_http_request): arguments, results, and errors of the flexible mode - [Exchange Rate Canister (XRC)](https://github.com/dfinity/exchange-rate-canister): a production service powered by HTTPS outcalls that fetches digital asset and fiat exchange rates - [Chain Fusion: Ethereum](../chain-fusion/ethereum.md): the EVM RPC canister uses HTTPS outcalls under the hood - [Cycles costs](../../references/cycle-costs.md#https-outcalls): outcall pricing details diff --git a/docs/guides/security/dos-prevention.md b/docs/guides/security/dos-prevention.md index ceb03f30..8333d65b 100644 --- a/docs/guides/security/dos-prevention.md +++ b/docs/guides/security/dos-prevention.md @@ -49,6 +49,7 @@ An attacker will target expensive calls to drain the cycles balance or available * **Use captchas**: Expensive operations should require a captcha to be solved. Try to use a library to implement a captcha instead of a cloud service, as such a service would require HTTPS outcalls and isn't decentralized. * **Use PoW (proof-of-work)**: Require a proof-of-work challenge to be solved by the client for any expensive operation. The parameters need to be carefully chosen to require sufficient computation per call to the expensive operation without creating too much impact for legitimate clients. Don't forget to consider clients on slow and older mobile devices while protecting against attackers on modern multi-GPU systems. Certain algorithms can limit the performance increase of GPUs to improve this uneven battlefield. +* **Bound outcall budgets**: under HTTPS outcall pricing version 2, the cycles you attach beyond the base fee are the call's spending limit, and the remote server decides how much of it is consumed. Attach what the call is expected to need rather than a generous margin, especially where the outcall can be triggered by a user, and the remote server is untrusted. See [HTTPS outcalls security](https-outcalls.md#be-aware-of-http-request-and-response-sizes). * **Charge for expensive calls**: You can require that certain expensive calls from other canisters include cycles to compensate for the resources consumed. In addition, one can charge for ingress messages. However, that is not currently supported by the protocol itself, and a custom solution, such as pre-paying a certain amount, would need to be designed. * **Differentiate between update and query calls**: Expensive computations should generally be avoided for update calls unless absolutely necessary. While query calls are not authenticated, they are faster and less resource-intensive. To check whether a method was called as a query or update call, you can use `ic0.in_replicated_execution()`. diff --git a/docs/guides/security/https-outcalls.md b/docs/guides/security/https-outcalls.md index d8b9dcae..5ba7b345 100644 --- a/docs/guides/security/https-outcalls.md +++ b/docs/guides/security/https-outcalls.md @@ -26,7 +26,7 @@ See also: [data confidentiality on ICP](./miscellaneous.md#data-confidentiality- ### Security concern -When an HTTPS outcall is performed, it is amplified by the number of replicas in the subnet. The target web server will receive not only one request but as many requests as the number of nodes in the subnet. +When a replicated HTTPS outcall is performed, it is amplified by the number of replicas in the subnet. The target web server will receive not only one request but as many requests as the number of nodes in the subnet. Non-replicated mode sends a single request, and flexible mode sends `total_requests` of them; see [Outcall modes](../backends/https-outcalls.md#outcall-modes). Most web servers implement some sort of rate limiting; this is a mechanism used to restrict the number of requests a client can make to a web server within a specific time period, preventing abuse or excessive usage of their API(s). @@ -40,7 +40,7 @@ See the [HTTPS outcalls guide](../backends/https-outcalls.md) for more details. ### Security concern -As mentioned before, if an HTTPS outcall is performed, it is amplified by the number of replicas in the subnet. That means the queried endpoint will receive the same request several times. This is especially risky in requests that change the endpoint state, given that one HTTPS outcall could lead to unintentionally changing the endpoint state several times. +As mentioned before, a replicated HTTPS outcall is amplified by the number of replicas in the subnet. That means the queried endpoint will receive the same request several times. This is especially risky in requests that change the endpoint state, given that one HTTPS outcall could lead to unintentionally changing the endpoint state several times. ### Recommendation @@ -76,7 +76,7 @@ The [pricing](../../references/cycle-costs.md#https-outcalls) of HTTPS outcalls When using HTTPS outcalls, be mindful of the HTTP request and response sizes. Ensure that the size of the request issued and the size of the HTTP response coming from the server are reasonable. -When making an HTTPS outcall, it is possible (and highly recommended) to define the `max_response_bytes` parameter, which allows you to set the maximum allowed response size. If this parameter is not defined, it defaults to the hard response size limit of the HTTPS outcalls feature, which is 2MiB. The cycle cost of the response is always charged based on the `max_response_bytes` or 2MB if not set. +When making an HTTPS outcall, it is possible (and highly recommended) to define the `max_response_bytes` parameter, which allows you to set the maximum allowed response size. If this parameter is not defined, it defaults to the hard response size limit of 2MB (2,000,000 bytes). Under pricing version 1 the call is charged against `max_response_bytes` whether or not the response uses it. Under version 2 the charge follows the bytes that actually arrive, but the cycles you attach still bound what the call may spend, so an oversized budget is what a slow or verbose server can drain. Finally, be aware that users may incur cycles costs for HTTPS outcalls in case these calls can be triggered by user actions. @@ -94,4 +94,10 @@ Perform input validation when using user-submitted data in the HTTPS outcalls. See the [OWASP Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html) for more information. +## Next steps + +- [HTTPS outcalls guide](../backends/https-outcalls.md): how to make outcalls, with the three modes and both pricing versions +- [Concepts: HTTPS outcalls](../../concepts/https-outcalls.md): how consensus works for outcalls +- [Cycles costs](../../references/cycle-costs.md#https-outcalls): outcall pricing formulas + diff --git a/docs/languages/rust/index.md b/docs/languages/rust/index.md index 3447f36a..035f9948 100644 --- a/docs/languages/rust/index.md +++ b/docs/languages/rust/index.md @@ -204,7 +204,7 @@ Rust canisters compile to `wasm32-unknown-unknown`. Most pure-computation crates | Current time | `std::time::Instant` | `ic_cdk::api::time()` returns nanoseconds since the epoch. | | Environment variables | `std::env::var` | Not available at runtime. Use `env!()` or `option_env!()` to embed values at compile time. | | Random numbers | `rand`, `getrandom` | Use `ic_cdk::management_canister::raw_rand()` for verifiable randomness, or implement `getrandom::register_custom_getrandom!` for crates that depend on `getrandom`. | -| Network I/O | `reqwest`, `hyper` | Use [HTTPS outcalls](../../guides/canister-calls/calling-from-clients.md) via the management canister. | +| Network I/O | `reqwest`, `hyper` | Use [HTTPS outcalls](../../guides/backends/https-outcalls.md) via the management canister. | Most crates that target `wasm32-unknown-unknown` for browser use (via `wasm-bindgen` or `wasm-pack`) will **not** work because they depend on JavaScript host bindings that do not exist in the ICP runtime. diff --git a/docs/references/cycle-costs.md b/docs/references/cycle-costs.md index b89d0fed..4e6dbcb4 100644 --- a/docs/references/cycle-costs.md +++ b/docs/references/cycle-costs.md @@ -112,11 +112,21 @@ If the canister may be blackholed or called by other canisters, send more cycles ## External integrations -These features involve outbound calls to external networks. Every node on the relevant subnet participates in each call, which is the primary driver of the additional cost. The subsections below are ordered by pricing mechanism: HTTPS outcalls first as the base primitive, then the two RPC canisters that build on it, then the native chain integrations that use a two-tier pricing model. +These features involve outbound calls to external networks. How many nodes take part in each call is the primary driver of the additional cost: the whole subnet for a fully replicated outcall, and fewer for the modes that reduce replication (see HTTPS outcalls below). The subsections below are ordered by pricing mechanism: HTTPS outcalls first as the base primitive, then the two RPC canisters that build on it, then the native chain integrations that use a two-tier pricing model. ### HTTPS outcalls -HTTPS outcall costs scale with subnet size (`n` = number of nodes): +Outcalls have two pricing versions, chosen per call by the `pricing_version` field of `http_request`. Version `1` prices the request and response bytes that a call reserves. Version `2` prices the resources a call actually consumes, and is also the only pricing available to [`flexible_http_request`](ic-interface-spec/management-canister.md#ic-flexible_http_request), which has no `pricing_version` field. + +:::caution[Version 1 is deprecated] + +Version `1` is still the default, but it is deprecated. Version `2` is to become the default, after which version `1` will be removed. Callers are advised to migrate to version `2`. + +::: + +Rather than hard-coding either formula, read the cost at runtime: `ic0.cost_http_request` for version `1`, `ic0.cost_http_request_v2` for version `2`. + +**Version 1 (default, deprecated).** Costs scale with subnet size (`n` = number of nodes): ``` total_fee = base_fee + size_fee @@ -132,6 +142,48 @@ size_fee = (400 * request_bytes + 800 * max_response_bytes) * n | Per request byte | 5_200 | ~$0.0000000071 | 13_600 | ~$0.0000000186 | | Per reserved response byte | 10_400 | ~$0.0000000142 | 27_200 | ~$0.0000000372 | +**Version 2 (pay-as-you-go).** The price has three parts: a base fee charged when the call is accepted, a usage fee charged for each node that performs the outcall, and a delivery fee for putting the result into a block. `max_response_bytes` appears in none of them. It still caps the response, and it affects how much of the payment is withheld while the call is in flight, but it no longer sets the price. + +What a call is **charged**, once it settles: + +``` +n = subnet size. K = responses delivered (1 unless flexible). + +base_fee = (1_000_000 + 50 * request_bytes + replication_term) * n + replication_term = 140_000 * n + 800 * n * n fully replicated + = 90_000 * n + (2_000 * n + 100_000) * min_responses otherwise + +usage_fee = 50 * raw_response_bytes + 300 * roundtrip_ms + + transform_instructions / 13 + (+ 50 * n * response_bytes non-replicated and flexible only) + +delivery_fee = n * (10 * n + 600) * response_bytes + (+ (2_000 * n + 100_000) * n * (K - min_responses) flexible only) +``` + +`usage_fee` is charged for each node that performs the outcall: all `n` of them for a fully replicated call, one for a non-replicated call, `total_requests` for a flexible one. `response_bytes` is the size after the transform. A non-replicated call (`is_replicated = false`) takes the `otherwise` branch with `min_responses = 1`. A flexible call that does not set `replication` defaults `min_responses` to `floor(2 / 3 * n) + 1`. + +| Component | 13-node cycles | ~USD | 34-node cycles | ~USD | +|-----------|----------------|------|----------------|------| +| Per fully replicated call (base) | 38_417_600 | ~$0.0000525 | 227_283_200 | ~$0.000311 | +| Per request byte | 650 | ~$0.0000000009 | 1_700 | ~$0.0000000023 | +| Per delivered response byte, charged | 9_490 | ~$0.0000000130 | 31_960 | ~$0.0000000437 | + +**What to attach.** `ic0.cost_http_request_v2` does not return the figure above. Neither how many nodes will respond nor which result they will produce is known when the call is made, and delivering the result has to be paid out of the per-node budgets, so the quote reserves for the most expensive result the call could still produce. It therefore exceeds what the call settles at, and the difference is refunded. + +Pass what you expect and you get a small reservation, at the cost of the outcall running within correspondingly tighter per-node limits. Pass the maxima a run could consume and you get the figure that cannot run short, which is also the most the system withholds: + +| Parameter | Maximum | +|-----------|---------| +| `http_roundtrip_time_ms` | `60_000`, the longest the system waits for a response | +| `raw_response_bytes` | `max_response_bytes`, or `2_000_000` if it is unset | +| `transformed_response_bytes` | the same as `raw_response_bytes`, plus `1_024` bytes reserved for the Candid encoding of the response | +| `transform_instructions` | `5_000_000_000` (5 billion), the instruction limit of a query call | + +`request_bytes` and `outcall_type` follow from the request itself. + +Either way, any attached surplus is refunded, so the charge follows the resources actually consumed. Those refunds arrive asynchronously: a node that never reported has its whole budget returned when the request is discarded, one minute after the response was delivered. A canister that reads its own balance right after an outcall will see it keep settling for a while afterwards, as further refunds arrive. + ### EVM RPC canister Calls to the EVM RPC canister use an HTTPS-outcall-based pricing structure with higher per-byte constants than standard HTTPS outcalls, scaled by the number of RPC services used for multi-provider consistency: diff --git a/docs/references/ic-interface-spec/abstract-behavior.md b/docs/references/ic-interface-spec/abstract-behavior.md index 89a807f5..6ecb9f63 100644 --- a/docs/references/ic-interface-spec/abstract-behavior.md +++ b/docs/references/ic-interface-spec/abstract-behavior.md @@ -5867,6 +5867,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;} @@ -6128,6 +6132,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..bf1d3c42 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, along with the pricing version it prices. 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, and deprecated). `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,51 @@ 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, or if the blob is too large. Because the decoder can skip only a small, fixed amount of extra data, two further conditions apply: 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 may trap. + + The function returns the recommended 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 cycles are 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. + + Of these parameters, only `request_bytes` and `outcall_type` are known before the outcall runs. For the others, pass the largest value the outcall is expected to consume, up to the following maxima: the longest time the system will wait for a response for `http_roundtrip_time_ms` (60s), the request's `max_response_bytes` for `raw_response_bytes`, and the instruction limit of a query call for `transform_instructions` (5 billion). For `transformed_response_bytes`, pass `max_response_bytes` plus the 1 KiB reserved for the Candid encoding of the response. For a flexible outcall with a positive `min_responses`, `transformed_response_bytes` need not exceed the total result limit (2MiB) divided by `min_responses`; for a fire-and-forget outcall (`max_responses` of `0`) no response is ever delivered. Delivery is settled against the participating nodes' allowances collectively, so that per-node figure covers any result the limit permits, whether the responses are of equal size or not. + + Attaching the amount so obtained means the outcall cannot run out of cycles in any run that could have succeeded. A caller that prefers to reserve less may instead pass the resources it expects the outcall to use and attach that smaller amount: as long as it still covers the base fee, the call is not rejected for it, but the per-node limits described under [`http_request`](./management-canister.md#ic-http_request) shrink accordingly, so a run that exceeds the estimate fails part-way through. Cycles attached beyond what is withheld as per-node budgets are refunded with the response; the unspent part of the budgets themselves is credited asynchronously afterwards, as described under [`http_request`](./management-canister.md#ic-http_request). + - `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/changelog.md b/docs/references/ic-interface-spec/changelog.md index 4225275e..cb29b49f 100644 --- a/docs/references/ic-interface-spec/changelog.md +++ b/docs/references/ic-interface-spec/changelog.md @@ -8,6 +8,28 @@ sidebar: ## Changelog {#changelog} +### 0.68.0 (2026-09-14) {$0_68_0} +* New management canister method `flexible_http_request`, a variant of `http_request` in which a committee + of nodes return their individual HTTP responses to the caller instead of the subnet reaching consensus + on a single response. The optional `replication` argument sizes the committee (`total_requests`) and + bounds how many responses the outcall requires (`min_responses`) and the caller will accept + (`max_responses`); it defaults to `floor(2 / 3 * N) + 1`, `N`, and `N` on a subnet of `N` nodes. + The result is a variant whose `err` arm reports why the requested replication could not be met and + what the individual nodes did; both arms are delivered as a reply rather than as a reject. +* New optional `pricing_version` field of `http_request` selecting the pricing mechanism for the outcall: + `1` ("legacy"), which prices the call by `max_response_bytes`, or `2` ("pay-as-you-go"), which prices + the resources the call actually consumes and makes the attached cycles bound what it may consume. + The default is `1` and an unrecognized value is treated as `1`. Pricing version `1` is deprecated: + version `2` is to become the default, after which version `1` will be removed and the field will no + longer have an effect. Flexible outcalls have no `pricing_version` and are always priced with version `2`. +* New canister System API `ic0.cost_http_request_v2` returning the cycles to attach to an HTTP outcall + priced with pricing version `2`, for a fully replicated, non-replicated, or flexible outcall. + The System API `ic0.cost_http_request` is deprecated along with the pricing version it prices. +* New canister System API `ic0.subnet_self_node_count` returning the number of nodes on the subnet + the canister is running on. +* The non-replicated mode of `http_request`, selected by the `is_replicated` field, is no longer + considered experimental. + ### 0.67.0 (2026-08-31) {$0_67_0} * New canister setting `log_memory_limit` bounding the memory used for canister logs: it must be either `0` or a number between `4096` and `2097152` (`2 MiB`), inclusively, with the default value `4096`. diff --git a/docs/references/ic-interface-spec/management-canister.md b/docs/references/ic-interface-spec/management-canister.md index 38308e56..dc5d0841 100644 --- a/docs/references/ic-interface-spec/management-canister.md +++ b/docs/references/ic-interface-spec/management-canister.md @@ -676,12 +676,6 @@ This method makes an HTTP request to a given URL and returns the HTTP response, The method can be called in either replicated or non-replicated mode. In the replicated mode, the same HTTP request is performed by multiple IC replicas, providing strong guarantees on the integrity of the response. In the non-replicated mode, the request is made by a single replica, with weak integrity guarantees. -:::note - -The non-replicated mode is considered EXPERIMENTAL. Canister developers must be aware that the API may evolve in a non-backward-compatible way. - -::: - Both because of replication and to handle network issues, the canister should aim to issue *idempotent* requests, meaning that it must not change the state at the remote server, or that the remote server has the means to identify duplicated requests. Otherwise, the risk of failure increases. In the replicated mode, the responses for all identical requests must match, too. However, a web service could return slightly different responses for identical idempotent requests. For example, it may include some unique identification or a timestamp that would vary across responses. @@ -710,7 +704,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", deprecated) 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. @@ -720,15 +714,25 @@ 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 +- `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", deprecated) or `2` ("pay-as-you-go"). For compatibility reasons, the default is `1`. If the field is omitted, or set to any value other than `1` or `2`, 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. -The `is_replicated` field is considered EXPERIMENTAL. + :::note -::: + Pricing version `1` is DEPRECATED. Version `2` is to become the default, after which version `1` will be removed and the `pricing_version` field will no longer have an effect. Canister developers are advised to select version `2`. -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). + ::: + +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 per-node budget. A node's remaining budget bounds the response it may download, the time it may wait for that response, and the 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. Since the cost of delivering a response depends on its size, this can happen after the remote server has already been contacted. + +Any unspent part of the per-node budgets is credited to the caller's cycles balance asynchronously, separately from the refund that accompanies the response: each node returns what it did not spend. A node that never reports has its whole budget returned after a timeout of one minute. A canister's cycles balance may therefore keep settling for a while after the response arrives, as further refunds come in. The returned response (and the response provided to the `transform` function, if specified) contains the following fields: @@ -763,10 +767,66 @@ 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 caller trade cost against integrity: fewer nodes issuing the request cost less, more nodes make the result harder for any single node to influence. + +Flexible outcalls have no `pricing_version` argument; 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 successful 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` successful 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 both `min_responses` and `max_responses` to `0` makes the outcall fire-and-forget: 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 `total_requests = 1`. 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 the smallest `min_responses` responses jointly exceed that limit does the call fail. + +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. That budget is split between the `total_requests` nodes performing the outcall rather than across the whole subnet, and a node that exhausts its share rejects, counting towards `too_many_rejects` below. + +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`: fewer than `min_responses` successful responses were collected from the nodes before the system-defined timeout of one minute. + +- `out_of_cycles`: 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`: no combination of at least `min_responses` available responses could fit into the total 2MiB result limit. + +- `too_many_rejects`: 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 at least `total_requests - min_responses + 1` of the 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 and takes one of three forms: absent when the resource is not reported, `used` when the node reports the amount it consumed, or `exceeded` when 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/docs/references/management-canister.md b/docs/references/management-canister.md index bec93740..a6ecc93a 100644 --- a/docs/references/management-canister.md +++ b/docs/references/management-canister.md @@ -444,25 +444,47 @@ Returns an encrypted vetKD key that can be decrypted with the caller's transport Makes an HTTP request to an external URL and returns the response. This enables canisters to fetch offchain data, call external APIs, and interact with other blockchain RPCs. +:::caution[Version 1 is deprecated] + +Pricing version 1 is still the default, but version 2 is to become the default, after which version 1 will be removed. Set `pricing_version = 2` on new calls and plan to migrate existing ones. + +::: + - **Caller:** Canisters only - **Parameters:** - `url` (`text`): must start with `https://`; max 8192 characters - `max_response_bytes` (`opt nat64`): max response size (up to 2 MB; defaults to 2 MB if not set) - - `method`: `GET`, `HEAD`, or `POST` (replicated); additionally `PUT` and `DELETE` (non-replicated mode only) + - `method`: `GET`, `HEAD`, or `POST` (replicated); additionally `PUT`, `DELETE`, and `PATCH` (non-replicated mode only) - `headers` (`vec record { name : text; value : text }`): request headers (max 64 headers, 8 KiB per name/value, 48 KiB total) - `body` (`opt blob`): request body - `transform` (`opt record { function : func; context : blob }`): response transformation function exported by the calling canister - - `is_replicated` (`opt bool`): select replicated (default) or non-replicated mode + - `is_replicated` (`opt bool`): select replicated (`opt true` or unset) or non-replicated (`opt false`) mode + - `pricing_version` (`opt nat32`): `1` (default, deprecated) or `2`. Version `2` prices the resources the call consumes instead of `max_response_bytes`. The field is not validated: any other value falls back to version `1` without an error. - **Returns:** - `status` (`nat`): HTTP status code - `headers` (`vec record { name : text; value : text }`) - `body` (`blob`) -- **Cycles:** Must be explicitly attached to the call. Charged based on `max_response_bytes`: always set this to a reasonable value to avoid overpaying. +- **Cycles:** Must be explicitly attached to the call. Under pricing version `1`, charged based on `max_response_bytes`: always set this to a reasonable value to avoid overpaying. Under version `2`, charged for the resources actually consumed, and the attached cycles also bound what the call may consume. In replicated mode, multiple replicas make the same request. Use the `transform` function to sanitize non-deterministic parts of the response (timestamps, unique IDs) so replicas can reach consensus. For concept details, see [HTTPS outcalls](../concepts/https-outcalls.md). +### `flexible_http_request` + +Makes an HTTP request from a committee of nodes and returns their individual responses instead of one response the subnet agreed on. Use it for endpoints whose data changes too fast for replicas to agree, and to trade cost against integrity by sizing the committee. + +- **Caller:** Canisters only +- **Parameters:** as for `http_request`, except that there is no `is_replicated` and no `pricing_version`, and one argument is added: + - `method`: `GET`, `HEAD`, and `POST` are always supported; `PUT`, `DELETE`, and `PATCH` only when `min_responses`, `max_responses`, and `total_requests` are all equal + - `replication` (`opt record { min_responses : nat32; max_responses : nat32; total_requests : nat32 }`): how many nodes issue the request, and the fewest and most responses the caller will accept. Must satisfy `0 <= min_responses <= max_responses <= total_requests` and `1 <= total_requests <= N`, where `N` is the subnet's node count as reported by `ic0.subnet_self_node_count`. Defaults to `floor(2 / 3 * N) + 1`, `N`, and `N`. +- **Returns:** `variant { ok : vec http_request_result; err : flexible_http_request_err }`. Both arms arrive as a reply, not a reject: a call that cannot meet the requested replication replies with `err`, carrying a `global_error` of `timeout`, `out_of_cycles`, `responses_too_large`, or `too_many_rejects`, a message, and per-node details. Only failures detected before the requests go out are rejects, e.g. invalid or oversized parameters. +- **Cycles:** Must be explicitly attached to the call. Always priced with pricing version `2`, so the attached cycles also bound what each node may consume. + +A successful call returns between `min_responses` and `max_responses` responses, and may return as few as `min_responses` even when every node answered. The responses do not identify the node that produced them and their order is not specified, so handle any count in that range and reconcile disagreement yourself. + +For the full argument, result, and error types, see [`flexible_http_request`](ic-interface-spec/management-canister.md#ic-flexible_http_request) in the interface specification. + ## Bitcoin API (deprecated) > The management canister Bitcoin API is **deprecated**. Call the Bitcoin canisters directly instead: `ghsi2-tqaaa-aaaan-aaaca-cai` (mainnet) or `g4xu7-jiaaa-aaaan-aaaaq-cai` (testnet). @@ -621,12 +643,13 @@ Cycle costs for management canister calls vary depending on subnet replication f - `ic0.cost_create_canister`: cost of `create_canister` - `ic0.cost_call`: cost of an inter-canister call (base + per-byte) -- `ic0.cost_http_request`: cost of `http_request` +- `ic0.cost_http_request`: cost of `http_request` under pricing version `1` (deprecated) +- `ic0.cost_http_request_v2`: cost of `http_request` under pricing version `2`, and of `flexible_http_request` - `ic0.cost_sign_with_ecdsa`: cost of `sign_with_ecdsa` - `ic0.cost_sign_with_schnorr`: cost of `sign_with_schnorr` - `ic0.cost_vetkd_derive_key`: cost of `vetkd_derive_key` -Methods that require explicit cycle attachment (`create_canister`, `sign_with_ecdsa`, `sign_with_schnorr`, `vetkd_derive_key`, `http_request`) will fail if insufficient cycles are provided. +Methods that require explicit cycle attachment (`create_canister`, `sign_with_ecdsa`, `sign_with_schnorr`, `vetkd_derive_key`, `http_request`, `flexible_http_request`) will fail if insufficient cycles are provided. Under `http_request` pricing version `2`, and for `flexible_http_request`, only the base fee has to be covered up front; the rest of the attached cycles is the call's resource budget. ## Candid interface diff --git a/public/references/ic.did b/public/references/ic.did index fc939abf..8918b2fc 100644 --- a/public/references/ic.did +++ b/public/references/ic.did @@ -124,6 +124,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; }; @@ -365,6 +393,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 { @@ -689,6 +735,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) query;