Quickstart
Two calls, across two blocks. request commits; reveal settles once the future block is mined.
uint256 id = entropy.request(seed); // TX 1 — commit; id from the Requested event // wait until block.number > revealBlock (poll status() to 2 = READY, ~10s on KUB) uint256 r = entropy.reveal(id); // TX 2 — settle; r is your randomness
block.number > revealBlock (revealBlock = requestBlock + 1), so it succeeds only at least 2 blocks after request. Poll status(id) to 2 (READY); never reveal one block after request.Addresses & config
| Item | Value |
|---|---|
| Chain | KUB Chain (Bitkub) · chainId 96 (0x60) |
| RPC | https://rpc.bitkubchain.io · https://rpc-l1.bitkubchain.io — the status-poll loop hits the RPC repeatedly, so use a fallback set (viem fallback() transport, below) so one rate-limited node can't stall it |
| Contract | 0xf2DF5c645d84Deb994979d07C0b02410D718754E ✓ verified |
| Explorer | https://www.kubscan.com/address/<addr> · /tx/<hash> · /block/<n> |
| Compiler | solc 0.8.24 · optimizer 200 · evm paris |
Ownerless and immutable — this address never changes. The core contract is source-available (don't fork/redeploy); the interface, helper, and examples below are MIT.
Contract functions
Commit. Reserves the next block (revealBlock = block.number + 1) and returns a globally-unique id. Read id from the Requested event. seed is an optional commitment folded into the result (pass bytes32(0) if unused — it adds no entropy, only binds the number to data you commit now).
Settle and return the randomness keccak256(blockhash(revealBlock), id, requester, seed). Anyone may call it; a second call on a settled id returns the same number and emits no new event.
too early (before the reveal block — this is what blocks atomic grinding), expired (after the 256-block window), no request (unknown id).Lifecycle: 0 NONE · 1 WAITING · 2 READY · 3 EXPIRED · 4 FULFILLED. Poll to 2 before revealing; bail on 3 (a request can go WAITING → EXPIRED without ever showing READY if nobody settles in 256 blocks).
The stored randomness for a settled id. Returns 0 for any id that is not FULFILLED.
The full request record. Use it (or the Requested event) to get requester, revealBlock, and seed for the off-chain recompute.
The id the next request will receive.
The reveal window, in blocks. Always 256.
Events
Emitted by request. The canonical source for the new id + the inputs needed to verify.
Emitted once, on the first reveal of an id. A second reveal emits nothing.
EntropyLib helpers (MIT)
One reveal is a full 256-bit number. Derive as many independent, domain-separated values as you need — don't make multiple requests for one event. EntropyLib is an internal pure library you inline into your contract (no external call).
using EntropyLib for uint256; uint256 r = entropy.reveal(id); uint256[] memory vals = r.expand(8); // 8 independent values (n <= 256) uint256 pocket = r.pick(0, 37); // roulette 0..36 (max != 0) uint256 dice = r.pick(1, 6); // a d6 — salt 1, independent of pocket uint256 lotto = r.digits(6); // 6 packed digits e.g. 482917 (m <= 77)
digits returns a number, so leading zeros drop (004821 → 4821) — pad when displaying. pick is effectively unbiased for max well below 2^256.
Frontend integration
ethers v6 — full flow
import { Contract, BrowserProvider } from "ethers"; const ENTROPY = "0xf2DF5c645d84Deb994979d07C0b02410D718754E"; const entropy = new Contract(ENTROPY, ABI, await (new BrowserProvider(window.ethereum)).getSigner()); const sleep = ms => new Promise(r => setTimeout(r, ms)); // 1) commit — capture id, requester, seed (needed to verify later) const seed = "0x" + "00".repeat(32); const rc = await (await entropy.request(seed)).wait(); const id = rc.logs.map(l => { try { return entropy.interface.parseLog(l); } catch { return null; } }) .find(p => p && p.name === "Requested").args.id; // 2) poll status to READY (2); bail on EXPIRED (3) let st = Number(await entropy.status(id)); while (st === 1) { await sleep(2500); st = Number(await entropy.status(id)); } if (st === 3) throw new Error("expired — treat as a loss"); // 3) reveal + read await (await entropy.reveal(id)).wait(); const r = await entropy.results(id); // uint256 const fourDigits = (r % 10000n).toString().padStart(4, "0");
viem — same flow
import { createPublicClient, createWalletClient, custom, http, fallback, parseEventLogs } from "viem"; const ENTROPY = "0xf2DF5c645d84Deb994979d07C0b02410D718754E", seed = "0x" + "00".repeat(32); const chain = { id: 96, name: "KUB", nativeCurrency: { name: "KUB", symbol: "KUB", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.bitkubchain.io"] } } }; // fallback() rotates RPCs so the status poll loop survives a rate-limited node const pub = createPublicClient({ chain, transport: fallback([http("https://rpc.bitkubchain.io"), http("https://rpc-l1.bitkubchain.io")]) }); const wallet = createWalletClient({ chain, transport: custom(window.ethereum) }); const [account] = await wallet.getAddresses(); const rc = await pub.waitForTransactionReceipt({ hash: await wallet.writeContract({ account, address: ENTROPY, abi: ABI, functionName: "request", args: [seed] }) }); const [{ args: { id } }] = parseEventLogs({ abi: ABI, eventName: "Requested", logs: rc.logs }); let st = await pub.readContract({ address: ENTROPY, abi: ABI, functionName: "status", args: [id] }); while (st === 1) { await new Promise(r => setTimeout(r, 2500)); st = await pub.readContract({ address: ENTROPY, abi: ABI, functionName: "status", args: [id] }); } if (st === 3) throw new Error("expired"); await pub.waitForTransactionReceipt({ hash: await wallet.writeContract({ account, address: ENTROPY, abi: ABI, functionName: "reveal", args: [id] }) }); const r = await pub.readContract({ address: ENTROPY, abi: ABI, functionName: "results", args: [id] });
Solidity consumers
Bind the live contract and call two functions. Follow the three rules (below) or you can get drained.
This interface plus the address is the entire on-chain SDK — nothing to install. Save it next to your contract as IDurianEntropy.sol (MIT, free to copy):
// SPDX-License-Identifier: MIT pragma solidity 0.8.24; interface IDurianEntropy { function request(bytes32 seed) external returns (uint256 id); function reveal(uint256 id) external returns (uint256 randomness); function results(uint256 id) external view returns (uint256 randomness); function status(uint256 id) external view returns (uint8); // 0 NONE 1 WAITING 2 READY 3 EXPIRED 4 FULFILLED }
import "./IDurianEntropy.sol"; IDurianEntropy constant entropy = IDurianEntropy(0xf2DF5c645d84Deb994979d07C0b02410D718754E); struct Pending { uint256 reqId; bool open; } mapping(address => Pending) public pending; function commitMint() external payable { // click 1: pay + lock the roll require(msg.value == PRICE); require(!pending[msg.sender].open, "pending"); pending[msg.sender] = Pending(entropy.request(bytes32(uint256(uint160(msg.sender)))), true); } function claimMint(address user) external { // click 2 (2 blocks later): permissionless Pending memory p = pending[user]; require(p.open, "none"); uint256 r = entropy.reveal(p.reqId); delete pending[user]; _mint(user, pickTier(r)); // grind-proof: payment already committed } function expireMint(address user) external { // rule 3: clear an expired roll = forfeit, no re-roll require(pending[user].open && entropy.status(pending[user].reqId) == 3, "not expired"); delete pending[user]; }
A full coin-flip reference (escrow, permissionless settle, forfeit) is in the downloadable guide.
Verify any result off-chain (provably fair)
Anyone can recompute a settled result from public chain data. Read requester, revealBlock, seed from requests(id).
import { keccak256, encodePacked } from "viem"; const req = await pub.readContract({ address: ENTROPY, abi: ABI, functionName: "requests", args: [id] }); const block = await pub.getBlock({ blockNumber: BigInt(req.revealBlock) }); const r = BigInt(keccak256(encodePacked( ["bytes32", "uint256", "address", "bytes32"], [block.hash, BigInt(id), req.requester, req.seed] ))); // === results(id)
ABI
[
{"type":"function","name":"request","stateMutability":"nonpayable","inputs":[{"name":"seed","type":"bytes32"}],"outputs":[{"name":"id","type":"uint256"}]},
{"type":"function","name":"reveal","stateMutability":"nonpayable","inputs":[{"name":"id","type":"uint256"}],"outputs":[{"name":"randomness","type":"uint256"}]},
{"type":"function","name":"results","stateMutability":"view","inputs":[{"name":"id","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},
{"type":"function","name":"status","stateMutability":"view","inputs":[{"name":"id","type":"uint256"}],"outputs":[{"name":"","type":"uint8"}]},
{"type":"function","name":"requests","stateMutability":"view","inputs":[{"name":"id","type":"uint256"}],"outputs":[{"name":"requester","type":"address"},{"name":"revealBlock","type":"uint64"},{"name":"fulfilled","type":"bool"},{"name":"seed","type":"bytes32"}]},
{"type":"function","name":"nextId","stateMutability":"view","inputs":[],"outputs":[{"type":"uint256"}]},
{"type":"function","name":"EXPIRY","stateMutability":"view","inputs":[],"outputs":[{"type":"uint256"}]},
{"type":"event","name":"Requested","inputs":[{"name":"id","type":"uint256","indexed":true},{"name":"requester","type":"address","indexed":true},{"name":"revealBlock","type":"uint64","indexed":false},{"name":"seed","type":"bytes32","indexed":false}]},
{"type":"event","name":"Revealed","inputs":[{"name":"id","type":"uint256","indexed":true},{"name":"requester","type":"address","indexed":true},{"name":"randomness","type":"uint256","indexed":false}]}
]Status · reverts · gas
status enum
| Value | Name | What to do |
|---|---|---|
| 0 | NONE | unknown id — nothing |
| 1 | WAITING | reveal block not passed — keep polling |
| 2 | READY | call reveal(id) |
| 3 | EXPIRED | forfeit / treat as a loss. Never auto re-roll the same stake |
| 4 | FULFILLED | read results(id) |
revert reasons
| String | When |
|---|---|
| no request | unknown id |
| too early | reveal before block.number > revealBlock (also blocks atomic grinding) |
| expired | reveal after the 256-block window |
| block overflow | request if block.number+1 > uint64 max (unreachable in practice) |
gas (local estimate at 25 gwei — on-chain may vary)
| Op | Gas | KUB |
|---|---|---|
| request | ~53,400 | ~0.0013 |
| reveal | ~53,700 | ~0.0013 |
| request + reveal | ~107,000 | ~0.0027 |
Consumer rules
request transaction, before the outcome is knowable.