Blockchain RNG (Random Number Generator) System Development
block.timestamp, block.prevrandao, hash of previous block — all of these have been tried as randomness sources. And all of it is unsafe: a miner or validator can influence these values in their interests. In 2019 a lottery smart contract lost $4M — an attacker controlled several pools and could choose when to send a transaction, manipulating the block hash. On-chain random numbers are non-trivial, solved differently depending on threat model.
Why On-Chain Randomness is Hard
Blockchain is deterministic. Every node must reach the same result executing the same operations. This fundamentally contradicts randomness: if the result is predictable — it's not random. Any source visible on the blockchain before the result is finalized can be exploited by an attacker.
Validator bias — on Ethereum a validator sees block.prevrandao (RANDAO reveal) before publishing a block. If the result is unfavorable — they can skip their slot (slot is skipped, result changes). Attack cost = missed slot reward (~0.01 ETH). If lottery stake > 0.01 ETH — attack is rational.
Chainlink VRF: Standard for Most Cases
Chainlink VRF (Verifiable Random Function) — the most proven solution for NFT minting, lotteries, game mechanics. Works through an oracle network:
- Contract requests random number, sends LINK
- Chainlink node generates random number and cryptographic proof
- Proof is verified on-chain before using the number
// VRF V2.5 (current version)
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
contract Lottery is VRFConsumerBaseV2Plus {
uint256 public s_subscriptionId;
bytes32 public keyHash; // gas lane
uint32 public callbackGasLimit = 200_000;
uint16 public requestConfirmations = 3;
mapping(uint256 => address) public requestToPlayer;
function requestRandomWinner() external returns (uint256 requestId) {
requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: keyHash,
subId: s_subscriptionId,
requestConfirmations: requestConfirmations,
callbackGasLimit: callbackGasLimit,
numWords: 1,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
requestToPlayer[requestId] = msg.sender;
}
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
address player = requestToPlayer[requestId];
uint256 result = randomWords[0] % totalTickets;
_declareWinner(player, result);
}
}
requestConfirmations: 3 — wait 3 block confirmations before generation. This complicates reorg attacks on requests.
VRF Limitations: latency 1-3 blocks (15-45 seconds on mainnet), LINK cost per request (0.25-2 LINK depending on network), need for subscription management. For high-frequency gameplay (each turn needs random) — too expensive and slow.
Commit-reveal: Randomness Without Oracle
For cases without Chainlink access or when minimizing costs — commit-reveal scheme:
Commit phase: each participant publishes keccak256(secret, address). Secret is stored off-chain.
Reveal phase: participants reveal secret. XOR of all secrets = final random number.
mapping(address => bytes32) public commits;
mapping(address => uint256) public reveals;
uint256 public combinedRandom;
function commit(bytes32 commitment) external {
commits[msg.sender] = commitment;
}
function reveal(uint256 secret) external {
require(keccak256(abi.encode(secret, msg.sender)) == commits[msg.sender]);
reveals[msg.sender] = secret;
combinedRandom ^= secret; // XOR all reveals
}
Weak point of commit-reveal: the last person to reveal sees the final result before publishing. They can choose not to reveal (griefing) or reveal only if result is favorable. Mitigation: penalty for not-revealing (bond on commit that burns if no-show).
RANDAO: Native Ethereum Randomness After Merge
After PoS transition Ethereum provides block.prevrandao — aggregated RANDAO reveal from validators. This is better than old block.difficulty, but has the validator bias problem described above.
For non-critical uses (cosmetics in games, queue order, small lotteries) — block.prevrandao is sufficient and free:
uint256 random = uint256(keccak256(abi.encode(
block.prevrandao,
block.timestamp,
msg.sender,
nonce++
)));
Adding msg.sender and nonce increases entropy and complicates prediction for a specific user, though doesn't eliminate validator bias.
Choosing Solution per Task
| Application | Stake / Value | Recommendation |
|---|---|---|
| NFT mint (whitelist random) | High | Chainlink VRF |
| Lottery with large prize | High | Chainlink VRF + requestConfirmations: 5+ |
| In-game random (items) | Medium | Commit-reveal or Chainlink VRF |
| Queue order | Low | block.prevrandao |
| PvP matchmaking | Low | block.prevrandao + nonce |
Hybrid Solutions
For gamefi projects requiring fast random with high throughput, use off-chain VRF with on-chain commitment:
- Backend generates seed via Chainlink VRF in advance
- Seed hash is published on-chain (commitment)
- For each game event — use HMAC(seed, event_id) as random
- After session — reveal seed, users can verify all results
This gives instant response to each action and full verifiability post-factum.
Work Process
Analysis. Determine threat model: who can attack? What's the maximum benefit from manipulation? What latency is acceptable? Is Chainlink available on target chain?
Development and testing. VRFConsumer tested via VRFCoordinatorV2_5Mock from Chainlink package — allows simulating fulfillment in unit tests without real oracle network. Commit-reveal tested on griefing and last-revealer attack scenarios.
Deployment. For Chainlink VRF — create subscription, fund LINK, add consumer. Configure subscription balance monitoring.
Timeline Estimates
Chainlink VRF integration into existing contract: 1-2 days. RNG system with commit-reveal and anti-griefing: 1-2 days. Hybrid off-chain VRF with on-chain commitment and verification: 3-5 days.







