🥷 Try it live — no local setup. ICP Ninja is a web-based IDE that builds and deploys this project to the mainnet for free, right in your browser. Click the badge above, or hit Deploy if you're already in Ninja. To build and run it locally instead, follow the steps below.
This example canister shows some advanced behavior between guards and asynchronous code. This example is meant for Rust canister developers that are already familiar with inter-canister calls and the security best practices related to it.
This example canister stores a bunch of items on the heap, where each item is simply modelled as a String. One
invariant that the canister aims at maintaining is that each item is processed at most once, where processing an
item involves some asynchronous code. A concrete example of such a setting would be a minter canister processing minting
requests by contacting a ledger canister, where crucially double minting should be avoided.
One tricky part in this scenario is that an item can therefore only be marked as processed after the asynchronous code has completed, meaning in the callback. As mentioned in the security best practices, it's not always feasible to guarantee that the callback will not trap, which in that case would break the invariant due to the state being rolled back.
The standard solution to maintain such an invariant, despite a potential panic in the callback is to use a guard.
However, this example canister shows that it's also crucially important that the declaration of the guard happens in
another message than the callback, which is the case for true asynchronous code (e.g. inter-canister calls, raw_rand,
etc.). It's in particular not enough to await a function that's declared to be async, since if the future can be
polled until completion directly, everything will be executed in a single message.
- Node.js
- icp-cli:
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm - Rust toolchain with
wasm32-unknown-unknowntarget (only for PocketIC tests): rustup.rs
git clone https://github.com/dfinity/examples
cd examples/rust/guardsicp network start -d
icp deploy
bash test.sh
icp network stopbash test.sh exercises the guard behavior via icp canister call — verifying that TrueAsyncCall marks items as processed and FalseAsyncCall does not, and asserting the exact trap message in each case.
The backend/tests/ directory contains Rust integration tests using PocketIC. These cover the same guard/async scenarios as test.sh and additionally test parallel processing prevention — submitting two concurrent calls for the same item and asserting that one is rejected with "Item already in processing!". This concurrent scenario cannot be reliably expressed in a bash script.
First, install the PocketIC server:
# macOS
curl -sL https://github.com/dfinity/pocketic/releases/download/14.0.0/pocket-ic-x86_64-darwin.gz | gunzip > pocket-ic-server
# Linux
curl -sL https://github.com/dfinity/pocketic/releases/download/14.0.0/pocket-ic-x86_64-linux.gz | gunzip > pocket-ic-server
chmod +x pocket-ic-server
export POCKET_IC_BIN=$(pwd)/pocket-ic-serverThen build the canister WASM and run the tests:
cargo build --package backend --target wasm32-unknown-unknown --release
cargo test --package backendBelow manually tests the behavior demonstrated by the TrueAsyncCall variant.
Set the item "mint" to be processed:
icp canister call backend set_non_processed_items '(vec { "mint" })'As a sanity check, ensure that the item is not yet processed:
icp canister call --query backend is_item_processed '("mint")'This should return (opt false).
Process the item by calling the panicking callback with a true async call (the guard fires in call_on_cleanup):
icp canister call backend process_single_item_with_panicking_callback '("mint", variant { TrueAsyncCall })'Ensure that the guard was executed and the item is marked as processed despite the previous panic:
icp canister call --query backend is_item_processed '("mint")'This should return (opt true).
Now repeat the test with FalseAsyncCall — here the future is polled to completion in a single message, so the
panic reverts all state changes and the guard has no effect:
icp canister call backend set_non_processed_items '(vec { "mint" })'
icp canister call backend process_single_item_with_panicking_callback '("mint", variant { FalseAsyncCall })'
icp canister call --query backend is_item_processed '("mint")'This should return (opt false) — the item was not marked as processed.
If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the security best practices for developing on ICP. This example may not implement all the best practices.