suideveloper
All questions
Objects

How do I use on-chain randomness on Sui?

November 10, 2026

Sui has secure randomness built into the protocol through the sui::random module. Write a private entry function that takes r: &Random (the system object at 0x8), create a RandomGenerator inside the function with random::new_generator(r, ctx), and draw values from it, for example generate_u8_in_range(&mut gen, 1, 6). The compiler and the network enforce a few rules that stop callers from looking at the outcome and reverting it.

A minimal example

Move
module example::dice;

use sui::random::{Self, Random};

public struct Die has key, store {
    id: UID,
    value: u8,
}

entry fun roll(r: &Random, ctx: &mut TxContext) {
    let mut generator = random::new_generator(r, ctx);
    let value = generator.generate_u8_in_range(1, 6);
    transfer::public_transfer(Die { id: object::new(ctx), value }, ctx.sender());
}

Call it with 0x8 as the argument. From TypeScript (@mysten/sui v2) use the helper:

TypeScript
import { Transaction } from '@mysten/sui/transactions'

const tx = new Transaction()
tx.moveCall({
  target: `${PACKAGE_ID}::dice::roll`,
  arguments: [tx.object.random()],
})

From the CLI: sui client call --package <PACKAGE_ID> --module dice --function roll --args 0x8.

The generator API

RandomGenerator is a pseudorandom generator seeded from the global randomness and unique to each transaction. It provides:

  • generate_u8 / u16 / u32 / u64 / u128 / u256, plus generate_bool
  • generate_u8_in_range through generate_u128_in_range (inclusive [min, max], bias at most 2^-64)
  • generate_bytes(n) and shuffle(&mut vector<T>)

The randomness itself comes from the validators. They run a distributed key generation (DKG) each epoch and then produce fresh values with a random beacon protocol. A RandomnessStateUpdate system transaction writes each new value into the Random object.

Rules that protect you

These are enforced, not just recommended:

  1. public functions can't take &Random. If they could, another package could wrap your function, look at the result and abort whenever it lost, so a bet would only ever go through on a win. A private entry function can't be called from other Move code. See entry vs public.
  2. public functions can't take a RandomGenerator. A generator passed in from outside can have its internal state read (for example with bcs::to_bytes), which makes its output predictable. Always create it inside the function that uses it.
  3. PTB restriction. After a MoveCall that uses Random, a PTB may only contain TransferObjects or MergeCoins commands. That stops a later command from inspecting the result and aborting the whole transaction.
  4. Random is read-only. Any transaction that tries to take it mutably fails.

Gotchas the compiler can't catch

  • Gas-based attacks. If the winning branch costs much more gas than the losing branch, an attacker can set a gas budget that only covers the losing path. Then a win runs out of gas and reverts. Keep all outcome paths at roughly equal cost, or split the flow.
  • Commit-reveal for high stakes. In transaction 1, take payment, draw the random value and store it in an object the caller can't read in the same transaction. In transaction 2, reveal the result and settle. Make sure the inputs to step 2 can't change after step 1, and plan for users who never come back to finish step 2.
  • Per-transaction limits (new objects, events, dynamic fields) also count as resources an attacker could exhaust on purpose. See object and transaction size limits.
  • Transactions that use Random go through consensus because 0x8 is a shared object.

Testing

In unit tests, random::new_generator_for_testing() gives you a non-deterministic generator, and random::new_generator_from_seed_for_testing(seed) gives you a deterministic one. You can also create a test Random object with random::create_for_testing inside a test_scenario.

Sources

More on objects