From 12755c53c9543fc0f02c0e21fe02694c7a759ef6 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 12:12:15 +0200 Subject: [PATCH 1/3] docs: onchain randomness guide Covers raw_rand API, Motoko and Rust implementations, range generation, winner selection, getrandom/rand PRNG seeding, security considerations, and the random_maze example. --- docs/guides/backends/randomness.md | 238 +++++++++++++++++++++++++++-- 1 file changed, 226 insertions(+), 12 deletions(-) diff --git a/docs/guides/backends/randomness.md b/docs/guides/backends/randomness.md index 682378e7..6067aafa 100644 --- a/docs/guides/backends/randomness.md +++ b/docs/guides/backends/randomness.md @@ -1,21 +1,235 @@ --- title: "Onchain Randomness" -description: "Generate unpredictable random numbers using the IC management canister" +description: "Generate cryptographically secure random numbers in canisters using the management canister's raw_rand API" sidebar: order: 4 --- -TODO: Write content for this page. +Canisters can generate cryptographically secure random numbers directly from canister code. This guide shows how to call the `raw_rand` method, derive typed values from the returned bytes, and use randomness safely. - -Generate cryptographically secure random numbers in canisters using the management canister's raw_rand API. Explain why randomness is hard on blockchains and how ICP solves it with VRF. Use cases: games, lotteries, fair selection. Security considerations: never use randomness in query calls, always use in update calls. +For how ICP produces unpredictable randomness without any trusted party, see [Onchain Randomness](../../concepts/onchain-randomness.md). - -- Portal: building-apps/integrations/randomness.mdx -- Examples: random_maze (Motoko) -- Management canister: raw_rand method +## Why blockchain randomness is different - -- concepts/onchain-randomness -- VRF-based randomness explained -- guides/security/data-integrity -- randomness and security -- reference/management-canister -- raw_rand API +Most blockchains execute transactions deterministically — every node replays the same operations and must reach the same state. This means you cannot use typical randomness sources like `Math.random()` or `/dev/urandom`: they would produce different values on each replica, breaking consensus. + +ICP solves this with a threshold Verifiable Random Function (VRF). The result of `raw_rand` is produced collaboratively by the subnet's nodes using a random beacon that no single node can predict or bias. Every node independently verifies the output is correct, and the same 32 bytes are delivered to all replicas — satisfying both unpredictability and consensus. + +## The `raw_rand` API + +The management canister (`aaaaa-aa`) exposes `raw_rand`, which returns 32 bytes of cryptographic randomness: + +- **Caller:** Canisters only (not callable via ingress messages / external clients) +- **Parameters:** None +- **Returns:** `blob` — 32 bytes + +Because `raw_rand` is an update call to the management canister, it can only be invoked from an update context in your canister. **Randomness is not available in query calls** — a query executes on a single replica and cannot access the subnet-level random beacon. Attempting to call `raw_rand` from a query will trap. + +See the [Management Canister reference](../../reference/management-canister.md#raw_rand) for the full API specification. + +## Getting random bytes + +**Motoko** + +Motoko's `mo:core/Random` module wraps `raw_rand` and provides a `Finite` class for consuming entropy from a blob: + +```motoko +import Random "mo:core/Random"; + +public shared func getRandomBytes() : async Blob { + let entropy : Blob = await Random.blob(); + entropy +}; +``` + +`Random.blob()` calls `raw_rand` internally and returns the 32-byte blob. Each call to `getRandomBytes` makes one call to the management canister. + +**Rust** + +The `ic_cdk` crate provides `ic_cdk::management_canister::raw_rand()` which wraps the `raw_rand` management canister call: + +```rust +#[ic_cdk::update] +async fn get_random_bytes() -> Vec { + let (random_bytes,) = ic_cdk::management_canister::raw_rand() + .await + .expect("raw_rand failed"); + random_bytes +} +``` + +`raw_rand` is an async call — it must be awaited from an `async` function marked `#[ic_cdk::update]`. + +## Generating a random number in a range + +To generate a random integer in the range `[0, n)`, extract bytes from the result and reduce modulo `n`. + +**Motoko** + +```motoko +import Random "mo:core/Random"; +import Nat8 "mo:core/Nat8"; + +public shared func rollDie(sides : Nat) : async Nat { + let entropy : Blob = await Random.blob(); + let f = Random.Finite(entropy); + switch (f.byte()) { + case (?b) { Nat8.toNat(b) % sides }; + case null { 0 }; // entropy exhausted (won't happen with a fresh 32-byte blob) + } +}; +``` + +For multiple random values in a single call, use the `Finite` class to consume bytes incrementally from the same 32-byte blob rather than making multiple `raw_rand` calls: + +```motoko +import Buffer "mo:core/Buffer"; +import Random "mo:core/Random"; +import Nat8 "mo:core/Nat8"; + +public shared func rollMultipleDice(count : Nat, sides : Nat) : async [Nat] { + let entropy : Blob = await Random.blob(); + let f = Random.Finite(entropy); + let buf = Buffer.Buffer(count); + label loop_ loop { + if (buf.size() >= count) break loop_; + switch (f.byte()) { + case (?b) { buf.add(Nat8.toNat(b) % sides) }; + case null { break loop_ }; + } + }; + Buffer.toArray(buf) +}; +``` + +**Rust** + +```rust +#[ic_cdk::update] +async fn roll_die(sides: u64) -> u64 { + let (random_bytes,) = ic_cdk::management_canister::raw_rand() + .await + .expect("raw_rand failed"); + // take the first 8 bytes as a u64 + let n = u64::from_le_bytes(random_bytes[..8].try_into().unwrap()); + n % sides +} +``` + +For multiple random values from a single `raw_rand` call, slice the 32-byte result into windows: + +```rust +#[ic_cdk::update] +async fn roll_multiple_dice(count: usize, sides: u64) -> Vec { + let (random_bytes,) = ic_cdk::management_canister::raw_rand() + .await + .expect("raw_rand failed"); + // yields up to 4 independent u64 values from 32 bytes + random_bytes + .chunks_exact(8) + .take(count) + .map(|chunk| { + let n = u64::from_le_bytes(chunk.try_into().unwrap()); + n % sides + }) + .collect() +} +``` + +## Choosing winners from a list + +A common use case is selecting one or more random elements from a list — for example, choosing a lottery winner or assigning roles in a game. + +**Motoko** + +```motoko +import Random "mo:core/Random"; +import Nat8 "mo:core/Nat8"; + +public shared func pickWinner(participants : [Text]) : async ?Text { + if (participants.size() == 0) { return null }; + let entropy : Blob = await Random.blob(); + let f = Random.Finite(entropy); + switch (f.byte()) { + case (?b) { + let idx = Nat8.toNat(b) % participants.size(); + ?participants[idx] + }; + case null { null }; + } +}; +``` + +**Rust** + +```rust +#[ic_cdk::update] +async fn pick_winner(participants: Vec) -> Option { + if participants.is_empty() { + return None; + } + let (random_bytes,) = ic_cdk::management_canister::raw_rand() + .await + .expect("raw_rand failed"); + let idx = (random_bytes[0] as usize) % participants.len(); + Some(participants[idx].clone()) +} +``` + +## Using `getrandom` in Rust + +Some Rust crates (for example, `rand`) depend on `getrandom` as a randomness source. Because `getrandom` uses OS-level entropy that does not exist in the Wasm environment, you must register a custom handler. + +The recommended approach is to seed a `rand` PRNG from a single `raw_rand` call, then use the PRNG for subsequent draws: + +```rust +use rand::SeedableRng; +use rand::rngs::StdRng; +use rand::Rng; + +#[ic_cdk::update] +async fn generate_with_prng(count: usize) -> Vec { + let (seed_bytes,) = ic_cdk::management_canister::raw_rand() + .await + .expect("raw_rand failed"); + let seed: [u8; 32] = seed_bytes.try_into().unwrap(); + let mut rng = StdRng::from_seed(seed); + // use rng for multiple draws without additional management canister calls + (0..count).map(|_| rng.gen::()).collect() +} +``` + +Add `rand` to your `Cargo.toml`: + +```toml +[dependencies] +rand = { version = "0.8", default-features = false, features = ["std_rng"] } +``` + +The `std_rng` feature compiles `StdRng` without requiring OS entropy, which is compatible with the Wasm target. + +## Security considerations + +**Always use randomness in update calls, never in queries.** Query calls execute on a single replica and cannot access the random beacon. The `raw_rand` API will trap if called from a query context. + +**One call per decision round.** Each call to `raw_rand` costs cycles and involves an inter-canister call to the management canister. Batch your entropy needs: a single 32-byte blob provides 256 bits of entropy — enough for 4 independent `u64` values, 32 independent byte selections, or one `StdRng` seed for unlimited draws. + +**Understand the timing guarantee.** The value returned by `raw_rand` is determined during the round in which the management canister processes the call, not when your canister submits it. Subnet nodes collaborate to produce the value under the consensus protocol — no individual node can predict or bias the output. This is appropriate for games, lotteries, and fair selection. For use cases requiring verifiable fairness to external observers who do not trust the subnet operator, combine `raw_rand` with a commit-reveal scheme. + +**Reentrancy caution.** Because `raw_rand` is an async call, your canister's execution can be interleaved with other messages at the `await` point. If you check state before the `await` and rely on that state after, another message may have modified it in between. See [Canister security](../security/inter-canister-calls.md) for reentrancy patterns. + +## Example: random maze + +The `random_maze` example in the ICP examples repository generates a maze using randomness to decide which walls to remove during a depth-first search. It demonstrates how to seed a Motoko `Finite` generator from a single `raw_rand` call and consume entropy incrementally across many cells: + +- [random_maze (Motoko)](https://github.com/dfinity/examples/tree/master/motoko/random_maze) + +## Next steps + +- [Onchain Randomness (concept)](../../concepts/onchain-randomness.md) — how the IC's threshold VRF works +- [Management Canister](../../reference/management-canister.md) — `raw_rand` API reference +- [Data Integrity](../security/data-integrity.md) — using randomness in a secure application design +- [Inter-canister calls](../canister-calls/onchain-calls.md) — async patterns and reentrancy + + + From 635058e1482f4a48469a6165f902df1cc2407484 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 13:53:11 +0200 Subject: [PATCH 2/3] docs(randomness): address PR #62 feedback - fix Rust raw_rand tuple destructuring --- docs/guides/backends/randomness.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/guides/backends/randomness.md b/docs/guides/backends/randomness.md index 6067aafa..e9cca6da 100644 --- a/docs/guides/backends/randomness.md +++ b/docs/guides/backends/randomness.md @@ -51,7 +51,7 @@ The `ic_cdk` crate provides `ic_cdk::management_canister::raw_rand()` which wrap ```rust #[ic_cdk::update] async fn get_random_bytes() -> Vec { - let (random_bytes,) = ic_cdk::management_canister::raw_rand() + let random_bytes = ic_cdk::management_canister::raw_rand() .await .expect("raw_rand failed"); random_bytes @@ -83,22 +83,22 @@ public shared func rollDie(sides : Nat) : async Nat { For multiple random values in a single call, use the `Finite` class to consume bytes incrementally from the same 32-byte blob rather than making multiple `raw_rand` calls: ```motoko -import Buffer "mo:core/Buffer"; +import List "mo:core/List"; import Random "mo:core/Random"; import Nat8 "mo:core/Nat8"; public shared func rollMultipleDice(count : Nat, sides : Nat) : async [Nat] { let entropy : Blob = await Random.blob(); let f = Random.Finite(entropy); - let buf = Buffer.Buffer(count); + let results = List.empty(); label loop_ loop { - if (buf.size() >= count) break loop_; + if (List.size(results) >= count) break loop_; switch (f.byte()) { - case (?b) { buf.add(Nat8.toNat(b) % sides) }; + case (?b) { List.add(results, Nat8.toNat(b) % sides) }; case null { break loop_ }; } }; - Buffer.toArray(buf) + List.toArray(results) }; ``` @@ -107,7 +107,7 @@ public shared func rollMultipleDice(count : Nat, sides : Nat) : async [Nat] { ```rust #[ic_cdk::update] async fn roll_die(sides: u64) -> u64 { - let (random_bytes,) = ic_cdk::management_canister::raw_rand() + let random_bytes = ic_cdk::management_canister::raw_rand() .await .expect("raw_rand failed"); // take the first 8 bytes as a u64 @@ -121,7 +121,7 @@ For multiple random values from a single `raw_rand` call, slice the 32-byte resu ```rust #[ic_cdk::update] async fn roll_multiple_dice(count: usize, sides: u64) -> Vec { - let (random_bytes,) = ic_cdk::management_canister::raw_rand() + let random_bytes = ic_cdk::management_canister::raw_rand() .await .expect("raw_rand failed"); // yields up to 4 independent u64 values from 32 bytes @@ -168,7 +168,7 @@ async fn pick_winner(participants: Vec) -> Option { if participants.is_empty() { return None; } - let (random_bytes,) = ic_cdk::management_canister::raw_rand() + let random_bytes = ic_cdk::management_canister::raw_rand() .await .expect("raw_rand failed"); let idx = (random_bytes[0] as usize) % participants.len(); @@ -176,7 +176,7 @@ async fn pick_winner(participants: Vec) -> Option { } ``` -## Using `getrandom` in Rust +## Seeding a PRNG from `raw_rand` (Rust) Some Rust crates (for example, `rand`) depend on `getrandom` as a randomness source. Because `getrandom` uses OS-level entropy that does not exist in the Wasm environment, you must register a custom handler. @@ -189,7 +189,7 @@ use rand::Rng; #[ic_cdk::update] async fn generate_with_prng(count: usize) -> Vec { - let (seed_bytes,) = ic_cdk::management_canister::raw_rand() + let seed_bytes = ic_cdk::management_canister::raw_rand() .await .expect("raw_rand failed"); let seed: [u8; 32] = seed_bytes.try_into().unwrap(); @@ -232,4 +232,3 @@ The `random_maze` example in the ICP examples repository generates a maze using - [Inter-canister calls](../canister-calls/onchain-calls.md) — async patterns and reentrancy - From dda8c860d0f3e00af3e9eb20660bb3c5d298fabe Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 14:49:43 +0200 Subject: [PATCH 3/3] fix(randomness): rewrite Motoko examples to use actual mo:core/Random API (no Finite class) --- docs/guides/backends/randomness.md | 51 ++++++++++++++---------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/docs/guides/backends/randomness.md b/docs/guides/backends/randomness.md index e9cca6da..93b5d197 100644 --- a/docs/guides/backends/randomness.md +++ b/docs/guides/backends/randomness.md @@ -31,7 +31,7 @@ See the [Management Canister reference](../../reference/management-canister.md#r **Motoko** -Motoko's `mo:core/Random` module wraps `raw_rand` and provides a `Finite` class for consuming entropy from a blob: +Motoko's `mo:core/Random` module wraps `raw_rand`. `Random.blob()` returns the raw 32-byte blob, which you convert to an array with `Blob.toArray` for byte-level access: ```motoko import Random "mo:core/Random"; @@ -68,35 +68,34 @@ To generate a random integer in the range `[0, n)`, extract bytes from the resul ```motoko import Random "mo:core/Random"; +import Blob "mo:core/Blob"; import Nat8 "mo:core/Nat8"; public shared func rollDie(sides : Nat) : async Nat { - let entropy : Blob = await Random.blob(); - let f = Random.Finite(entropy); - switch (f.byte()) { - case (?b) { Nat8.toNat(b) % sides }; - case null { 0 }; // entropy exhausted (won't happen with a fresh 32-byte blob) - } + let entropy = await Random.blob(); + let bytes = Blob.toArray(entropy); + Nat8.toNat(bytes[0]) % sides }; ``` -For multiple random values in a single call, use the `Finite` class to consume bytes incrementally from the same 32-byte blob rather than making multiple `raw_rand` calls: +For multiple random values in a single call, convert the 32-byte blob to an array and index directly — no additional `raw_rand` calls needed: ```motoko -import List "mo:core/List"; import Random "mo:core/Random"; +import List "mo:core/List"; +import Blob "mo:core/Blob"; import Nat8 "mo:core/Nat8"; public shared func rollMultipleDice(count : Nat, sides : Nat) : async [Nat] { - let entropy : Blob = await Random.blob(); - let f = Random.Finite(entropy); + let entropy = await Random.blob(); + let bytes = Blob.toArray(entropy); + // raw_rand returns 32 bytes; each byte gives one independent value let results = List.empty(); + var i = 0; label loop_ loop { - if (List.size(results) >= count) break loop_; - switch (f.byte()) { - case (?b) { List.add(results, Nat8.toNat(b) % sides) }; - case null { break loop_ }; - } + if (i >= count or i >= bytes.size()) break loop_; + List.add(results, Nat8.toNat(bytes[i]) % sides); + i += 1; }; List.toArray(results) }; @@ -144,19 +143,15 @@ A common use case is selecting one or more random elements from a list — for e ```motoko import Random "mo:core/Random"; +import Blob "mo:core/Blob"; import Nat8 "mo:core/Nat8"; public shared func pickWinner(participants : [Text]) : async ?Text { if (participants.size() == 0) { return null }; - let entropy : Blob = await Random.blob(); - let f = Random.Finite(entropy); - switch (f.byte()) { - case (?b) { - let idx = Nat8.toNat(b) % participants.size(); - ?participants[idx] - }; - case null { null }; - } + let entropy = await Random.blob(); + let bytes = Blob.toArray(entropy); + let idx = Nat8.toNat(bytes[0]) % participants.size(); + ?participants[idx] }; ``` @@ -220,7 +215,9 @@ The `std_rng` feature compiles `StdRng` without requiring OS entropy, which is c ## Example: random maze -The `random_maze` example in the ICP examples repository generates a maze using randomness to decide which walls to remove during a depth-first search. It demonstrates how to seed a Motoko `Finite` generator from a single `raw_rand` call and consume entropy incrementally across many cells: +The `random_maze` example in the ICP examples repository generates a maze using randomness to decide which walls to remove during a depth-first search. It demonstrates how to consume entropy incrementally across many cells. + +Note: this example predates `mo:core` and uses `mo:base/Random.Finite`. The patterns in this guide use `mo:core/Random` instead. - [random_maze (Motoko)](https://github.com/dfinity/examples/tree/master/motoko/random_maze) @@ -231,4 +228,4 @@ The `random_maze` example in the ICP examples repository generates a maze using - [Data Integrity](../security/data-integrity.md) — using randomness in a secure application design - [Inter-canister calls](../canister-calls/onchain-calls.md) — async patterns and reentrancy - +