Skip to main content

Secure Randomness

Sapphire.randomBytes is OXN's on-chain random number source. It draws from the enclave's own entropy — not from block hashes, timestamps, or validator-controlled values — making it unpredictable to observers and unmanipulable by validators.

Signature

function randomBytes(uint256 numBytes, bytes memory personalization)
internal view returns (bytes memory);
  • numBytes — how many random bytes you want. Typically 32 for a uint256, less for smaller ranges.
  • personalization — optional context. Different personalization strings yield different random streams even within the same block, useful when you need multiple independent random draws.

Returns bytes of length numBytes.

Basic usage

import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";

contract Raffle {
address[] public participants;

function pickWinner() external view returns (address) {
require(participants.length > 0, "no participants");
bytes memory r = Sapphire.randomBytes(32, "");
uint256 idx = uint256(bytes32(r)) % participants.length;
return participants[idx];
}
}

Two random draws in one call

If you need multiple independent random values within one function, use different personalization values:

bytes memory r1 = Sapphire.randomBytes(32, "draw-1");
bytes memory r2 = Sapphire.randomBytes(32, "draw-2");

Without different personalization, calling the precompile twice in one block gives you the same result. This is deliberate — the entropy is derived per-block; personalization strings let you produce independent streams from the same underlying source.

Security properties

Unpredictable to observers. The random output is generated inside the enclave, from entropy that never leaves the enclave. No RPC observer, block explorer, or node operator can predict the value ahead of its use.

Unmanipulable by validators. Because the entropy source is inside the enclave rather than derived from block metadata, validators cannot bias the outcome by re-ordering transactions or choosing which block to include a call in.

Not future-predictable. Random values produced now cannot be predicted by simulating future blocks, because the entropy stream depends on runtime state that only advances forward.

What NOT to use for randomness

block.timestamp, block.number, block.hash, PREVRANDAO — all public and predictable. Any observer can read them; validators can manipulate them. Do not use for anything that gives out value based on randomness.

keccak256(abi.encodePacked(block.timestamp, msg.sender)) — a common Ethereum pattern that provides zero real randomness. Every input is public.

Off-chain oracles — introduce trust in the oracle. Fine for public data but pointless for confidential contracts.

Only Sapphire.randomBytes provides real cryptographic randomness on OXN.

Common patterns

Fair raffle selection:

function selectWinner(address[] memory candidates) internal view returns (address) {
bytes memory r = Sapphire.randomBytes(32, "raffle");
return candidates[uint256(bytes32(r)) % candidates.length];
}

Random shuffle of a small array:

function shuffle(uint256[] memory arr) internal view returns (uint256[] memory) {
for (uint256 i = arr.length - 1; i > 0; i--) {
bytes memory r = Sapphire.randomBytes(32, abi.encode(i));
uint256 j = uint256(bytes32(r)) % (i + 1);
(arr[i], arr[j]) = (arr[j], arr[i]);
}
return arr;
}

Encrypted per-user secret:

function generateSecret() internal view returns (bytes32) {
return bytes32(Sapphire.randomBytes(32, abi.encode(msg.sender)));
}

Gas cost

Sapphire.randomBytes has a base gas cost plus a small per-byte cost. See Gas Cost Reference for exact numbers. As a rule of thumb, generating 32 random bytes is comparable in cost to a sha256 of a 32-byte input.

Next steps