()
+ .await
+ {
+ Ok(signature) => Ok(hex::encode(signature.signature)),
+ Err(e) => match e {
+ // A SysUnknown error means that we won't get any cycles refunded, even
+ // if the call didn't make it to the callee. But we don't care here since
+ // we only attached a small amount of cycles.
+ CallError::OutComeUnknown(OutcomeUnknown::SysUnknown(err)) => Err(format!(
+ "Got a SysUnknown error while signing message: {:?}; cycles are not refunded",
+ err
+ )),
+ _ => Err(format!("Error signing message: {:?}", e)),
+ },
+ }
+}
```
+
+Cycles can be attached to both bounded and unbounded wait messages. For unbounded wait messages, cycles that are not consumed by the callee are guaranteed to be refunded to the caller. For bounded wait calls, refunds do not happen when the call returns a `SysUnknown` error. However, this is usually acceptable for API calls that charge for cycles, since the amount charged is usually low (10 billion cycles for signatures with the test key). For transferring larger amounts of cycles, switch to using unbounded wait calls. See the section on [inter-canister calls](/docs/current/developer-docs/smart-contracts/advanced-features/async-code) for more details.
+
+:::info
+Summary: you can transfer cycles to the callee by attaching them to a (bounded- or unbounded-wait) call. Bounded-wait calls may drop the attached cycles, so avoid using them for large cycle amounts.
+:::
+
+## Further reading
+
+To understand more details about how inter-canister calls execute and how the different call types work, an explanation is provided in the documentation on [inter-canister calls and async code](/docs/current/developer-docs/smart-contracts/advanced-features/async-code). Also consult the documentation on [properties of call execution]( /docs/current/references/message-execution-properties) and [security best practices](docs/current/developer-docs/security/security-best-practices/inter-canister-calls). To allow your callers to handle call errors robustly, follow the [best practice](/docs/current/developer-docs/smart-contracts/best-practices/idempotency) document on retries and idempotency.
+
+As noted in our examples, an `update` method can always call any method of any other canister. In cases where you only need to call query methods on other canisters, and if you are sure that these canisters are on the same subnet as your canister, you can also use [composite query calls](/docs/current/developer-docs/smart-contracts/call/overview) methods instead of update methods.
+
+For a real-life example of how to handle errors when calling canisters, see the [ICRC-1 examples]( /docs/current/developer-docs/defi/tokens/ledger/usage/icrc1_ledger_usage#interacting-with-an-icrc-1-ledger-from-another-canister-inter-canister-calls-via-ic-cdk)
+
diff --git a/docs/developer-docs/defi/tokens/ledger/usage/icrc1_ledger_usage.mdx b/docs/developer-docs/defi/tokens/ledger/usage/icrc1_ledger_usage.mdx
index 3413fb44da..79953313ed 100644
--- a/docs/developer-docs/defi/tokens/ledger/usage/icrc1_ledger_usage.mdx
+++ b/docs/developer-docs/defi/tokens/ledger/usage/icrc1_ledger_usage.mdx
@@ -57,21 +57,109 @@ View the [`dfx canister call` documentation](/docs/current/developer-docs/develo
## Interacting with an ICRC-1 ledger from another canister (inter-canister calls via `ic-cdk`)
-View the [inter-canister call documentation] (/docs/developer-docs/backend/rust/intercanister) to see how you can call one canister from within another.
-
-Here is an example of how to fetch the token name from the ICP ledger using Rust and the `ic-cdk` [library](https://github.com/dfinity/cdk-rs) from within a canister:
-
+When calling into arbitrary ICRC-1 ledgers, we recommend you use [bounded wait (aka best-effort response) calls](/docs/current/developer-docs/smart-contracts/advanced-features/async-code). These calls ensure that your canister does not get stuck waiting for a response from the ledger.
+
+Here sample code to fetch the transfer fee from an ICRC-1 ledger using Rust and the `ic-cdk` [library](https://github.com/dfinity/cdk-rs) from within a canister. The example includes retry logic to handle errors when possible.
+
+```rust
+/// THIS DOESN'T COMPILE YET
+/// This is just sample code using not-yet-implemented Rust CDK API
+
+pub enum GetFeeError {
+ /// The ledger didn't implement the ICRC-1 API correctly, e.g., returning an invalid response.
+ Icrc1ApiViolation(String),
+ /// A CDK CallError that we cannot recover from synchronously.
+ FatalCallError(CallError),
+}
+
+/// Obtain the fee that the ledger canister charges for a transfer.
+/// This functiuon will keep retrying to fetch the fees for as long possible, and for as long as the
+/// `should_retry` predicate returns true. Note that using a predicate that just always returns
+/// `true` can keep your canister in a retry loop, and potentially unable to upgrade. The
+/// recommended way is to set a limit on the number of retries, use a timeout, or abort when the
+/// caller canister enters the stopping state.
+#[ic_cdk::update]
+pub async fn icrc1_get_fee(ledger: Principal, should_retry: P) -> Result
+ where P: Fn() -> bool
+{
+ loop {
+ match Call::bounded_wait(ledger, "icrc1_fee")
+ .await
+ {
+ Ok(res) => match res.decode_candid() {
+ Ok(fee) => return Ok(fee),
+ Err(msg) => return GetFeeError::Icrc1ApiViolation(format!("Unable to decode the fee: {:?}", res)),
+ },
+ // The system rejected our call, but it is possible to retry immediately.
+ // Since obtaining the fees is idempotent, it's always safe to retry.
+ Err(err) if err.is_immediately_retriable() && should_retry() => continue,
+ // The system rejected our call, but it is not possible to retry immediately.
+ Err(err) => GetFeeError::FatalCallError(err),
+ }
+ }
+}
```
-// You will need the canister ID of the ICRC ledger.
-
-let ledger_id = Principal::from_text("ss2fx-dyaaa-aaaar-qacoq-cai").unwrap();
-
-// The request object of the `icrc1_name` endpoint is empty.
- let req = ();
- let (res,): (String,) =
- ic_cdk::call(ledger_id, "icrc1_name", (req,))
- .await.unwrap();
+Here's sample code for transferring ICRC-1 ledger tokens using bounded wait response calls. It handles the unknown state case by using the [transaction deduplication feature](https://internetcomputer.org/docs/current/references/icrc1-standard#transaction-deduplication-) of ICRC-1 ledgers.
+
+```rust
+/// THIS DOESN'T COMPILE YET
+/// This is just sample code using not-yet-implemented Rust CDK API
+
+/// Transfer the tokens on the specified ledger. The caller must ensure that:
+/// 1. The `created_at` time of the `TransferArg` is set.
+/// 2. The transaction described by the `TransferArg` has not yet been executed by the ledger.
+/// Otherwise, the function may return `Ok` even if the transfer didn't happen.
+#[ic_cdk::update]
+pub async fn icrc1_transfer(ledger: Principal, arg: TransferArg, should_retry: P) -> Result
+ where P: Fn() -> bool
+{
+ assert!(arg.created_at_time.is_some(), "The created_at_time must be set in the TransferArg");
+ // In the first step, obtain the fee. Use the method above to handle retries.
+ // A more efficient way would be to extract the core of the fee fetching logic into a separate
+ // function that doesn't do Candid en/decoding, and call it from both `icrc1_transfer` and
+ // `icrc1_get_fee`.
+ let fee: NumTokens = Call::bounded_wait(canister_self(), "icrc1_get_fee")
+ .await
+ .map_err(|e| Icrc1TransferError::TransferFailed(TransferErrorCause::FatalCallError(e)))
+ .and_then(|res| match decode_candid().unwrap() {
+ Ok(fee) => Ok(fee),
+ Err(e) => Icrc1TransferError::TransferFailed(match e {
+ GetFeeError::FatalCallError(e) => TransferErrorCause::FatalCallError(e),
+ GetFeeError::Icrc1ApiViolation(msg) => TransferErrorCause::Icrc1ApiViolation(msg),
+ })
+ })?;
+
+ let no_unknowns = true;
+ loop {
+ match Call::new(ledger, "icrc1_transfer")
+ .with_arg(&arg)
+ .await {
+ Ok(res) => match res.decode_candid() {
+ Ok(Ok(_)) => return Ok(BlockIndex),
+ Ok(Err(e)) => match e {
+ // Since the assumption is that the transaction didn't happen before our call, we
+ // treat a duplicate error as a success.
+ TransferError::Duplicate { duplicate_of } => return Ok(duplicate_of),
+ e if no_unknowns => return Err(Icrc1TransferError::TransferFailed(TransferErrorCause::LedgerError(e))),
+ e => return Err(Icrc1TransferError::UnknownState(TransferErrorCause::LedgerError(e))),
+ },
+ Err(msg) => return Err(Icrc1TransferError::TransferFailed(TransferErrorCause::Icrc1ApiViolation(msg))),
+ }
+ // Since the call is idempotent, we can safely retry if the system returns an error with
+ // the ledger canister state being unknown.
+ Err(e) if e.immediately_retryable() && should_retry() => {
+ // If the reject wasn't clean, we don't know what the state is
+ if !e.is_clean_reject() {
+ no_unknowns = false;
+ }
+ continue;
+ }
+ Err(e) if e.is_clean_reject() && no_unknowns => Err(Icrc1TransferError::TransferFailed(TransferErrorCause::FatalCallError(e))),
+ Err(e) => Err(Icrc1TransferError::UnknownState(TransferErrorCause::FatalCallError(e))),
+ }
+ }
+}
```
You can find all available methods for your ICRC-1 ledger within the ICRC-1 ledger canister's Candid file or, if your ICRC-1 ledger has been deployed to the mainnet, view your ICRC-1 ledger canister [on the dashboard](https://dashboard.internetcomputer.org/canisters). An example of an ICRC-1 ledger deployed on the mainnet that you can reference is the [ckETH ledger canister](https://dashboard.internetcomputer.org/canister/ss2fx-dyaaa-aaaar-qacoq-cai).
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 508617c0fd..5d4da5dd21 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,13 +309,17 @@ 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 because some data structures like message queues are 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 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).
-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".
+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.
### Recommendation
-When making inter-canister calls, always handle the error cases (rejects) correctly. These errors imply that the message has not been successfully executed.
+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.
## Be aware of the risks involved in calling untrustworthy canisters
@@ -323,15 +327,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 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.
+- 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`.
- 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 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 or its subnet that makes it stall forever.
-- 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.
+- 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.
- Sanitize data returned from inter-canister calls.
@@ -348,7 +352,7 @@ Loops in the call graph (e.g. canister A calling B, B calling C, C calling A) ma
### Recommendation
-- Avoid such loops.
+- Avoid such loops, or rely on best-effort response calls instead, since these provide timeouts.
- 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 9a9089d24e..6a726e73cf 100644
--- a/docs/developer-docs/smart-contracts/advanced-features/async-code.mdx
+++ b/docs/developer-docs/smart-contracts/advanced-features/async-code.mdx
@@ -1,90 +1,98 @@
---
-keywords: [advanced, concept, async code, inter-canister calls, async inter-canister, async]
+keywords: [beginner, concept, async code, inter-canister calls, async inter-canister, async]
---
import { MarkdownChipRow } from "/src/components/Chip/MarkdownChipRow";
-# Async code and inter-canister calls
+# Inter-canister calls & async code
-
+
-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.
+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:
-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.
+- 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.
-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.
+- 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.
- ## Language runtime for asynchronous code
+- 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 **bounded-wait** or **unbounded-wait** modes. The former is better suited for a wider range of trust assumptions, 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.
-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.
+- 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.
-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.
+## Service discovery
-## 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.
+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).
+```bash
+dfx canister metadata ryjl3-tyaaa-aaaaa-aaaba-cai candid:service --network ic
```
-/// Canister A
-async fn foo() {
- do_work();
- call(canister_b, 'bar').await;
- do_more_work(res);
-}
-/// Canister B
-fn bar() {
- do_some_more_work();
-}
-```
+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
-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`.
+[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.
-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`).
+:::info
+Note that cycles may get dropped when using bounded-wait calls. See the section on #[bounded- vs unbounded-wait calls](#bounded-vs-unbounded-wait-calls) for more details.
+:::
-## Asynchronous functions
+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.
-In the following example, another call is added to a local function to canister A before making the inter-canister call to canister B.
+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.
+
+## Bounded- vs unbounded-wait calls
+
+ICP supports two kind of inter-canister calls:
+
+* Unbounded wait calls instruct the system to wait for as long as it takes to get a response to a call. The response might still be a failure, either because the request didn't get delivered to the callee, or because the callee rejected the request or produced an error while processing it. The unbounded wait provides the caller with the guarantee that it will learn the exact response to the call, which is why we also sometimes refer to these calls as *guaranteed response calls*. 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.
+
+* Bounded-wait calls do not wait for every, and may return just return an "unknown" error response after some time. 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 stop waiting. This is why we also refer to these calls as *best-effort response calls*. When the system stops waiting for a response, the request may or may not have been processed by the callee; the system is free to drop the request, but it may also deliver it to the target, and simply drop the response later. 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.
+
+While unbounded-wait calls require the caller to handle one error condition (unknown call status) less, bounded-wait calls also have significant advantages:
+
+* 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), a caller that made an unbounded-wait call stalls and has no control over when it can resume processing or provide an answer. The callee that keeps processing the request forever forces the caller to also wait forever for a response. As safe upgrades require stopping the canister and waiting for all outstanding calls to return, canisters that issue unbounded-wait calls may be prevented from safely upgrading, potentially forever.
+* Bounded-wait calls scale much better. When the system is under high load, canisters are much more likely to still be able to issue bounded-wait calls than unbounded-wait calals.
+
+Here are some guidelines how to choose between the two types of calls:
+
+* Always prefer bounded-wait 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 bounded-wait calls for such endpoints, and handle the additional edge cases.
+* Use unbounded-wait calls for endpoints that mutate state and do not enable safe retries, or calls that perform larger cycle transfers. As mentioned, bounded-wait calls will currently lose cycles when request/responses are dropped. Be aware of the limitations of unbounded-wait 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).
+
+## `async/await` syntax, concurrency and state changes
+
+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):
```
-/// Canister A
async fn foo() {
- baz().await;
+ do_work();
call(canister_b, 'bar').await;
do_more_work(res);
}
+```
-async fn baz() {
- do_local_work();
-}
+While this looks like an ordinary function, there are important differences in behavior between synchronous functions and functions that perform inter-canister calls.
-/// Canister B
-fn bar() {
- do_some_more_work();
-}
-```
+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.
+
+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.
-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).
+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.
-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.
+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.
diff --git a/docs/developer-docs/smart-contracts/best-practices/idempotency.mdx b/docs/developer-docs/smart-contracts/best-practices/idempotency.mdx
index b20f6db610..a24874750a 100644
--- a/docs/developer-docs/smart-contracts/best-practices/idempotency.mdx
+++ b/docs/developer-docs/smart-contracts/best-practices/idempotency.mdx
@@ -15,8 +15,9 @@ 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 bounded-wait calls may be unable to determine whether the call was successful or not.
-This can be risky as the application might decide to retry transactions, potentially leading to serious security vulnerabilities such as double spending.
+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.
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.
@@ -26,10 +27,12 @@ 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 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 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.
![Retrying an idempotent call](/img/docs/retry_idempotency.png)
+The situation is similar for bounded-wat inter-canister calls. 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
@@ -41,13 +44,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 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. 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. 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 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 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.
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.
@@ -60,7 +63,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 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.
+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.
Summarizing, the advantages of this approach are:
1. It can support high throughput.
@@ -80,21 +83,23 @@ 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), 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, 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.
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, 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.
+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.
### 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, 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, 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 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.
diff --git a/docs/developer-docs/smart-contracts/call/overview.mdx b/docs/developer-docs/smart-contracts/call/overview.mdx
index 20bfde5816..33e55f6e12 100644
--- a/docs/developer-docs/smart-contracts/call/overview.mdx
+++ b/docs/developer-docs/smart-contracts/call/overview.mdx
@@ -136,7 +136,7 @@ Calls to canisters can be initiated by end users or other canisters. A call is p
Typically, a call is processed within a single message execution unless there are calls to other canisters involved, in which case the call will be split across several message executions.
-[Learn more about the properties of ICP message executions.](/docs/current/references/execution-errors)
+[Learn more about the properties of ICP message executions.](/docs/current/references/message-execution-properties)
## Other types of calls
diff --git a/docs/references/message-execution-properties.mdx b/docs/references/message-execution-properties.mdx
index 359eb28568..7636e2cd79 100644
--- a/docs/references/message-execution-properties.mdx
+++ b/docs/references/message-execution-properties.mdx
@@ -6,35 +6,36 @@ 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 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).
+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).
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 **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.
+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`.
-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.
+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.
-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:
+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:
-- **Property 1**: Only a single message is processed at a time per canister. Message execution is atomic and sequential, and never parallel.
+- **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 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.
+Note that parallel message execution over multiple canisters is possible — this property talks about just a single canister.
-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.
+- **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 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 (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).
+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).
:::
-
+
:::info
-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`.
-
+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`.
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.
:::
@@ -44,19 +45,36 @@ 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 messages, e.g., from different calls, can interleave and have no reliable execution
+- **Property 4**: Multiple message executions, e.g., from different calls, can interleave and have no reliable execution
ordering.
-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.
+ 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 an unbounded-wait (as opposed to a bounded-wait) 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 bounded-wait 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`.
-![example_orderings](./_attachments/example_orderings.png)
+ 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.
-- **Property 5**: On a trap or panic, modifications to the canister state for the current message are not applied.
+- **Property 10**: If cycles are attached to an unbounded-wait call, the sum of cycles accepted by the callee, and those refunded to the caller equals the amount that the caller attached.
-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.
+- **Property 11**: If cycles are attached to a bounded-wait 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 bounded-wait calls.
-- **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.
+ 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.
-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.
\ No newline at end of file
+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.
diff --git a/static/.ic-assets.json b/static/.ic-assets.json
index 17eac8d788..f66bee0ac2 100644
--- a/static/.ic-assets.json
+++ b/static/.ic-assets.json
@@ -7,7 +7,7 @@
"match": "**/*",
"allow_raw_access": true,
"headers": {
- "Content-Security-Policy": "default-src 'self';script-src 'self' 'unsafe-eval' 'unsafe-inline' https://internetcomputer.matomo.cloud https://cdn.matomo.cloud https://widget.kapa.ai https://www.google.com https://www.gstatic.com;connect-src 'self' https://*.ic0.app https://ic0.app https://icp0.io https://*.icp0.io https://internetcomputer.matomo.cloud https://cdn.matomo.cloud ic-api.internetcomputer.org icrc-api.internetcomputer.org mxzaz-hqaaa-aaaar-qaada-cai.raw.ic0.app https://data.jsdelivr.com https://cdn.jsdelivr.net https://kapa-widget-proxy-la7dkmplpq-uc.a.run.app;img-src 'self' data: https:;style-src * 'unsafe-inline';style-src-elem * 'unsafe-inline';font-src * data:;object-src 'none';base-uri 'self';frame-src https://motoko.agorapp.dev https://3d5wy-5aaaa-aaaag-qkhsq-cai.icp0.io https://www.google.com https://internetcomputer.matomo.cloud https://www.youtube.com;frame-ancestors https://internetcomputer.matomo.cloud;form-action 'self' https://dfinity.us16.list-manage.com https://internetcomputer.org;upgrade-insecure-requests;",
+ "Content-Security-Policy": "default-src 'self';script-src 'self' 'unsafe-eval' 'unsafe-inline' https://internetcomputer.matomo.cloud https://cdn.matomo.cloud https://widget.kapa.ai https://www.google.com https://www.gstatic.com;connect-src 'self' https://*.ic0.app https://ic0.app https://icp0.io https://*.icp0.io https://internetcomputer.matomo.cloud https://cdn.matomo.cloud ic-api.internetcomputer.org icrc-api.internetcomputer.org github.com raw.githubusercontent.com mxzaz-hqaaa-aaaar-qaada-cai.raw.ic0.app https://data.jsdelivr.com https://cdn.jsdelivr.net https://kapa-widget-proxy-la7dkmplpq-uc.a.run.app;img-src 'self' data: https:;style-src * 'unsafe-inline';style-src-elem * 'unsafe-inline';font-src * data:;object-src 'none';base-uri 'self';frame-src https://motoko.agorapp.dev https://3d5wy-5aaaa-aaaag-qkhsq-cai.icp0.io https://www.google.com https://internetcomputer.matomo.cloud https://www.youtube.com;frame-ancestors https://internetcomputer.matomo.cloud;form-action 'self' https://dfinity.us16.list-manage.com https://internetcomputer.org;upgrade-insecure-requests;",
"X-Frame-Options": "DENY",
"Referrer-Policy": "same-origin",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",