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 certificate carries one, which requires the committee that certified the block to hold 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 certificate carries no threshold signature. 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 draws from either seed, and no Move function reports which one
the current block has. The EVM precompile refuses the block-hash seed
instead. Mainnet and 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.
Building and running it
The commands below create a package named dice whose example address is the account that
publishes it. <PROFILE> is a funded CLI profile, <ACCOUNT> is its address and <NETWORK> is
mainnet, testnet or devnet, the network the package targets. Save the module above as
dice/sources/dice.move after the first command:
supra move tool init --name dice --package-dir dice --framework-network <NETWORK> \
--named-addresses example=<ACCOUNT> --assume-yes
supra move tool publish --package-dir dice --profile <PROFILE> --accept-defaults
supra move tool run --function-id <ACCOUNT>::dice::roll --profile <PROFILE> --accept-defaults
supra move tool view --function-id <ACCOUNT>::dice::last_roll --args address:<ACCOUNT>--framework-network makes init write a dependency on the branch named <NETWORK> of
https://github.com/Entropy-Foundation/supra-framework.git, which holds the framework deployed on
that network. CLIs before v12.0.0, including the supra_node_v11.7.1 CLI that Mainnet and
Testnet run, have no such option and write a dependency on the dev branch of
Entropy-Foundation/aptos-core, which is out of sync with the Mainnet and Testnet frameworks. After
init, replace the [dependencies.SupraFramework] table in dice/Move.toml with:
[dependencies.SupraFramework]
git = "https://github.com/Entropy-Foundation/supra-framework.git"
rev = "<NETWORK>"
subdir = "supra-framework"The roll transaction emits a RandomnessGeneratedEvent, and last_roll returns the roll as a
one-element array holding a string, for example ["4"]. The network must have a seed, as
When a draw aborts describes.
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).
| Function | Result |
|---|---|
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.
| Function | Purpose |
|---|---|
initialize(framework) | creates PerBlockRandomness; restricted to the Supra framework signer, and called by the genesis of a network generated in testnet mode |
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’s session hash, and a transaction-local counter. The session hash is the hash of
the transaction’s sender, sequence number and script hash, the last of which is empty for an entry
function; it is not the hash of the signed transaction. 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. A private one compiles as it is, and a public one 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 six 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. Codes 2, 3 and 4 occur only on
a network at gas feature version 26 or later; see Minimum gas.
| Abort | What happened | What to do |
|---|---|---|
E_API_USE_IS_BIASIBLE, code 1 in supra_framework::randomness | the transaction is not marked unbiasable | submit a private or friend #[randomness] entry function as the payload; see The transaction boundary |
VM status MISSING_DATA | PerBlockRandomness has not been published on this network | Mainnet and Testnet publish it; on a local network, generate genesis in testnet mode |
EOPTION_NOT_SET, code 0x40001 in std::option | the network has no seed for this block, because SUPRA_DKG is disabled | enable SUPRA_DKG on the network; it has no randomness until then |
code 3 in supra_framework::randomness | the entry function sets #[randomness(min_remaining_gas = N)], and a fungible asset’s dispatch function requested a draw before the entry function’s first draw | withdraw, deposit or read the balance of that asset in a separate transaction; drawing first instead lets a sender who created the asset abort after the draw, see Outcome-dependent aborts |
code 2 in supra_framework::randomness | this was the transaction’s first draw, and less gas remained from the transaction’s max_gas_amount 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 alone as the limit; see Minimum gas |
code 4 in supra_framework::randomness | this was the transaction’s first draw, and less gas remained under the per-transaction execution or IO limit than the entry function’s min_remaining_execution_gas | send arguments that make the work before the first draw smaller; raising max_gas_amount does not help; see Minimum gas |
VM status ARITHMETIC_ERROR | a range function received an empty or reversed range | pass min_incl < max_excl |
bytes(0), permutation(0), and permutation(1) make no draw, so they succeed without checking
the first six 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.
Before the first draw, code the sender can influence matters in one respect. When the entry
function sets min_remaining_gas, a draw requested by a fungible asset’s dispatch function before
the entry function’s own first draw aborts the transaction with code 3 (see
Minimum gas). An entry function that transfers a fungible asset the sender chose
before its first draw can therefore be made to abort with code 3, with no draw served. Moving the
transfer after the draw is not a fix, because a transfer after the draw can abort and undo the
outcome, as above. Such an entry function withdraws, deposits or reads the balance of a fungible
asset the sender chose in a separate transaction: for example, a deposit into the module’s escrow
before the transaction that draws, and a claim after it.
An escrow protects the outcome only if nothing but the module can move what it holds. The creator
of a fungible asset can hold a TransferRef, which withdraws from and freezes any store of that
asset, the escrow’s included, so a stake in an asset the sender created can be taken out of the
escrow or frozen after the outcome is recorded, and the winner’s claim then aborts. Accept stakes
only in assets whose creator you trust, such as SUPRA.
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. The design holds when:
- the first transaction draws once for each stake and refuses a second draw for a stake that already has an outcome, so the sender cannot draw again until the outcome suits them;
- the stake is held where only the module can move it, as above;
- the second transaction can be sent by the party the outcome pays, or by anyone, so the party it does not pay cannot withhold 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 it uses a new sequence number and executes against a later block’s seed, 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.
On a network below gas feature version 26, the minimum in Minimum gas is not
checked. These designs, and the max_gas_amount() check described there, are then the protection
against outcome-dependent gas.
Minimum gas
From gas feature version 26, an entry function annotated #[randomness(min_remaining_gas = N)]
requires N gas to remain from the transaction’s max_gas_amount 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.
Not every network runs gas feature version 26 yet; the command below reads a network’s version. On a network below
version 26, a module that carries the attribute can be published, and it runs as if it declared no minimum: codes 2,
3 and 4 do not occur. Write a module for such a network as Outcome-dependent gas
describes, and add the attribute as well. The minimum then takes effect when the network reaches version 26, without
publishing the module again.
A network’s gas feature version is the feature_version field of the
0x1::gas_schedule::GasScheduleV2 resource. For Mainnet:
curl -s https://rpc-mainnet.supra.com/rpc/v3/accounts/0x1/resources/0x1::gas_schedule::GasScheduleV2 \
| jq -r .data.feature_versionFor Testnet and Devnet, use https://rpc-testnet.supra.com and https://rpc-devnet.supra.com.
The attribute can add a second minimum, min_remaining_execution_gas = M:
#[randomness(min_remaining_gas = 3000, min_remaining_execution_gas = 200)]
entry fun play(player: &signer, picks: vector<u8>) {
// ...
}M requires M gas to remain under each of two per-transaction limits at the first draw: the
execution limit (max_execution_gas in the gas schedule) and the IO limit (max_io_gas). A first
draw requested with less under either aborts with code 4. These limits do not depend on the
transaction’s max_gas_amount, so raising it does not change the result; the transaction has to
spend less before its first draw. M must not exceed N. Without M, neither limit is checked at
the draw.
The minimums are checked at a draw that the entry function’s own code requests. A draw requested by
a fungible asset’s dispatch function, the code that the asset’s creator registers to run on
withdraw, deposit and balance reads, before the first draw aborts with code 3. A dispatch
function’s draws after the first draw are served as any later draw is. A function the entry
function calls by name, such as a library function that draws, is part of the entry function’s own
code: its draw is the first draw when it comes first.
Gas spent before the first draw comes out of the sender’s limit and not out of N, so a
transaction 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 no property, max_gas alone, or min_remaining_gas with or without
min_remaining_execution_gas. #[randomness(max_gas = N)] is accepted for compatibility with
modules written for Aptos, and is not enforced. Building a package that uses min_remaining_gas or
min_remaining_execution_gas needs a Supra CLI whose compiler records them, which is the CLI of a
release after supra_node_v11.7.0. An older compiler either rejects them, or compiles the module
without recording the #[randomness] annotation at all, and every draw in such a module then aborts
with code 1. The supra_node_v11.7.0 CLI rejects them: min_remaining_gas alone fails with
Unknown key for randomness attribute: min_remaining_gas, and both properties together fail with
Randomness attribute should only have one `max_gas` property. Run the module’s unit tests with the same CLI, or call it
once on a test network, before relying on a build.
What the minimums cover
The check runs once, at the first draw. It compares N with the gas remaining from the
max_gas_amount at that moment, and M with the gas remaining under the execution and IO limits.
It covers what is charged against those budgets after that moment. It does not cover charges
deferred from work done before the draw, or limits that are not measured in gas.
Each rule below applies only to work that can vary with the transaction’s arguments or with state
the sender controls. An entry function whose work before its first draw is fixed needs no M and
no additions for that work.
N must cover:
- everything the transaction does after the first draw, at the largest input the entry function
accepts, including later draws and the work between them. This includes work that does not
depend on the drawn value:
Nis checked once, so work after the draw that the sender can enlarge must fit insideNat its maximum; - the IO gas and storage fees of the writes made after the draw. They are charged after the entry function returns, from the same remaining gas;
- the IO gas and storage fees of the writes made before the first draw. They are also charged after
the entry function returns, so the check at the draw does not see them. Bound the writes made
before the draw and add their maximum fees to
N, or move them after the draw.
Measure storage fees 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.
M must cover the execution and IO gas of the same work after the draw, at the largest input, and
the IO gas of the writes made before the first draw, which counts against the IO limit after the
entry function returns. Set M when the execution or IO gas spent before the first draw depends on
the transaction’s arguments, for example a loop over a vector argument or up to a count argument.
Without M, the arguments can leave less gas under the execution limit than the most expensive
outcome needs, and that outcome then fails after the draw with EXECUTION_LIMIT_REACHED.
Neither minimum covers:
- the memory quota. The VM limits the memory that a transaction’s values occupy. A value counts
until it is popped or the function holding it returns, not after its last use, so a vector
argument held by the entry function counts until the entry function returns. Before the first
draw, drop values whose size the sender chooses, with
let _ = v;or by passing them by value to a function that consumes them, or bound their length; - the dependency limits. The modules of the entry function and its type arguments are counted before it runs. A withdrawal or deposit of a fungible asset the sender chose loads that asset’s dispatch module when it runs, and the module counts against the dependency limits for the rest of the transaction;
- the IO gas for the transaction’s size and for the events emitted before the draw. Both are
charged after the entry function returns. The transaction’s size grows with its arguments, used
or not, so give a randomness entry function only arguments of fixed size or bound the length of
each, and include in
Nthe IO gas of the largest transaction it accepts; - the write limits. The VM refuses a transaction whose writes or events exceed the limits on the
number of writes, the bytes of one write, the bytes of all writes, the bytes of events, or the
storage fee, with
STORAGE_WRITE_LIMIT_REACHED. These are checked on the transaction’s whole output after it runs and are not measured in gas, so give every outcome writes and events of the same number and size.
When N and M cover every outcome, the sender cannot choose a gas limit or arguments that
complete one outcome and exhaust the gas of another. Outcomes whose writes and events differ in
number or size are not covered by N and M, as the last item above describes.
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 reads N from the entry function’s module and
offers the simulated gas plus N as the transaction’s max_gas_amount, for an entry function
payload and for a multisig payload that carries one. The simulated gas is at least the gas spent
before the first draw, so that limit passes the first draw’s comparison with N. A client that sets the limit to
the simulated gas alone sets it below N plus the gas before the first draw, and the draw aborts
with code 2; with such a client, set the limit explicitly.
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 Mainnet, Testnet and Devnet:
use std::transaction_context;
const MIN_GAS: u64 = 3000;
const E_GAS_LIMIT_TOO_LOW: u64 = 1;
// In the entry function, before its first draw:
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. Nor can it protect one that takes a
vector or string argument, used or not, unless the function asserts a bound on that argument’s
length before the check and MIN_GAS covers the largest transaction the bound allows: a longer
argument raises both the gas spent before the check and the IO gas charged for the transaction’s
size after the function returns.
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);
}Run it with supra move tool test --package-dir dice. 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. Save this script as draw.move:
script {
use supra_framework::randomness;
fun main(_account: &signer) {
let _ = randomness::u8_integer();
}
}Run it with:
supra move tool run-script --script-path draw.move --framework-network <NETWORK> \
--profile <PROFILE> --accept-defaultsWith a CLI before v12.0.0, replace --framework-network <NETWORK> with
--framework-local-dir <DIR>, where <DIR> is the supra-framework directory of a clone made with
git clone --branch <NETWORK> https://github.com/Entropy-Foundation/supra-framework.git.
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, because simulation 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.
Related APIs
- Using Native Randomness from an EVM Contract uses the same threshold-derived block seed through SupraEVM’s rules and derivation.
- Reading the Block Timestamp in Microseconds describes the shared block clock. A timestamp is public and predictable enough to bias; it is not a randomness source.