Skip to Content
Supra Layer 1MoveVMMove Native Randomness

Using Native Randomness from Move

Supra Move modules obtain synchronous randomness from supra_framework::randomness. The block prologue writes a per-block seed into PerBlockRandomness.

Where the seed comes from

A seed exists only while the SUPRA_DKG feature is enabled. It is one of two values:

  • The BLS threshold signature on the block’s quorum certificate, when the committee that certified the block holds DKG threshold keys. Computing the signature takes as many committee members as the certificate’s signing threshold, each member counting once regardless of stake. While fewer members than that collude, the seed can be neither predicted before the block is certified nor biased by validators, module developers, or users.
  • The committed block hash, when the certifying committee holds no threshold keys. The hash is known to validators before the block commits and depends on contents the proposer chose, so it is predictable and biasable.

A committee holds threshold keys only for an epoch that follows an epoch in which SUPRA_DKG was enabled, because the DKG runs at the end of an epoch to produce the next epoch’s keys, and it does not run at the end of the genesis epoch. With SUPRA_DKG enabled, the block hash is therefore the seed for exactly:

  • the genesis epoch and the epoch after it, on a network whose genesis enables SUPRA_DKG;
  • the first epoch after governance enables SUPRA_DKG, including a re-enablement after it was disabled.

supra_framework::randomness serves draw from either seed, and no Move function reports which one the current block has. Supra mainnet and Supra testnet both run with threshold-signature seeds, so neither serves the block-hash seed. A local network generated with SUPRA_DKG enabled serves it for its first two epochs, and a local network with SUPRA_DKG disabled has no seed at all (see When a draw aborts).

The value is random, not secret. Transaction execution and its results are visible on chain. Do not use this API to create private keys, hidden card orders, or other secrets.

Quick start

This entry function stores a die roll in [1, 7). It is private and carries the #[randomness] attribute required by the VM.

module example::dice { use std::signer; use supra_framework::randomness; struct LastRoll has key { value: u64, } #[randomness] entry fun roll(account: &signer) acquires LastRoll { let account_address = signer::address_of(account); let value = randomness::u64_range(1, 7); if (exists<LastRoll>(account_address)) { borrow_global_mut<LastRoll>(account_address).value = value; } else { move_to(account, LastRoll { value }); }; } #[view] public fun last_roll(account: address): u64 acquires LastRoll { borrow_global<LastRoll>(account).value } }

Submit example::dice::roll as the transaction payload. A user’s Move script or a public entry function cannot replace that transaction boundary.

Randomness API

The integer functions return values across the full range of their type. Every range uses an inclusive lower bound and an exclusive upper bound: [min_incl, max_excl).

FunctionResult
bytes(n)n random bytes; n = 0 returns an empty vector
u8_integer()a random u8
u16_integer()a random u16
u32_integer()a random u32
u64_integer()a random u64
u128_integer()a random u128
u256_integer()a random u256
u8_range(min_incl, max_excl)a random u8 in [min_incl, max_excl)
u16_range(min_incl, max_excl)a random u16 in [min_incl, max_excl)
u32_range(min_incl, max_excl)a random u32 in [min_incl, max_excl)
u64_range(min_incl, max_excl)a random u64 in [min_incl, max_excl)
u128_range(min_incl, max_excl)a random u128 in [min_incl, max_excl)
u256_range(min_incl, max_excl)a random u256 in [min_incl, max_excl)
permutation(n)a random permutation of [0, 1, ..., n - 1]; n = 0 returns an empty vector

The range functions require min_incl < max_excl. An empty or reversed range aborts during arithmetic. Their modulo reduction has negligible bias rather than perfect uniformity. Applications that require exact uniformity can use rejection sampling with an integer function.

bytes(n) draws in 32-byte chunks. u256_range draws twice, and permutation(n) draws repeatedly. Their gas use therefore grows with the requested byte count or permutation size; bound values that come from a transaction argument.

Other public functions

randomness.move also exposes the following public functions. They are not all application-facing randomness wrappers.

FunctionPurpose
initialize(framework)creates PerBlockRandomness; restricted to the Supra framework signer and used by genesis
initialize_for_testing(framework)test-only initialization with a 32-byte zero seed
set_seed(seed)replaces the seed in Move unit tests; test-only and requires exactly 32 bytes
u64_range_internal(min_incl, max_excl)performs the same range calculation used by u64_range, but does not emit RandomnessGeneratedEvent

Applications should call u64_range, not u64_range_internal, when they want the event behavior of the other public randomness wrappers.

Each draw is fresh

The module derives each 32-byte draw with SHA3-256 over a domain separator, the current block seed, the transaction hash, and a transaction-local counter. The native fetch_and_increment_txn_counter advances the counter for every draw. Repeated calls in one transaction therefore use different inputs, as do calls in different transactions in one block.

One public call can consume several draws, as bytes, u256_range, and permutation do. Freshness applies to each draw rather than only to each public wrapper call.

The transaction boundary

The VM serves randomness to a transaction it has marked unbiasable. A user transaction is marked when its payload is a private or friend entry function annotated with #[randomness], and the entry function of an automated task is marked on the same condition. The entry function can call private helpers; the check applies to the payload entry function at the outer transaction boundary. While SUPRA_DKG is enabled, the VM also marks two system transactions: a governance-approved script and the block-metadata transaction.

This requirement prevents a test-and-abort attack. Without it, another module could call a public randomness-dependent function, inspect the result, and abort whenever the result was unfavorable. Repeating until a transaction succeeds would change the distribution of committed outcomes.

The Move compiler enforces the same boundary when a package is built:

  • #[randomness] on a public entry function is an error;
  • a private or friend entry function that reaches a randomness API without #[randomness] is an error;
  • a public function that reaches a randomness API is an error unless it carries #[lint::allow_unsafe_randomness].

All randomness-dependent helpers must therefore remain private or friend. Suppressing the lint with #[lint::allow_unsafe_randomness] lets another module place the helper behind its own annotated private entry function, observe the result, and abort selectively.

The runtime check applies to the published bytecode whatever compiler produced it. Every underlying random draw checks is_unbiasable(), and the first draw attempted by a transaction that is not marked aborts in supra_framework::randomness with E_API_USE_IS_BIASIBLE, code 1. The shapes that reach this abort are:

  • a user’s Move script;
  • a public entry function carrying #[lint::allow_unsafe_randomness];
  • a #[view] function, which is public and so needs the same lint suppression to compile;
  • bytecode that was not built with the checks above.

When a draw aborts

Each draw checks the first four rows in order, and the first one that fails aborts the transaction. A range function computes its range before drawing, so a reversed range aborts before any of those checks, and an empty range aborts after its draw.

AbortWhat happenedWhat to do
E_API_USE_IS_BIASIBLE, code 1 in supra_framework::randomnessthe transaction is not marked unbiasablesubmit a private or friend #[randomness] entry function as the payload; see The transaction boundary
VM status MISSING_DATAPerBlockRandomness has not been published on this networkSupra mainnet and Supra testnet publish it; on a local network, generate genesis in testnet mode
EOPTION_NOT_SET, code 0x40001 in std::optionthe network has no seed for this block, because SUPRA_DKG is disabledenable SUPRA_DKG on the network; it has no randomness until then
code 2 in supra_framework::randomnessthis was the transaction’s first draw, and less gas remained than the entry function’s #[randomness(min_remaining_gas = N)]raise the transaction’s max_gas_amount, and do not use a simulation’s gas estimate as the limit; see Minimum gas
VM status ARITHMETIC_ERRORa range function received an empty or reversed rangepass min_incl < max_excl

bytes(0), permutation(0), and permutation(1) make no draw, so they succeed without checking the first four rows.

Outcome-dependent aborts

The private-entry rule prevents an external wrapper from testing a result and aborting. The module that reads the value can still abort after the read. Its code must not make an unfavorable result selectively reversible.

After a draw, any call into code the sender can influence can abort the transaction and undo the outcome. In Move this includes:

  • a transfer of a fungible asset the sender created, whose dispatch functions run on withdraw and deposit;
  • a call that is generic over a type the sender chose in the transaction’s type arguments, or that operates on an object the sender passed in;
  • a payout to the sender that can fail because of state the sender controls, such as a store that is frozen.

Writing the outcome to storage before such a call does not protect it. An abort unwinds the whole transaction, storage writes included.

Keep any operation that can abort independently of the random value before the draw. After the draw, avoid calls and assertions whose failure depends on the outcome. A two-transaction design records the result in the first transaction and applies it in a later one, after the result has committed. The first transaction must store the drawn value itself: Move has no function that returns a past block’s seed, so a later transaction cannot recompute it.

Outcome-dependent gas

The sender chooses the transaction’s gas limit, and running out of gas aborts the transaction and rolls back the draw exactly as an explicit abort does. If the work after a draw costs more on an outcome the sender would reject, the sender can choose a limit that completes the outcomes they want and exhausts the others. Every resubmission is served a fresh value, because the transaction hash feeds each draw, so repeated attempts skew the outcomes that commit.

Make the gas consumed after a draw independent of the result. A fixed-shape write of the result, followed by a separate settlement transaction, removes outcome-dependent work from the randomness transaction. If the paths cannot be equal, an unfavorable outcome must not cost more than the favorable outcome a sender is trying to retain. This inequality depends on the gas schedule and is less durable than equal paths. Restricting the randomness entry function to a trusted signer also removes an untrusted sender’s control over retries and gas limits.

Minimum gas

From gas feature version 26, an entry function annotated #[randomness(min_remaining_gas = N)] requires N gas to remain when the transaction’s first draw is requested. A first draw requested with less aborts with code 2, before any value is produced. Later draws in the same transaction are not checked: a check there would be an abort that depends on the earlier results, which is what the minimum exists to prevent.

The check measures the gas actually left at the first draw, so gas spent before it, including work that depends on arguments the sender chooses, comes out of the sender’s limit and not out of N. A transaction therefore needs a max_gas_amount of at least N plus the gas it spends before its first draw. A sender who declares less pays for the gas used up to the aborted draw.

An entry function annotated #[randomness] without min_remaining_gas has no minimum. The attribute accepts one property. #[randomness(max_gas = N)] is also accepted, for compatibility with modules written for Aptos, and is not enforced. Building a package that uses min_remaining_gas needs a Supra CLI whose compiler accepts it; an older compiler rejects it as an unknown key.

Choose N to cover the most expensive outcome from the first draw to the end of the transaction, including any later draws and the work between them:

  • Include the storage fees of that outcome’s writes. They are charged after the entry function returns, from the same remaining gas.
  • Measure at the network’s minimum gas unit price. Storage fees are fixed amounts of SUPRA, converted to gas units by dividing by the transaction’s gas unit price, so the same write costs more gas units at a lower price.

When N covers every outcome, no gas limit the sender chooses can complete one outcome and exhaust another.

A simulation’s gas estimate covers only the outcome the simulation happened to draw, and does not include N. Without --max-gas, the Supra CLI offers that estimate as the transaction’s max_gas_amount, which is below N plus the gas before the first draw, so the draw aborts with code 2. Pass --max-gas explicitly for these entry functions.

Before gas feature version 26, an entry function can check its own limit with transaction_context::max_gas_amount(), which requires the TRANSACTION_CONTEXT_EXTENSION feature, enabled on Supra mainnet:

assert!(transaction_context::max_gas_amount() >= MIN_GAS, E_GAS_LIMIT_TOO_LOW);

This checks the transaction’s limit, not the gas remaining at the draw, so MIN_GAS must also cover everything the transaction spends before the draw. It cannot protect an entry function whose work before the draw depends on arguments the sender chooses.

Automated tasks

An automated task whose entry function is a private or friend #[randomness] function draws randomness each time it runs. The registrant chooses the task’s max_gas_amount at registration, so the rule in Outcome-dependent gas applies to that limit in the same way as to a user transaction’s gas limit.

A minimum gas amount applies to tasks as well. A registration transaction whose task max_gas_amount is below its entry function’s min_remaining_gas is discarded and pays nothing. A multisig registration below it is not registered, but the executing owner’s transaction is kept and charged, the proposal is consumed and has to be proposed again with a larger max_gas_amount, and status 39 (REQUIRED_DEPOSIT_INCONSISTENT_WITH_TXN_MAX_GAS) is the error_code of the multisig account’s execution-failure event. The comparison is a lower bound: each run also spends intrinsic and dependency gas before its first draw, so register the task with N plus that gas. Each run is then subject to the first-draw check. Registration from Move code is not checked against the minimum, so a task registered that way below the minimum aborts with code 2 on every run that draws.

Cost

Each 32-byte draw computes one SHA3-256 over the domain separator, the seed, the transaction hash and the counter, about 150 bytes with a threshold-signature seed. At the current gas schedule that hash costs about 0.04 gas units (a base of 14,704 internal gas plus 165 per byte, at 1,000,000 internal gas per gas unit). Each completed wrapper call also emits one RandomnessGeneratedEvent, with a base cost of 20,006 internal gas, and runs the wrapper’s own Move instructions. The gas that matters is the work your code does after a draw, and bytes(n), u256_range, and permutation(n) multiply the draw count as described in Randomness API.

RandomnessGeneratedEvent

Each successful call to bytes, a public integer wrapper, a public range wrapper, or permutation emits one zero-field RandomnessGeneratedEvent. This includes bytes(0) and permutation(0), which emit despite consuming no underlying draw. The event records that one of those wrappers completed. It does not identify the function, arguments, returned value, or number of underlying 32-byte draws. Like the rest of a transaction’s effects, the event is removed if the transaction aborts.

A direct call to the public u64_range_internal function produces randomness without emitting the event. An indexer cannot treat the event count as a complete count of every possible direct call to the module’s public functions.

Unit tests

Move unit tests can initialize the resource and choose a deterministic seed with the test-only functions. The seed passed to set_seed must contain 32 bytes. Add the following test to the example::dice module from the quick start:

#[test(account = @0xcafe, fx = @supra_framework)] fun test_roll(account: signer, fx: signer) acquires LastRoll { randomness::initialize_for_testing(&fx); randomness::set_seed( x"0101010101010101010101010101010101010101010101010101010101010101" ); roll(&account); let value = last_roll(signer::address_of(&account)); assert!(1 <= value && value < 7, 0); }

Move unit tests treat randomness calls as unbiasable, so this test exercises the module logic and deterministic seed. It does not exercise transaction-payload admission. Exercise the annotated entry function end to end by publishing the module to a local Supra network whose genesis enables SUPRA_DKG, and submitting example::dice::roll directly as the transaction payload. On a network without a seed every draw aborts; see When a draw aborts.

To check the runtime refusal, submit a user script that calls a randomness API; it aborts with code 1. A public or unannotated entry function that reaches randomness does not compile, so it cannot be submitted without suppressing the checks described in The transaction boundary.

Simulation

Simulating a #[randomness] transaction runs the same admission check as execution, so it is admitted or refused with code 1 as the real transaction would be. The value it draws is not the value the submitted transaction receives: simulation takes a transaction without a valid signature, and every draw depends on the transaction hash, which changes once the transaction is signed. Simulation also reads the seed of the latest committed block, not the block that will include the transaction.

The gas a simulation reports therefore covers only the outcome its draw happened to produce. Set the gas limit for the most expensive outcome rather than from a single estimate.

Last updated on