diff --git a/docs/developer-docs/backend/rust/intercanister.mdx b/docs/developer-docs/backend/rust/intercanister.mdx index ee76c5f77f..1590c8e56d 100644 --- a/docs/developer-docs/backend/rust/intercanister.mdx +++ b/docs/developer-docs/backend/rust/intercanister.mdx @@ -8,69 +8,169 @@ import { MarkdownChipRow } from "/src/components/Chip/MarkdownChipRow"; -Just like users can call canisters, canisters can also call other canisters. This document shows how to use these inter-canister calls in Rust. To fully understand calls, their properties, and common pitfalls and security issues, refer to the section on [inter-canister calls](/docs/current/developer-docs/smart-contracts/advanced-features/async-code). -Our examples will center around tokens. We will write a simple wallet canister that holds tokens on behalf of its owner, and allows the owner to transfer tokens. We'll first show an example of interacting with the ICP ledger, and then also any ledger that supports the ICRC-1 standard. Finally, we will allow the wallet to determine the exchange rate between supported tokens using the exchange rate canister. -## Dependencies and imports +Inter-canister calls can be used to update information between two or more canisters. -We start by listing the dependencies used in this example as specified in `Cargo.toml`. +To demonstrate these inter-canister calls, you'll use an example project called "PubSub". -```rust reference -https://github.com/oggy-dfin/icc_rust_docs/blob/7af82d98f05ebbf6c73641a3013ca15db7944394/src/icc_rust_docs_backend/Cargo.toml#L11-L16 -``` +A common problem in both distributed and decentralized systems is keeping separate services (or canisters) synchronized with one another. While there are many potential solutions to this problem, a popular one is the **publisher/subscriber** pattern, or "PubSub". PubSub is an especially valuable pattern on ICP as its primary drawback, message delivery failures, does not apply. -Next, here are the imports used: +## Prerequisites -```rust reference -https://github.com/oggy-dfin/icc_rust_docs/blob/7af82d98f05ebbf6c73641a3013ca15db7944394/src/icc_rust_docs_backend/src/lib.rs#L1-L8 -``` +Before getting started, assure you have set up your developer environment according to the instructions in the [developer environment guide](./dev-env.mdx). -Furthermore, for simplicity we'll hardcode the owner of the wallet. If you want to test this example interactively, you can set it to your own principal that you can obtain using `dfx identity get-principal`. +Then, download the sample project's files with the commands: -```rust reference -https://github.com/oggy-dfin/icc_rust_docs/blob/81b5dbac5feaddab0bf62fcc2f31fecf42b535c1/src/icc_rust_docs_backend/src/lib.rs#L9-L10 +```bash +git clone https://github.com/dfinity/examples/ +cd examples/rust/pub-sub/ ``` -## Basic ICP ledger transfer: unbounded wait calls -The simplest way to interact with the ICP ledger is to use calls where the caller is willing to wait for the response for an unbounded amount of time. These calls can still fail before reaching the ledger, or even while the ledger is processing the call. but the ledger's response is guaranteed do be delivered to the caller, which is why we also refer to these calls as *guaranteed response* calls. +## Viewing the canister code + +This project is comprised of two canisters: publisher and subscriber. + +The **subscriber** canister contains a record of topics. The **publisher** canister uses inter-canister calls to add topics to the record within the subscriber canister. + +Let's take a look at the `src/lib.rs` file for each of these canisters. + +```rust title="src/publisher/src/lib.rs" +use candid::{CandidType, Principal}; +use ic_cdk::update; +use serde::Deserialize; +use std::cell::RefCell; +use std::collections::BTreeMap; + +type SubscriberStore = BTreeMap; + +thread_local! { +    static SUBSCRIBERS: RefCell = RefCell::default(); +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +struct Counter { +    topic: String, +    value: u64, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +struct Subscriber { +    topic: String, +} + +#[update] +fn subscribe(subscriber: Subscriber) { +    let subscriber_principal_id = ic_cdk::caller(); +    SUBSCRIBERS.with(|subscribers| { +        subscribers +            .borrow_mut() +            .insert(subscriber_principal_id, subscriber) +    }); +} + +#[update] +async fn publish(counter: Counter) { +    SUBSCRIBERS.with(|subscribers| { +        // This example is explicitly ignoring the error. +        for (k, v) in subscribers.borrow().iter() { +            if v.topic == counter.topic { +                let _call_result: Result<(), _> = +                    ic_cdk::notify(*k, "update_count", (&counter,)); +            } +        } +    }); +} +``` -```rust reference -https://github.com/oggy-dfin/icc_rust_docs/blob/e8cefbf8af94b7e595189648fa5de4a00dec7cc7/src/icc_rust_docs_backend/src/lib.rs#L13-L80 +In this code, you can see two inter-canister update calls: `fn subscribe(subscriber: Subscriber)` and `async fn publish(counter: Counter)`. The first method allows for the subscriber canister to make a call to the publisher canister and subscribe to topics. The second method allows the publisher canister to publish information on a topic in the subscriber canister. + +```rust title="src/subscriber/src/lib.rs" +use candid::{CandidType, Principal}; +use ic_cdk::{update, query}; +use serde::Deserialize; +use std::cell::Cell; + +thread_local! { +    static COUNTER: Cell = Cell::new(0); +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +struct Counter { +    topic: String, +    value: u64, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +struct Subscriber { +    topic: String, +} + +#[update] +async fn setup_subscribe(publisher_id: Principal, topic: String) { +    let subscriber = Subscriber { topic }; +    let _call_result: Result<(), _> = +        ic_cdk::call(publisher_id, "subscribe", (subscriber,)).await; +} + +#[update] +fn update_count(counter: Counter) { +    COUNTER.with(|c| { +        c.set(c.get() + counter.value); +    }); +} + +#[query] +fn get_count() -> u64 { +    COUNTER.with(|c| { +        c.get() +    }) +} ``` -Unbounded wait calls, however, have a few downsides: -1. Since there is no bound on when the call will return, the caller may be prevented from upgrading safely. This can in particular be problematic when calling untrusted canisters, such as an arbitrary ledger. -2. They don't scale well, and canisters may be unable to issue such calls when the system is under high load. +In this code, there are three main methods: two inter-canister update methods and a query method. + +The first method, `async fn setup_subscribe(publisher_id: Principal, topic: String)` provides functionality for the publisher canister to subscribe to topics within the `subscriber` canister. This function is called by the publisher canister. -We will next show how to use *bounded wait* calls instead. These calls don't guarantee that the response will be delivered, which is why we also refer to the as *best-effort response calls*. See the section on [inter-canister calls](/docs/current/developer-docs/smart-contracts/advanced-features/async-code) for more information on best-effort vs. guaranteed response calls. +The second method, `fn update_count(counter: Counter)` updates the counter record for each published value in a topic within the subscriber canister. -## ICRC-1 transfers: bounded wait calls +The third method, `fn get_count() -> u64` allows the `Counter` value to be queried and returned in a call. -We will now allow our wallet to transfer tokens on an arbitrary ICRC-1 ledger instead of just the ICP ledger. Ledgers generally charge fees for transfers. While this fee is fixed for the ICP ledger, it may vary for other ledgers. Thus, we start with an example of how to determine the required fee. +## Deploying the canisters -### Learning the transfer fee +Now that you've taken a look at your canisters, let's deploy them. -Querying the transfer fee does not change the ledger state. Thus, it's simple to retry in case that it fails, and the code below implements basic retries. +Open a terminal window on your local computer, if you don’t already have one open. -```rust reference -https://github.com/oggy-dfin/icc_rust_docs/blob/66d36e734bc3a65599a1419ebf1f1b3e520924dc/src/icc_rust_docs_backend/src/lib.rs#L81-L130 +Then run the commands: + +```bash +dfx start --clean --background +dfx deploy ``` -As noted in the example, the code after the first call executes in a different callback. See the sections on [inter-canister calls and async code](/docs/current/developer-docs/smart-contracts/advanced-features/async-code), [properties of call execution](/docs/current/developer-docs/security/security-best-practices/inter-canister-calls) and [security best practices](docs/current/developer-docs/security/security-best-practices/inter-canister-calls) to understand potential security implications for your application when using inter-canister calls. +## Making inter-canister calls -### Transferring tokens +First, let's subscribe to a topic. For example, to subscribe to the "Apples" topic, use the command: -```rust reference -https://github.com/oggy-dfin/icc_rust_docs/blob/e8cefbf8af94b7e595189648fa5de4a00dec7cc7/src/icc_rust_docs_backend/src/lib.rs#L135-L210 +```bash +dfx canister call subscriber setup_subscribe '(principal "", "Apples")' ``` -## Exchange rate canister: attaching cycles +Then, to publish a record to the "Apples" topic, use the command: + +```bash +dfx canister call publisher publish '(record { "topic" = "Apples"; "value" = 2 })' +``` -For our final example, we will use the [exchange rate canister](/docs/current/developer-docs/defi/exchange-rate-canister/) (XRC) to determine the exchange rate between assets, including tokens, but also currencies. The XRC uses [HTTP outcalls](/docs/current/developer-docs/smart-contracts/advanced-features/https-outcalls/https-outcalls-overview) to determine the exchange rate. Similar to ledgers charging transfer fees, the XRC charges a fee to the caller to determine the exchange rate. However, since the XRC doesn't have a token of its own, the XRC fee is paid in cycles rather than a token. The user has to attach cycles to such a call. +Then, you can query and receive the subscription record value with the command: -```rust reference -https://github.com/oggy-dfin/icc_rust_docs/blob/7af82d98f05ebbf6c73641a3013ca15db7944394/src/icc_rust_docs_backend/src/lib.rs#L213-L245 +```bash +dfx canister call subscriber get_count ``` -As noted in the example, for transferring larger amounts of cycles, switch to using unbounded wait calls. Bounded wait calls run the risk of losing cycles; see the section on [inter-canister calls](/docs/current/developer-docs/smart-contracts/advanced-features/async-code) for more details. +The output should resemble the following: + +```bash +(2 : nat64) +``` \ No newline at end of file diff --git a/docs/developer-docs/security/security-best-practices/inter-canister-calls.mdx b/docs/developer-docs/security/security-best-practices/inter-canister-calls.mdx index 5d4da5dd21..a48742bd73 100644 --- a/docs/developer-docs/security/security-best-practices/inter-canister-calls.mdx +++ b/docs/developer-docs/security/security-best-practices/inter-canister-calls.mdx @@ -309,17 +309,13 @@ Finally, note that the same guard can be used in several methods to restrict par ### Security concern -As stated by the [Property 6](/docs/current/references/message-execution-properties#message-execution-properties), inter-canister calls can fail in which case they result in a **reject**. See [reject codes](/docs/current/references/ic-interface-spec#reject-codes) for more detail. The caller must correctly deal with the reject cases, as they can happen in normal operation, because of insufficient cycles on the sender or receiver side, or even for reasons outside of the sender's or receiver's control, like the system (Internet Computer) being under heavy load (e.g., message queues becoming full). +As stated by the [Property 6](/docs/current/references/message-execution-properties#message-execution-properties), inter-canister calls can fail in which case they result in a **reject**. See [reject codes](/docs/current/references/ic-interface-spec#reject-codes) for more detail. The caller must correctly deal with the reject cases, as they can happen in normal operation, because of insufficient cycles on the sender or receiver side, or because some data structures like message queues are full. -Not handling the reject cases correctly is risky: For example, if a ledger transfer results in a reject, the callback dealing with that error must interpret it correctly. That is, it should be interpreted as "the transfer did not happen", unless: - -1. the call was issued as a best-effort response call, and the system responded with a `SYS_UNKNOWN` reject code. In this case, the caller cannot be a priori sure whether the call took effect or not. -2. the system responded with a `CANISTER_ERROR` reject code. This indicates a bug in the ledger canister. In this case, it is still possible that the call had a partial effect on the ledger canister. -3. the system responded with a `CANISTER_REJECT` reject code. This means that the call was explicitly rejected by the ledger canister. Normally, this indicates that the transfer didn't happen, but this depends on the ledger canister. The ICP ledger canister for example never rejects calls explicitly. +Not handling the error cases correctly is risky: For example, if a ledger transfer results in an error, the callback dealing with that error must interpret it correctly. That is, it must be interpreted as "the transfer did not happen". ### Recommendation -When making inter-canister calls, always handle the error cases (rejects) correctly. Other than the `SYS_UNKNOWN` error code, these errors imply that the message has not been successfully executed. For `SYS_UNKNOWN`, follow the guidelines in the [safe retries & idempotency](/docs/current/developer-docs/smart-contracts/best-practices/idempotency) document to handle this scenario correctly. +When making inter-canister calls, always handle the error cases (rejects) correctly. These errors imply that the message has not been successfully executed. ## Be aware of the risks involved in calling untrustworthy canisters @@ -327,15 +323,15 @@ When making inter-canister calls, always handle the error cases (rejects) correc - If inter-canister calls are made to potentially malicious canisters, this can lead to DoS issues or there could be issues related to candid decoding. Also, the data returned from a canister call could be assumed to be trustworthy when it is not. -- When a canister `C1` calls a canister `C2` using a guaranteed-response inter-canister call, and `C2` stalls the response indefinitely by not responding, the result would be a DoS on `C1`. Additionally, since the call registers a callback on `C1`, `C1` can no longer be stopped because of the outstanding callback, and thus can no longer be cleanly upgraded. Recovery would require wiping the state of the canister by reinstalling it. Note that even if `C2` was trustworthy it could still stall indefinitely. This could happen due to a bug in`C2` (which is rather unlikely to occur). But other causes could be a stall of the subnet hosting `C2` (assuming that `C1` and `C2` are on different subnets), or `C2` making a downstream call to an untrusted canister `C3`. +- When another canister is called with a callback being registered, and the receiver stalls the response indefinitely by not responding, the result would be a DoS. Additionally, that canister can no longer be upgraded if it has callbacks registered. Recovery would require wiping the state of the canister by reinstalling it. Note that even a trustworthy canister could have a bug causing it to stall indefinitely. However, such a bug seems rather unlikely to occur. - In summary, this can DoS a canister, consume an excessive amount of resources, or lead to logic bugs if the behavior of the canister depends on the inter-canister call response. ### Recommendation -- Making inter-canister calls to trustworthy canisters is safe, except for the rather unlikely case that there is a bug in the callee or its subnet that makes it stall forever. +- Making inter-canister calls to trustworthy canisters is safe, except for the rather unlikely case that there is a bug in the callee that makes it stall forever. -- Interacting with untrustworthy canisters is still possible by using best-effort response calls, which cannot be stalled by the recipient. In particular, when using calls that do not change the callee's state (e.g., just fetching information), prefer using best-effort response calls. Another option is using guaranteed response calls through a state-free proxy canister which could easily be re-installed if it is attacked as described above and is stuck. When the proxy is reinstalled, the caller obtains an error response to the open calls. +- Interacting with untrustworthy canisters is still possible by using a state-free proxy canister which could easily be re-installed if it is attacked as described above and is stuck. When the proxy is reinstalled, the caller obtains an error response to the open calls. - Sanitize data returned from inter-canister calls. @@ -352,7 +348,6 @@ Loops in the call graph (e.g. canister A calling B, B calling C, C calling A) ma ### Recommendation -- Avoid such loops, or rely on best-effort response calls instead, since these provide timeouts. +- Avoid such loops. - For more information, see [current limitations of the Internet Computer](https://wiki.internetcomputer.org/wiki/Current_limitations_of_the_Internet_Computer), section "Loops in call graphs". - diff --git a/docs/developer-docs/smart-contracts/advanced-features/async-code.mdx b/docs/developer-docs/smart-contracts/advanced-features/async-code.mdx index bc5a3ecf68..9c1e2830a0 100644 --- a/docs/developer-docs/smart-contracts/advanced-features/async-code.mdx +++ b/docs/developer-docs/smart-contracts/advanced-features/async-code.mdx @@ -1,98 +1,90 @@ --- -keywords: [beginner, concept, async code, inter-canister calls, async inter-canister, async] +keywords: [advanced, concept, async code, inter-canister calls, async inter-canister, async] --- import { MarkdownChipRow } from "/src/components/Chip/MarkdownChipRow"; -# Inter-canister calls & async code +# Async code and inter-canister calls - + -The ICP allows canisters to seamlessly interact with other canisters by calling their methods, just like external users call canisters. You are likely to need inter-canister calls early on: they are necessary to transfer the ICP token, or to access certain system functionality through the [management canister](/docs/current/developer-docs/smart-contracts/advanced-features/management-canister). This remote procedure call (RPC) mechanism, based on a request-response paradigm, will feel familiar if you are coming from Ethereum or other smart-contract enabled blockchains, but there are also important differences that you should understand: +In programming, the [async/await pattern](https://en.wikipedia.org/wiki/Async/await) is a syntactic feature of many programming languages that allows an asynchronous, non-blocking function to be structured in a similar way to an ordinary synchronous function. -- On ICP, inter-canister calls are **asynchronous**. That means that when a canister `C1` calls a canister `C2`, `C1` can process other calls while waiting on the response from `C2`. This significantly increases the canisters' scalability. However, it also means that canister authors have to correctly handle calls executing concurrently. In particular, this differs from the transactional nature of Ethereum's smart contract calls. +On ICP, `async/await` is useful in the context of inter-canister calls. Inter-canister calls cannot be handled synchronously (as the two canisters +interacting might not even be in the same subnet), so when a canister sends a call to another, it will have to wait until a response comes back +before it can continue processing the call it received. -- Since the calls are asynchronous, they are based on **callbacks**: When issuing a request to the callee canister, the caller also specifies a callback that will handle the callee's response. Many programming languages provide syntactic sugar for handling asynchronous calls in the form of `async/await` syntax, which obviates the need for explicit callbacks and allows structuring asynchronous code similar to an ordinary synchronous function. Motoko, the Rust Canister Development Kit (CDK), Azle (Typescript CDK) and Kybra (Python CDK) all support such syntactic sugar. +The `async/await` pattern makes this look straightforward, as the developer can write code as if processing happens sequentially and the call to +the callee is handled “synchronously”. However, given ICP's message passing model, one has to be careful in case there are [concurrent calls that +message executions can interleave](/docs/current/references/message-execution-properties) as these can lead to inconsistencies if they are not anticipated. -- The requests are not guaranteed to be delivered. This allows the system to use its resources more optimally overall, but again differs from Ethereum and many other blockchains, and needs to be handled correctly. Moreover, calls can be made in either best-effort or guaranteed response modes. The best-effort mode supports a higher volume of messages and is much more resilient under high system load, but in this mode the "true" response is not guaranteed to be delivered, and can be masked by a system generated error response instead. + ## Language runtime for asynchronous code -- ICP canisters can be written in any language that compiles down to Wasm. That means that the caller and callee canisters can be written in different languages, and need some common data format to exchange messages. While the ICP doesn't enforce a data format, [Candid](/docs/current/developer-docs/smart-contracts/candid/candid-concepts) is the de facto standard. +Most languages implement asynchronous code through constructs that they typically call [futures](https://docs.rs/futures/latest/futures/) +([promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) in Javascript). A future is a representation +of an eventual value produced by some asynchronous computation. Typically, a future is modeled to have different states depending on whether the +result of the asynchronous computation is ready or not. The language runtime can poll the future to learn about its result. As long as the result +is not ready, the code will typically yield execution and the future will be polled again later. -## Service discovery +In order to implement inter-canister calls on ICP, there would need to be a future which, once created, takes care of calling +[the respective system APIs](/docs/current/references/ic-interface-spec#system-api-call) to make the call and will +become ready once the response from the calling canister is received. The language runtime can poll the future once it's ready and retrieve the +result of the inter-canister call. -Before you can call other canisters that are not part of your project, you need to learn the available endpoints (methods) that you can call, as well as their arguments and return values. Candid serves both as a data format and an interface description language. The IC SDK will by default bundle the interface description (if available) in the `candid:service` metadata section of the canister code. For example, you can examine the interface of the ICP ledger canister (whose principal is `ryjl3-tyaaa-aaaaa-aaaba-cai`) using [`dfx`](/docs/current/developer-docs/getting-started/install). +## Inter-canister calls -```bash -dfx canister metadata ryjl3-tyaaa-aaaaa-aaaba-cai candid:service --network ic -``` - -If you are using Candid, you should also write or generate service description files for your own canisters so that other canisters and external applications can easily call into your canister. See the [Candid documentation](/docs/current/developer-docs/smart-contracts/candid/candid-howto) for more instructions. - -When the canister authors support it, remote canisters can also be made "pullable", such that you can easily test against the canisters locally. See the [dfx documentation](/docs/current/developer-docs/developer-tools/cli-tools/dfx-json#pullable-canisters) for more details. - -## Performing inter-canister calls - -Once you know the endpoints you want to call, in most cases you will want to use the CDK of your language to perform the calls. The CDK will generally take care of Candid encoding and decoding, and provide an `async/await` based syntax for your inter-canister calls. Refer to the language documentation -([Motoko](/docs/current/motoko/main/writing-motoko/intercanister-calls), [Rust](/docs/current/developer-docs/backend/rust/intercanister), [Typescript](https://demergent-labs.github.io/azle/), [Python](https://demergent-labs.github.io/kybra/)) -for more details. - - -### Low-level API - -Under the hood, the CDKs use the low-level Wasm API to perform inter-canister calls, and set appropriate callbacks to handle responses. Most users will not need to interact with this API directly, but you can find more details in the [Internet Computer interface specification](/docs/current/references/ic-interface-spec/#system-api-call) if you need to perform workflows or use functionality that might not be directly exposed by your CDK or if you intend to implement a CDK yourself. - - -### Attaching cycles - -[Cycles](/docs/current/developer-docs/gas-cost) are the currency in which canisters pay for their resource usage. Canisters can send some of the cycles they hold to other canisters. This can be done either by directly attaching cycles to any call to the target canister, or by calling the dedicated [`deposit_cycles`](/docs/current/references/ic-interface-spec/#ic-deposit_cycles) method of the management canister. When attaching cycles to a call (i.e., not using `deposit_cycles`), the target canister must explicitly accept part or all of the sent cycles; the remainder is refunded to the caller. - -:::info -Note that cycles may get dropped when using best-effort response calls. See the section on #[guaranteed vs best-effort response calls](#guaranteed-response-vs-best-effort-response-calls) for more details. -::: - -Some endpoints require the caller to attach cycles to the call. For example, the [onchain signature](/docs/current/developer-docs/smart-contracts/signatures/signing-messages-t-ecdsa) operations of the management canister require cycles to be attached. Requiring cycles can be used to implement a "direct gas" model, as opposed to the default ["reverse gas"](/docs/current/developer-docs/gas-cost) model of ICP. - -Refer to the language documentation ([Motoko](/docs/current/motoko/main/writing-motoko/intercanister-calls), [Rust](/docs/current/developer-docs/backend/rust/intercanister), [Typescript](/docs/current/developer-docs/backend/typescript/), [Python](/docs/current/developer-docs/backend/python/)) for details on how to send and accept cycles. - -## Guaranteed response vs best-effort response calls - -ICP supports two kind of inter-canister calls: +Let's look at an example where two canisters interact with each other. In the following example, canister A calls into canister B. -* Guaranteed response calls provide the caller with the guarantee that if the callee produces a response (which may be unsuccessful, i.e., an error during call processing), that exact response will be delivered to the caller. Furthermore, if the request isn't successfully delivered to the callee (which can happen during high load, callee running out of cycles, and other reasons), the response will notify the caller of this. - -* Best-effort response calls drop this guarantee. They allow the caller to specify a timeout (which is capped from above by the system to some maximum value, such as 5 minutes), after which the system will return an error to the caller. The request may or may not be processed by the callee; the system is free to drop the request, but it may also deliver it to the target. The caller must, if necessary, determine whether the call took place or not by some other mechanism. Any cycles associated with a dropped message (request or response) disappear. - -Guaranteed response calls are a legacy call mechanism. While on the surface they make it easier to write correct applications, they also suffer some significant drawbacks: +``` +/// Canister A +async fn foo() { + do_work(); + call(canister_b, 'bar').await; + do_more_work(res); +} -* If the callee is unresponsive (which could happen because of high load on the callee, high load or an outage on the callee's subnet, or even because the callee is malicious and delays the response on purpose), the caller canister is stalled and has no control over when it can resume processing or provide an answer. Furthermore, the caller canister cannot be safely stopped and upgraded. -* They do not scale well. When the system is under high load, canisters may be unable to issue further inter-canister calls. +/// Canister B +fn bar() { + do_some_more_work(); +} +``` -Best-effort calls address both of these problems. However, they can require more effort on the part of the caller to handle the additional error case. Here are some guidelines how to choose between the two types of calls: +When canister A calls B, a future that represents the result of the call is created. The future calls [the system API](/docs/current/references/ic-interface-spec#system-api-call) +to enqueue the outgoing call. The future is not ready yet as it needs the response from canister B, so the code will yield and the message +execution for canister A will stop at the point before `do_more_work`. -* Always prefer best-effort response calls for calls that don't change the state of the callee, i.e., reads. -* For endpoints that change the state of the callee, the best practice is to make such endpoints amenable to [safe retries](/docs/current/developer-docs/smart-contracts/best-practices/idempotency). Note that making user-facing endpoints amenable to safe retries is a good idea anyway, as it's needed to safely handle external user calls. Use best-effort responses calls for such endpoints, and handle the additional edge cases. -* Use guaranteed response calls for endpoints that mutate state and do not enable safe retries, or calls that perform larger cycle transfers. As mentioned, best-effort response calls will currently lose cycles when request/responses are dropped. Be aware of the limitations of guaranteed response calls listed above. If safe upgrades are needed, consider using a stateless proxy canister (see the [security best practices](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/inter-canister-calls) for more information). +Once the response from B is delivered, the future will be ready and contain the result of the call. Once polled (by the language's runtime when +the callback handler is invoked) it will pass the result to canister A and execution can proceed with the remaining part (i.e. `do_more_work`). -## `async/await` syntax, concurrency and state changes +## Asynchronous functions -The ``async/await`` syntax allows canister methods to issue asynchronous inter-canister calls and still be structured like an ordinary synchronous function. In this syntax, a method `foo` on a canister `A` could be written something like this: (in a Rust-like syntax): +In the following example, another call is added to a local function to canister A before making the inter-canister call to canister B. ``` +/// Canister A async fn foo() { - do_work(); + baz().await; call(canister_b, 'bar').await; do_more_work(res); } -``` -While this looks like an ordinary function, there are important differences in behavior between synchronous functions and functions that perform inter-canister calls. - -First, unlike a synchronous function (used on, say Ethereum), multiple method call executions on the same canister can be executed concurrently in the presence of inter-canister calls. This increases the canister's throughput, but it also means that the code needs to be correct also in the presence of concurrent behaviors. In particular, the developers have to ensure that no re-entrancy issues occur. +async fn baz() { + do_local_work(); +} -Second, canister methods that use inter-canister calls are **not atomic**. A failure somewhere in the method might not roll back all the changes that the method performed. Under the hood, such methods use the [low-level Wasm API](#low-level-api) calls, and are translated into multiple *message handler Wasm functions*: An initial handler function that handles the method call itself (corresponding to `do_work` above), and other handler functions that serve as callbacks to handle the responses for any inter-canister calls that have been issued (`do_more_work` in the example above). The execution of each message handler is atomic on its own; an error (more precisely, a Wasm trap) rolls back only the changes performed by the current message handler, but does not affect the changes made by the other handlers. +/// Canister B +fn bar() { + do_some_more_work(); +} +``` -For these reasons, you should understand how the CDK for your language of choice desugars the `async/await` syntax into Wasm code in order to write correct code in the presence of inter-canister calls. In particular, you should understand where the message handler boundaries are, since these are both "commit points" for state changes and also potential interleaving points for concurrent executions. +When `baz().await` is called a future is created to represent its result. The result can actually be immediately available since there's only +local computation (i.e. no inter-canister call involved) happening in `baz`. So, the future can return a result directly when polled and message +execution can still proceed up to the point where the call to canister B is made (similar to the example from the previous section). -A [separate document](https://internetcomputer.org/docs/current/references/message-execution-properties) contains more details on interleaving/commit points and message execution properties. Refer to the [security best practices](/docs/current/developer-docs/security/security-best-practices/inter-canister-calls) for advice on how to handle concurrency and state rollback issues correctly. Finally, refer to the CDK documentation for your language to learn more about desugaring of `async/await` and interleaving/commit points. +The above behavior is one possibility depending on how the language runtime handles the `async/await` pattern. Rust follows this approach. +In contrast, Motoko takes another approach where every async/await call will be converted to an inter-canister call, more specifically a self-call +in case a local function is called. This means that in Motoko `foo` would be split across 3 message executions -- the first would be until `baz` is +called, the second until canister B is called and the final one until the end of the function body. \ No newline at end of file diff --git a/docs/developer-docs/smart-contracts/best-practices/idempotency.mdx b/docs/developer-docs/smart-contracts/best-practices/idempotency.mdx index e0e198246c..ab6d9596d1 100644 --- a/docs/developer-docs/smart-contracts/best-practices/idempotency.mdx +++ b/docs/developer-docs/smart-contracts/best-practices/idempotency.mdx @@ -15,9 +15,8 @@ In the case of network issues or other unexpected behavior, ICP clients (such as ingress update calls may be unable to determine whether their ingress request has been processed. For example, this can happen if the client loses its connection until the request status has been removed from the state tree, since ICP will remove the request from the system state tree some time after the ingress expiry. -Similarly, canisters which call other canisters using calls with best-effort responses may be unable to determine whether the call was successful or not. -This can be risky as the callers (external users or applications for ingress messages, or canisters for inter-canister calls) might decide to retry transactions, potentially leading to serious security vulnerabilities such as double spending. +This can be risky as the application might decide to retry transactions, potentially leading to serious security vulnerabilities such as double spending. Thus, it is important to design and/or use canister APIs such that it is possible to retry requests safely, even when the ICP provides no information about previous request attempts. This page describes general approaches that both the canister authors and the clients can adopt to enable safe retries. @@ -27,12 +26,10 @@ Please read the [reference on ingress messaging](/docs/current/references/ingres We say that a canister endpoint is idempotent if executing it multiple times is equivalent to executing it once.[^1] Whenever an endpoint is idempotent or can be made idempotent by the developer, this provides an easy way to implement safe retries. -Given an idempotent endpoint, you can implement retries from an external application by retrying the call until you observe a certified response, either a replied or rejected status; see the illustration below. If such a response is ever observed, it's sure that the transaction has been executed at least once which, thanks to idempotency, has the same result as executing it exactly once. However, the application may not be willing to wait for a response indefinitely and a timeout could be implemented. Upon timeout, an error should be displayed to the user instructing them to wait until the latest message that has been sent has expired (as defined by the request's `ingress_expiry`) and then manually check the status of the transaction. Ideally, timeouts should be rare and not occur during normal operation. +Given an idempotent endpoint, you can implement retries by retrying the call until you observe a certified response, either a replied or rejected status; see the illustration below. If such a response is ever observed, it's sure that the transaction has been executed at least once which, thanks to idempotency, has the same result as executing it exactly once. However, the application may not be willing to wait for a response indefinitely and a timeout could be implemented. Upon timeout, an error should be displayed to the user instructing them to wait until the latest message that has been sent has expired (as defined by the request's `ingress_expiry`) and then manually check the status of the transaction. Ideally, timeouts should be rare and not occur during normal operation. ![Retrying an idempotent call](/img/docs/retry_idempotency.png) -The situation is similar for inter-canister calls with best-effort responses. Given an idempotent endpoint, the calling canister can keep retrying until a response other than `SYS_UNKNOWN` is observed, or give up after a timeout, if waiting indefinitely is not an option. - Below are two approaches to making endpoints idempotent: sequence numbers and (time window) ID deduplication. ### Update sequence numbers @@ -44,13 +41,13 @@ The advantages of this approach are: 1. When applicable, it has a modest memory footprint because only the next expected sequence number must be stored (for example, per active account). The approach also has some disadvantages: -1. It limits the throughput. When per-caller sequence numbers are used, it means that the caller can generally perform only one ingress call per consensus block, translating to a throughput of about 1 ingress call per second for that user. The situation is better for inter-canister calls, as the requests (if delivered) will be delivered in the order in which they were sent. Thus, the calling canister can issue multiple requests simultaneously, using appropriate sequence numbers. Under normal load, all requests should be delivered. However, under heavy load where the system may drop some requests, requests that follow such a dropped request may become invalid. -1. It limits concurrency. The user has to sequentialize all their calls. This is straightforward to do when the user is another canister, but it can be much more difficult when the canister is called through ingress messages. In particular, it's complicated when the user is using multiple clients or devices to access the canister, for example. This concurrency problem also makes the approach inapplicable to cases where anonymous users are allowed to trigger update calls. +1. It limits the throughput. When per-caller sequence numbers are used, it means that the caller can generally perform only one ingress call per consensus block, translating to a throughput of about 1 call per second for that user. +1. It limits concurrency. The user has to sequentialize all their calls. This can be difficult when the user is using multiple clients or devices to access the canister, for example. This concurrency problem also makes the approach inapplicable to cases where anonymous users are allowed to trigger update calls. 1. If the sequence number is stored per user or per account, tracking them for too many users can exhaust the canister memory, even if each individual number is small. This could, e.g., be exploited by an attacker to exhaust the memory. The approach is thus best suited for cases where the user has to pay for the usage in some way (e.g., the ledgers usually require a fee to both create an account and transfer funds), which thwarts attackers by requiring them to invest significant funds in an attack. ### ID deduplication -Another approach to idempotency is to make the calls uniquely identifiable on the receiving canister side (e.g., by using user-chosen IDs, sequence numbers, or a combination of several argument fields) to make sure a given call is executed at most once. The canister then deduplicates calls before executing them; if a call with the same ID has been executed previously, the new call is simply ignored (potentially returning the result of the previous call). Thus, the user can safely keep retrying the call until they get a response. +Another approach to idempotency is to make the calls uniquely identifiable on the canister side (e.g., by using user-chosen IDs, sequence numbers, or a combination of several argument fields) to make sure a given call is executed at most once. The canister then deduplicates calls before executing them; if a call with the same ID has been executed previously, the new call is simply ignored (potentially returning the result of the previous call). Thus, the user can safely keep retrying the call until they get a response. For example, the ICRC ledger standard provides deduplication in this way. Using identical values for all call parameters, including the `created_at_time` and `memo` parameters, when issuing a transaction makes the transaction call idempotent by deduplicating calls with the same parameters. @@ -63,7 +60,7 @@ But even with this improvement used in the ledgers, the time window approach imp Relying solely on a time window for deduplication does not guarantee bounded memory usage. In theory, an unlimited number of updates could occur within the time window, though in practice, this is constrained by the scaling limits of the ICP. The ICP/ICRC ledgers thus also define a maximum capacity: a limit on the number of deduplicated transactions (i.e., deduplication IDs) that can be stored in their deduplication store. Once this capacity is reached, further transactions are rejected until older transactions expire from the deduplication store at the end of the time window. Yet another extension of the approach is to guaranteed deduplication for the stated time window as above, but keep storing deduplication IDs even beyond that window, as long as the capacity is not reached. This way, the clients obtain a hard deduplication guarantee for the time window, and a best-effort attempt to deduplicate transactions even past the window. -An alternative is to do away with the time window and store the deduplication data forever. This requires storing this data in multiple canisters in order to prevent exhausting canister memory, similar to how the ICP/ICRC ledgers store transaction data in the archive canister. This shifts the tedious part of querying the deduplication data (e.g., ledger blocks) from the user to the canister. +An alternative is to do away with the time window, and store the deduplication data forever. This requires multiple canisters to prevent exhausting the canister memory, similar to how the ICP/ICRC ledgers store the transaction data in the archive canister. This shifts the tedious part of querying the deduplication data (e.g., ledger blocks) from the user to the canister. Summarizing, the advantages of this approach are: 1. It can support high throughput. @@ -83,26 +80,24 @@ In absence of idempotent endpoints, or even in addition to them, clients may be If the canister, in addition to the update endpoint, also exposes a query that can inform the user of the result of the update, the client can also use this for safe retries as follows: 1. Attempt to perform the update. -1. If the result of the update is unknown (e.g., not present in the ingress history anymore, or a `SYS_UNKNOWN` error is returned for an inter-canister call), query the call result endpoint to determine whether the update was applied or not. Moreover, one needs to ensure that the previously sent call cannot be applied in the future. If both of these are true, the call might be retried or safely reported as failed. +1. If the result of the update is unknown (e.g., not present in the ingress history anymore), query the call result endpoint to determine whether the update was applied or not. Moreover, one needs to ensure that the previously sent call cannot be applied in the future. If both of these are true, the call might be retried or safely reported as failed. In practice, this pattern may be more complicated. For example, the ICP ledger exposes a `query_blocks` method that can be used to implement the above pattern for transfers initiated as ingress messages: 1. Call the `query_blocks` method on the ledger to determine what the last block (as specified in the `chain_length` field of the response) currently is. Let's call this `last_block`. 1. Attempt to perform a transfer. This ingress message includes an `ingress_expiry` field. -1. If the result of the transfer is unknown, ensure that the transfer will not be applied at a later point: - * If using ingress messages, call the `read_state` endpoint on the ledger canister to obtain the `/time` branch of the system state tree. Repeat this until the reported time exceeds the `ingress_expiry` time. - * If using inter-canister calls, perform all subsequent calls (`query_blocks`) listed below from the same canister that initiated the transfer. The [ordering guarantees](/docs/current/references/message-execution-properties) then ensure that the transfer cannot happen later. -1. Call the `query_blocks` method on the ledger again to retrieve all ledger blocks since `last_block`, and check that the `timestamp` also exceeds the `ingress_expiry` time. In case of failure, retry until a result is obtained. Then, scan through the returned blocks to determine whether the transaction has been included or not. +1. If the result of the transfer is unknown, call the `read_state` endpoint on the ledger canister to obtain the `/time` branch of the system state tree. Repeat this until the reported time exceeds the `ingress_expiry` time. This ensures that the transfer will not be applied at a later point. +1. Call the `query_blocks` method on the ledger again to retrieve all ledger blocks since `last_block`, and check that the `timestamp` also exceeds the `ingress_expiry` time. Then, scan through the returned blocks to determine whether the transaction has been included or not. ### 2-step transfers Another approach applicable to ledgers (such as ICRC-1 or ICP) is to perform transfers in two steps: 1. First, transfer the tokens to an intermediate subaccount of the sender that's specific to this transaction. For example, if the transaction has a unique ID, the client can hash the ID to obtain a subaccount. The transferred amount should be the desired amount plus the ledger transaction fee. -1. If the result of the above transfer is unknown, query the balance of the transaction-specific subaccount. Like in the ["queryable call result"](#queryable-call-results) approach, if using ingress messages, this should be repeated until the `timestamp` accompanying the response exceeds the `ingress_expiry`. If the balance is 0, the transaction can safely be reported as failed, or it can be retried (starting from step 1). If the balance is at least the expected balance, one can proceed. +1. If the result of the above transfer is unknown, query the balance of the transaction-specific subaccount. Like in the ["queryable call result"](#queryable-call-results) approach, this should be repeated until the `timestamp` accompanying the response exceeds the `ingress_expiry`. If the balance is 0, the transaction can safely be reported as failed, or it can be retried (starting from step 1). If the balance is at least the expected balance, one can proceed. 1. If the transfer to the transaction-specific subaccount succeeded (as determined either by the transfer result or by the balance query above), the client sends another transfer from the transaction-specific subaccount to the desired target account. This can be repeated as many times as necessary until a result of the call is known. Once a result is known, the overall transfer can be declared as succeeded, even if this step fails with an error, as this signifies that some previous attempt to transfer the money to the target succeeded. [^1]: "Equivalent" is meant from the user perspective here. Multiple executions may trigger changes such as those in the canister's cycle balance, but they are not relevant for the user. - [^2]: More precisely, the ledger also allows for a small time drift of `created_at_time` into the future, which has to be taken into account when clearing the deduplication window. + [^2]: More precisely, the ledger also allows for a small time drift of `created_at_time` into the future, which has to be taken into account when clearing the deduplication window. \ No newline at end of file diff --git a/docs/references/message-execution-properties.mdx b/docs/references/message-execution-properties.mdx index f5c2b8d6c4..ac33713ef8 100644 --- a/docs/references/message-execution-properties.mdx +++ b/docs/references/message-execution-properties.mdx @@ -6,36 +6,35 @@ import { MarkdownChipRow } from "/src/components/Chip/MarkdownChipRow"; ## Asynchronous messaging model -ICP relies on an asynchronous messaging model. Compared to synchronous messaging like on Ethereum, this provides performance advantages because multiple calls can be executed concurrently — and also in parallel, when multiple canisters are involved. However, asynchronous message execution can also lead to sometimes unexpected or unintuitive behavior. Therefore, it is important to understand the properties of message execution. Potential security issues that arise in this model, such as reentrancy bugs, are discussed in the [security best practices on inter-canister calls](/docs/current/developer-docs/security/security-best-practices/inter-canister-calls). +ICP relies on an asynchronous messaging model. Compared to synchronous messaging like on Ethereum, this provides performance advantages because individual messages can be executed in parallel. However, asynchrounous message execution can also lead to sometimes unexpected or unintuitive behavior. Therefore, it is important to understand the properties of message execution. Potential security issues that arise in this model, such as reentrancy bugs, are discussed in the [security best practices on inter-canister calls](/docs/current/developer-docs/security/security-best-practices/inter-canister-calls). The [community conversation on security best practices](https://www.youtube.com/watch?v=PneRzDmf_Xw&list=PLuhDt1vhGcrez-f3I0_hvbwGZHZzkZ7Ng&index=2&t=4s) also discusses the messaging properties. ## Message execution properties -To interact with a canister's methods, there are two primary types of **entry points** or **methods** that can be called, update and query methods. If the Rust CDK is used, these are usually annotated with `#[update]` or `#[query]`, respectively. In Motoko, updates are declared as `public func`, and queries use the dedicated keyword: `public query func`. +To interact with a canister's methods, there are two primary types of **calls** that can be used: [update](/references/ic-interface-spec.md#http-call) calls and [query](/docs/current/references/ic-interface-spec#http-query) calls. -These entry points can be called either by external users through the IC's HTTP interface, or by other canisters. ICP also supports additional [entry points](/references/ic-interface-spec.md#entry-points) such as heartbeats, timers, initialization or upgrade hooks. These cannot be called directly, but the properties listed in this document are still relevant for them, and in particular heartbeats and timers, as they behave like update methods that are called by the system. +If the Rust CDK is used, these are usually annotated with `#[update]` or `#[query]`, respectively. In Motoko, updates are declared as `public func`, and queries us the dedicated keyword: `public query func`. ICP also supports additional [entry points](/references/ic-interface-spec.md#entry-points) such as heartbeats, timers, initialization or upgrade hooks. -A **message execution** is a set of consecutive instructions that a subnet executes when a canister's method is invoked. The code execution for any such method can be split into several message executions if the method makes inter-canister calls. The following properties are essential: +A **message** is a set of consecutive instructions that a subnet executes for a canister. The code execution for any such entry point can be split into several messages if inter-canister calls are made. The following properties are essential: -- **Property 1**: Only a single message execution is run at a time per canister. Message execution within a single canister is atomic and sequential, and never parallel. +- **Property 1**: Only a single message is processed at a time per canister. Message execution is atomic and sequential, and never parallel. -Note that parallel message execution over multiple canisters is possible — this property talks about just a single canister. +- **Property 2**: Each call, query or update, triggers a message. When using `await` on the response from an inter-canister call, the code after the `await` (the callback, highlighted in blue) is executed as a separate message. -- **Property 2**: Each downstream call that a canister makes, query or update, triggers a message. When using `await` on the response from an inter-canister call, the code after the `await` (the callback, highlighted in blue) is executed as a separate message execution. +For example, consider the following Motoko code: + +![example_highlighted_code](./_attachments/example_highlighted_code.png) + +The first message that is executed here are lines 2-3, until the inter-canister call is made using the `await` syntax (orange box). The second message executes lines 3-5: when the inter-canister call returns (blue box). This part is called the _callback_ of the inter-canister call. The two messages involved in this example will always be scheduled sequentially. - For example, consider the following Motoko code: - - ![example_highlighted_code](./_attachments/example_highlighted_code.png) - - The first message execution spans the lines 2-3, until the inter-canister call is made using the `await` syntax (orange box). The second message execution spans lines 3-5 when the inter-canister call returns (blue box). This part is called the _callback_ of the inter-canister call. The two message executions involved in this example will always be scheduled sequentially. - :::info -Note that an `await` in the code does not necessarily mean that an inter-canister call is made and thus a message execution ends and the code after the `await` is executed as a separate message execution (callback). Async code with the `await` syntax (e.g. in Rust or Motoko) can also be used "internally" in the canister, without issuing an inter-canister call. In that case, the code part including the `await` will be processed within a single message execution. For Rust, both cases are possible if `await` is used. An inter-canister call is only made if the system API `ic0.call_perform` is called, e.g. when awaiting result of the CDK's `call` method. In Motoko, `await` always commits the current state and triggers a new message send, while `await*` does not necessarily commit the current state or trigger new message sends. See [Motoko `await`](/docs/current/motoko/main/reference/language-manual#await) vs. [Motoko `await*`](/docs/current/motoko/main/reference/language-manual#await-1). +Note that an `await` in the code does not necessarily mean that an inter-canister call is made and thus a message execution ends and the code after the `await` is executed as a separate message (callback). Async code with the `await` syntax (e.g. in Rust or Motoko) can also be used "internally" in the canister, without issuing an inter-canister call. In that case, the code part including the `await` will be processed within a single message. For Rust, both cases are possible if `await` is used. An inter-canister call is only made if the system API `ic0.call_perform` is called, e.g. by using the CDK's call method. In Motoko, `await` always commits the current state and triggers a new message send, while `await*` does not necessarily commit the current state or trigger new message sends. See [Motoko `await`](/docs/current/motoko/main/reference/language-manual#await) vs. [Motoko `await*`](/docs/current/motoko/main/reference/language-manual#await-1). ::: - + :::info -Note that, in the Rust CDK, it is the `await` expression that triggers the canister call, rather than invoking the `call` function itself. That is, a call that is not `await`-ed is never executed. In Motoko, if the code does not `await` the response of a call, the call is still made, but the code after the call is executed in the same message execution, until the next inter-canister call is triggered using `await`. +Note that if the code does not `await` the response, the code after the callback is executed in the same message, until the next inter-canister call is triggered using `await`. + Also, multiple outgoing calls can be triggered in parallel from the same message execution; see the [Rust](https://internetcomputer.org/docs/current/references/samples/rust/parallel_calls/) and [Motoko](https://internetcomputer.org/docs/current/references/samples/motoko/parallel_calls/) examples. ::: @@ -45,36 +44,19 @@ Also, multiple outgoing calls can be triggered in parallel from the same message Note that this property only gives a guarantee on when the request messages are executed, but there is no guarantee on the ordering of the responses received. ::: -- **Property 4**: Multiple message executions, e.g., from different calls, can interleave and have no reliable execution +- **Property 4**: Multiple messages, e.g., from different calls, can interleave and have no reliable execution ordering. - Property 3 provides a guarantee on the order of message executions on a target canister. However, if multiple calls interleave, one cannot assume additional ordering guarantees for these interleaving calls. To illustrate this, let's consider the above example code again, and assume the method `example` is called twice in parallel, the resulting calls being Call 1 and Call 2. The following illustration shows two possible message execution orderings. On the left, the first call's message executions are scheduled first, and only then the second call's messages are executed. On the right, you can see another possible message execution scheduling, where the first messages of each call are executed first. Your code should result in a correct state regardless of the message execution ordering. - - ![example_orderings](./_attachments/example_orderings.png) - -- **Property 5**: On a trap or panic, modifications to the canister state for the current message execution are not applied. - - For example, if a trap occurs in the execution of the second message (blue box) of the above example, canister state changes resulting from that message execution, i.e. everything in the blue box, are discarded. However, note that any state changes from earlier message executions and in particular the first message execution (orange box) have been applied, as that message executed successfully. - -- **Property 6**: Inter-canister calls are not guaranteed to be delivered to the destination canister, but they are guaranteed to be delivered at most once. When a call does reach the destination canister, the destination canister may trap or return a reject response while processing the call. - - There are many reasons why a call might not be delivered to the destination canister. Some of them are under the control of the canister developers, such as the destination having sufficient cycles to process the call. Others are not; the Internet Computer may decide to fail the call under high load. - -- **Property 7**: Every inter-canister call is guaranteed to receive a single response, either from the callee, or synthetically produced by the protocol. - - However, a malicious destination canister could choose to delay the response for arbitrarily long if it is willing to put in the required cycles. Also, the response does not have to be successful, but can also be a reject response. The reject may come from the called canister, but it may also be generated by ICP. Such protocol-generated rejects can occur at any time before the call reaches the callee-canister, as well as once the call does reach the callee-canister, for example if the callee-canister traps while processing the call. Thus, it's important that the calling canister handles reject responses as well. A reject response means that the message hasn't been successfully processed by the receiver but doesn't guarantee that the receiver's state wasn't changed. - -- **Property 8**: If the calling canister made a guaranteed response (as opposed to a best-effort response) inter-canister call, if the call is delivered to the callee and the callee responds without trapping, the protocol guarantees that the first such response will get back to the caller canister. Otherwise, the caller will receive a reject response (with the code never being `SYS_UNKNOWN`). - -- **Property 9**: If the calling canister makes a best-effort response call, the system may generate a reject response and deliver it to the caller. This can happen even if the call is successfully delivered to the callee and the callee responds without trapping. But this can only happen if the reject response is `SYS_UNKNOWN`. +Property 3 provides a guarantee on the execution order of messages on a target canister. However, if multiple calls interleave, one cannot assume additional ordering guarantees for these interleaving calls. To illustrate this, let's consider the above example code again, and assume the method `example` is called twice in parallel, the resulting calls being Call 1 and Call 2. The following illustration shows two possible message orderings. On the left, the first call's messages are scheduled first, and only then the second call's messages are executed. On the right, you can see another possible message scheduling, where the first messages of each call are executed first. Your code should result in a correct state regardless of the message ordering. - Since, by property 7, the caller will only ever receive a single response, this means that the real response from the callee, if produced, will get dropped by the system if `SYS_UNKNOWN` is returned. +![example_orderings](./_attachments/example_orderings.png) -- **Property 10**: If cycles are attached to a guaranteed response call, the sum of cycles accepted by the callee, and those refunded to the caller equals the amount that the caller attached. +- **Property 5**: On a trap or panic, modifications to the canister state for the current message are not applied. -- **Property 11**: If cycles are attached to a best-effort response call, they may get lost whenever a `SYS_UNKNOWN` response is received by the caller. In particular, any cycles refunded by the callee are lost, and if the callee doesn't receive the call, all attached cycles are lost. If any response other than `SYS_UNKNOWN` is received, property 10 holds even for best-effort responses. +For example, if a trap in the second message (blue box) of the above example occurs, canister state changes resulting from that message, even earlier in the blue box, are discarded. However, note that any state changes from earlier messages and in particular the first message (orange box) have been applied, as that message executed successfully. - Note that cycles do not necessarily get lost. Even if the caller receives `SYS_UNKNOWN`, it can happen that the callee still receives the sent cycles. +- **Property 6**: Inter-canister calls are not guaranteed to make it to the destination canister, and if a call does reach the destination canister, the destination canister can trap or return a reject response while processing the call. +Every inter-canister call is guaranteed to receive a response, either from the canister, or synthetically produced by the protocol. However, a malicious destination canister could choose to delay the response for arbitrarily long if it is willing to put in the required cycles. Also, the response does not have to be successful, but can also be a reject response. The reject may come from the called canister, but it may also be generated by ICP. Such protocol-generated rejects can occur at any time before the call reaches the callee-canister, as well as once the call does reach the callee-canister if the callee-canister traps while processing the call. If the call reaches the callee-canister, the callee-canister can produce a reply or reject response and the protocol guarantees that the callee-canister's generated reply or reject response gets back to the caller-canister. Thus, it's important that the calling canister handles reject responses as well. A reject response means that the message hasn't been successfully processed by the receiver but doesn't guarantee that the receiver's state wasn't changed. -For more details, refer to the Interface Specification [section on ordering guarantees](/docs/current/references/ic-interface-spec#ordering_guarantees) and the section on [abstract behavior](/docs/current/references/ic-interface-spec#abstract-behavior) which defines message execution in more detail. +For more details, refer to the Interface Specification [section on ordering guarantees](/docs/current/references/ic-interface-spec#ordering_guarantees) and the section on [abstract behavior](/docs/current/references/ic-interface-spec#abstract-behavior) which defines message execution in more detail. \ No newline at end of file