Quickstart
Inherit EntropyConsumerV21, request with _roll, and implement _onRandomness. The user signs only play(); a keeper (or anyone) settles it and your callback fires.
import "./EntropyConsumerV21.sol"; import "./EntropyLib.sol"; // optional: many values from one reveal contract MyGame is EntropyConsumerV21 { using EntropyLib for uint256; uint32 constant CB_GAS = 150_000; mapping(uint256 => address) public player; constructor(address entropyV2) EntropyConsumerV21(entropyV2) {} // user signs ONCE. Fund this tx from the frontend with entropy.feeAndGas() (over-funds gas = no "fee" revert) function play() external payable { require(msg.value >= entropy.fee(CB_GAS), "fee"); uint256 id = _roll(keccak256(abi.encodePacked(msg.sender, block.number)), CB_GAS); player[id] = msg.sender; } // fires later when revealed (by a keeper / the next user / anyone) function _onRandomness(uint256 id, uint256 rng) internal override { uint256 pocket = rng.pick(0, 37); // 0..36, domain-separated // ... settle / pay out / mint to player[id] ... } }
Addresses & config
| Item | Value |
|---|---|
| Chain | KUB Chain (Bitkub) · chainId 96 (0x60) |
| RPC | https://rpc.bitkubchain.io |
| DurianEntropyV21 | 0xa32236611eC11980B1F15760eB3432f7e25EFdC8 |
| Keeper API | https://keeper-v21.durianfun.xyz/reveal |
| Explorer | https://www.kubscan.com |
| SDK files | durian-entropy.js · EntropyConsumerV21.sol · IEntropyConsumer.sol · EntropyLib.sol · examples: CoinFlip / NFT / Roulette |
| Compiler | solc 0.8.24 · optimizer 200 · evm paris |
V2 is the commercial service (gas-scaled fee, sign-once UX), built on the same blockhash commit–reveal primitive.
Solidity SDK — the base handles the dangerous parts
Inherit EntropyConsumerV21 and you get all of this for free:
EntropyConsumerV21 — never implement IEntropyConsumer raw. Anyone can point requestCallback(seed, <yourConsumer>, cb) at your contract; the base's known[id] + delivered[id] guards reject spoofed / foreign callbacks.Pays entropy.fee(cbGas), requests a callback, and records the id as yours (known[id]) so a spoofed callback for a foreign id can never trigger your _onRandomness.
Implemented for you: requires msg.sender == entropy AND known[id], single-delivery (delivered[id]), then calls your _onRandomness.
If the auto-callback ever failed (out of gas, transient revert), anyone may poke this once the request is FULFILLED to deliver the stored result exactly once.
Settle/capture a previous pending request inside a new action, so users almost never sign a second tx.
You implement exactly one function: _onRandomness(uint256 id, uint256 rng).
Contract functions (DurianEntropyV21)
Pay the fee, get a callback to consumer on reveal. (What _roll calls.)
No callback, no fee — settle it yourself with seal/reveal.
Total fee = bounty + protocol, at the current gas price. Fund a request with the SDK's feeAndGas(), not a bare read (see Fees).
(bountyGas + cbGas) × max(tx.gasprice, minGasPrice) — reimburses whoever reveals (the keeper). Gas pass-through, not revenue.
bounty × protocolBps / 10000 — Durian's protocol revenue, booked to the owner at request time.
Capture blockhash(revealBlock) within the 256-block window so the result can be revealed later with no deadline.
Compute the result + fire the callback. Works anytime once sealed; else within the window.
0 NONE · 1 WAITING · 2 READY · 3 EXPIRED · 4 FULFILLED.
Stored randomness (0 until revealed).
Frontend SDK (JS) — request, then settle with a user fallback
The one rule: never depend solely on the keeper. settle() pings the keeper, watches for the result, and if the keeper is silent it falls back to a user-signed seal+reveal.
import { DurianEntropy } from "./durian-entropy.js"; const entropy = new DurianEntropy({ address: "0xa32236611eC11980B1F15760eB3432f7e25EFdC8", workerUrl: "https://keeper-v21.durianfun.xyz/reveal", provider, // ethers read provider }); // 1) user signs ONE tx. Use feeAndGas() (NOT fee()): it OVER-funds (2× gas) + does NOT pin gasPrice, // so a wallet gas-bump can't revert "fee". The contract refunds the surplus (your consumer refunds // it on the consumer path; reclaim() on the direct path). Never send the exact fee(). const rc = await (await myGame.connect(signer).play({ ...(await entropy.feeAndGas(150000)) })).wait(); const id = entropy.idFromReceipt(rc); // 2) keeper-first, user-fallback — always settles within the window const rng = await entropy.settle(id, { signer, onState: (s) => setStatus(s), // 'settling'→('fallback'→'sealing'→'revealing'→)'done' });
| Method | Notes |
|---|---|
| fee(cbGas) | fee (wei) at the current gas price — display only |
| feeAndGas(cbGas) | fund a request with this → {value}; spread onto your tx ({...await entropy.feeAndGas(cb)}). OVER-funds (2× gas, no pinned gasPrice) so a wallet gas-bump can't revert "fee"; the contract refunds the surplus (reclaim() on the direct path / your consumer refunds it). Never send the exact fee(). |
| withdrawable(addr) · reclaim(signer) | read / withdraw any surplus the contract holds for you |
| idFromReceipt(rc) | parse the Requested event id |
| settle(id, {signer, onState, timeoutMs, pollMs}) | the locked pattern — keeper-first, user-fallback. onState also gets {elapsedMs, remainingMs}. Resolves the RAW rng — with a consumer, read win/lose from YOUR contract's event/state, not this number |
| userSettle(id, signer, onState) | the fallback path alone (seal if needed → reveal) |
| status · results · sealedHash | reads |
e.code: EXPIRED · NO_SIGNER · NOT_READY · USER_REJECTED.Keeper API — POST /reveal
The Durian keeper is a Cloudflare Worker. Your frontend pings it right after the request tx; it settles on-ping as soon as the reveal block is mined (~6–9s). It is gated — it verifies the id on-chain before acting, so it can't be spammed into wasting gas. You normally never call it directly (settle() does).
POST https://keeper-v21.durianfun.xyz/reveal
Content-Type: application/json
{ "id": "<request id as decimal string>" }| Status | Body | Meaning |
|---|---|---|
| 200 | { ok: true, queued } | accepted; settling on-ping / next sweep |
| 200 | { ok: true, note } | already fulfilled |
| 404 | { error } | id doesn't exist on-chain (rejected, no gas spent) |
| 400 / 503 | { error } | malformed / keeper not configured |
Fees & cbGas
fee(cbGas) = bounty + protocol. bounty = (bountyGas + cbGas) × gasprice reimburses whoever reveals (a self-funding keeper market); protocol = bounty × protocolBps/10000 (currently 100% ⇒ fee = 2× bounty) is Durian revenue. Tracked to the live gas price with a floor. Fund a request with entropy.feeAndGas(cbGas) — never send the exact fee(). The fee is computed from tx.gasprice at execution and wallets override the gas price you set (seen live: Rabby bumps 25→30 gwei), so an exact fee reverts "fee". feeAndGas over-funds at 2× gas and does not pin gasPrice; the contract charges only the real fee and refunds the surplus — reclaim() on the direct path, or your consumer refunds the overpay (RouletteHouse does).
| Your callback does | Suggested cbGas |
|---|---|
| record / read a value (roulette pocket) | 80k – 120k |
| coinflip payout via pull ledger | 120k – 180k |
NFT _safeMint + storage | 180k – 250k |
Too low → the on-reveal callback fails (caught), and settle(id) recovers it in one extra tx (no fund loss). Ceiling 8,000,000. The fee funds the keeper + the Durian treasury (the fee recipient is an Ownable2Step owner — the only privileged role; it cannot touch randomness, escrows, or pause).
Seal → claim anytime
The EVM keeps only the last 256 blockhashes, so the entropy must be captured within 256 blocks — a hard limit, not a choice. seal(id) stores blockhash(revealBlock) in one cheap SSTORE inside that window; once sealed, reveal(id) (compute + callback) has no deadline — a winner claims whenever. The keeper's only time-critical job is the trivial, batchable seal.
If nobody seals within 256 blocks the request expires and is forfeit (the requester already escrowed). Extending the window is impossible and would be a re-roll exploit, so it is deliberately not done.
Rules every MONEY consumer MUST follow
_roll. A loser who refuses to settle gains nothing — the stake is already yours; unsettled → expiry forfeits it.committedAtBlock[block.number+1] += stake; require(≤ perBlockCap) and size perBlockCap below KUB block_reward / 10. See examples/CoinFlipHouseV21.sol.results(id). Show a cosmetic spin, ease to the real result on the event..call inside the callback (a reverting receiver would strand your state). Credit winnings[player] + add claim().Lifecycle & errors
request ──(block N+1 mined)──> WAITING ─> READY ──(seal <=256 blk)──> READY (forever) ─> FULFILLED
└─(no seal, 256 blk pass)──> EXPIRED (forfeit)| Revert | When |
|---|---|
| too early | before the reveal block is mined (also blocks atomic grinding) |
| expired | 256-block window passed unsealed |
| no request | unknown id |
| fee | underpaid |
| cbGas too high | > 8,000,000 |
Reference consumers to copy: CoinFlipHouseV21.sol (money game), RandomTierNFTV21.sol, RouletteConsumerV21.sol. Full guide: download the SDK (.md).