# Durian Entropy — Integration Guide

Verifiable, ownerless on-chain randomness, **live on KUB Chain (chainId 96)**. Any contract or app can
call it. There is no SDK package to install for on-chain use: import one interface, point at the
canonical address, and call two functions.

This document is everything you need to integrate against the **deployed** contract, end to end.

---

## 0. TL;DR

```solidity
uint256 id = entropy.request(seed);   // TX 1 — commit (id comes from the Requested event)
// ...wait until you are PAST the reveal block...
uint256 r  = entropy.reveal(id);       // TX 2 — reveal, r is your randomness
```

- `r` is a uniformly distributed `uint256`, fixed by a **future blockhash** that no one could predict
  at commit time.
- **Timing (read this — it is the #1 mistake).** `request` earmarks `revealBlock = requestBlock + 1`,
  and `reveal` requires `block.number > revealBlock`. So reveal succeeds only **at least 2 blocks
  after your request** (≈10s on KUB, not 5s). Do not reveal "the next block". **Poll `status(id)`
  until it returns `2` (READY) and never hand-roll the block math.**
- Settlement is **permissionless** and **idempotent**: anyone can call `reveal`, and a second call on
  a settled id returns the same number and emits no new event.

---

## 1. Addresses

| Item | Value |
|---|---|
| Chain | KUB Chain (Bitkub), `chainId 96` (`0x60`) |
| RPCs (use a fallback set) | `https://rpc.bitkubchain.io` · `https://rpc-l1.bitkubchain.io` · public KUB RPC can rate-limit, so rotate 2–3 |
| Explorer base | `https://www.kubscan.com/address/<addr>` · `/tx/<hash>` · `/block/<n>` |
| **`DurianEntropy`** | **`0xf2DF5c645d84Deb994979d07C0b02410D718754E`** |
| Deploy tx / block | `0x8809701a058b52201454df8741108fecec26755a0ae8f03f6c6ea34d78fb8eaf` · block 32,878,724 |
| Explorer | [**verified source on KUBScan**](https://www.kubscan.com/address/0xf2DF5c645d84Deb994979d07C0b02410D718754E) ✓ — read the exact Solidity + ABI, and call it from the Read/Write Contract tab (verified 2026-06-24) |
| Compiler | `solc 0.8.24`, optimizer 200 runs, `evm: paris` |

> The contract is ownerless and immutable. This address never changes. Use the canonical deployment —
> the core `DurianEntropy` is proprietary/source-available, **do not fork or redeploy** it.

---

## 2. On-chain integration (Solidity)

### 2.1 The interface (MIT — copy freely)

```solidity
// 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);
    // views
    function results(uint256 id) external view returns (uint256 randomness); // 0 until revealed
    function status(uint256 id)  external view returns (uint8); // 0 NONE 1 WAITING 2 READY 3 EXPIRED 4 FULFILLED
    function requests(uint256 id) external view returns (address requester, uint64 revealBlock, bool fulfilled, bytes32 seed);
    function nextId() external view returns (uint256);
    function EXPIRY() external view returns (uint256); // == 256
}
```

> Licensing: the interface, `EntropyLib`, and the example consumers are **MIT** (copy freely). The
> deployed `DurianEntropy` contract is proprietary/source-available — call it, don't redeploy it.

### 2.2 Three consumer rules (get these wrong and you get drained)

1. **Commit funds/intent at `request` time, never at reveal time.** The price of playing is paid
   before the outcome is knowable.
2. **Make settlement permissionless.** Let anyone settle a request inside the 256-block window, so a
   loser cannot dodge a bad outcome by simply not revealing.
3. **On expiry the requester LOSES (forfeit). Never refund-and-re-roll, never re-roll the same stake.**
   A *new, independently-funded* request is fine; re-rolling an expired stake re-opens the
   abort-on-loss hole.

### 2.3 Reference consumer: a fair coin flip

Demo only (fixed 2x, one open bet per player). A production house **must** also (a) keep
`bankroll >= max payout` or a winner's `settle` reverts until topped up, and (b) bounds-check the
`uint64` reveal-block cast.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import "./IDurianEntropy.sol";

contract CoinFlip {
    IDurianEntropy public immutable entropy;
    uint256 public constant MAX_BET = 1 ether;
    struct Bet { address player; uint96 amount; bool headsGuess; uint64 revealBlock; uint256 reqId; bool open; }
    mapping(address => Bet) public openBet;

    constructor(IDurianEntropy e) payable { entropy = e; } // fund the house on deploy

    function bet(bool headsGuess) external payable {
        require(msg.value > 0 && msg.value <= MAX_BET, "bad bet");
        require(!openBet[msg.sender].open, "bet open");
        bytes32 seed = keccak256(abi.encodePacked(msg.sender, msg.value, headsGuess));
        uint256 id = entropy.request(seed);                                  // rule 1: commit now
        openBet[msg.sender] = Bet(msg.sender, uint96(msg.value), headsGuess, uint64(block.number + 1), id, true);
    }

    // rule 2: permissionless. payout goes to the player regardless of who calls.
    function settle(address player) external {
        Bet memory b = openBet[player];
        require(b.open, "no bet");
        uint256 r = entropy.reveal(b.reqId);                                 // reverts "too early" same/next block
        delete openBet[player];
        if (((r & 1) == 0) == b.headsGuess) {                                // heads == even
            (bool ok, ) = b.player.call{value: uint256(b.amount) * 2}("");
            require(ok, "payout failed");
        }
    }

    // rule 3: if nobody settled in time, the stake is forfeit. no refund, no re-roll.
    // Authoritative expiry signal is status(id)==3 (EXPIRED) / reveal() reverting "expired".
    function forfeitExpired(address player) external {
        Bet memory b = openBet[player];
        require(b.open && entropy.status(b.reqId) == 3, "not expired");
        delete openBet[player];
    }
}
```

### 2.4 Reference consumer: grind-proof random NFT tier

```solidity
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);                 // reverts "too early"/"expired"
    delete pending[user];
    _mint(user, pickTier(r));                            // user can't dodge a bad tier; NO re-roll
}
// REQUIRED: clear an expired pending mint, or it is stuck forever (reveal would revert "expired").
function expireMint(address user) external {            // rule 3: forfeit PRICE, no NFT, no refund
    Pending memory p = pending[user];
    require(p.open && entropy.status(p.reqId) == 3, "not expired");
    delete pending[user];
}
```

### 2.5 Many values from one reveal — `EntropyLib` (MIT)

One reveal gives a full 256-bit number. Derive as many independent, domain-separated values as you
need. Do **not** make multiple requests for one event; that is more gas and more grind surface.

`EntropyLib` is an `internal pure` library you **inline into your own contract** — it is not deployed
at the entropy address and makes no external call.

```solidity
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 the pocket)
uint256 lotto         = r.digits(6);        // 6 packed digits, e.g. 482917 (m <= 77)
```

```solidity
library EntropyLib {
    function expand(uint256 r, uint256 n) internal pure returns (uint256[] memory); // n <= 256
    function pick(uint256 r, uint256 salt, uint256 max) internal pure returns (uint256); // [0,max), max != 0
    function digits(uint256 r, uint256 m) internal pure returns (uint256 number);   // m <= 77
}
```

> `digits` returns a numeric value, so leading zeros are lost (`004821` reads back as `4821`) — pad to
> `m` digits when displaying. `pick` is effectively unbiased for `max << 2^256` (roulette, dice); for
> very large ranges use rejection sampling.

---

## 3. Frontend integration (JS / TS)

### 3.1 ABI (the minimum you need)

```json
[
  { "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": "randomness", "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": "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 } ] }
]
```

### 3.2 ethers v6 — full flow

```js
import { Contract, BrowserProvider } from "ethers";

const ENTROPY = "0xf2DF5c645d84Deb994979d07C0b02410D718754E"; // canonical address (KUB)
const provider = new BrowserProvider(window.ethereum);
const signer   = await provider.getSigner();
const entropy  = new Contract(ENTROPY, ABI, signer);
const sleep    = (ms) => new Promise((r) => setTimeout(r, ms));

// 1) commit — capture id, requester, seed (needed later to verify)
const seed = "0x" + "00".repeat(32);                  // or any bytes32 you commit to
const requester = await signer.getAddress();
const rcReq = await (await entropy.request(seed)).wait();
const id = rcReq.logs.map((l) => { try { return entropy.interface.parseLog(l); } catch { return null; } })
                     .find((p) => p && p.name === "Requested").args.id;

// 2) poll status until READY (2). Bail on EXPIRED (3) — never loop forever.
let st = Number(await entropy.status(id));            // 1 WAITING -> 2 READY (or 3 EXPIRED)
while (st === 1) { await sleep(2500); st = Number(await entropy.status(id)); }
if (st === 3) throw new Error("request expired (256-block window passed) — treat as a loss");

// 3) reveal, then read the result
await (await entropy.reveal(id)).wait();
const r = await entropy.results(id);                  // uint256 (0 only if not FULFILLED)
const fourDigits = (r % 10000n).toString().padStart(4, "0");
```

### 3.3 viem — same flow

```js
import { createPublicClient, createWalletClient, custom, http, parseEventLogs } from "viem";

const ENTROPY = "0xf2DF5c645d84Deb994979d07C0b02410D718754E";
const 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"] } } };
const pub    = createPublicClient({ chain, transport: http() });
const wallet = createWalletClient({ chain, transport: custom(window.ethereum) });
const [account] = await wallet.getAddresses();

const hash = await wallet.writeContract({ account, address: ENTROPY, abi: ABI, functionName: "request", args: [seed] });
const rc   = await pub.waitForTransactionReceipt({ hash });
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] });
```

### 3.4 Verify any result off-chain (provably fair)

Anyone can recompute a settled result from public data. Every input comes from on-chain: read
`requester`, `revealBlock`, `seed` from `requests(id)` (or from the `Requested` event).

```js
import { keccak256, encodePacked } from "viem";

// inputs straight from the chain
const req   = await pub.readContract({ address: ENTROPY, abi: ABI, functionName: "requests", args: [id] });
// req => { requester, revealBlock, fulfilled, seed }
const block = await pub.getBlock({ blockNumber: BigInt(req.revealBlock) });

// contract computes keccak256(abi.encodePacked(blockhash(revealBlock), id, requester, seed))
const r = BigInt(keccak256(encodePacked(
  ["bytes32", "uint256", "address", "bytes32"],
  [block.hash, BigInt(id), req.requester, req.seed]
)));

// r === await pub.readContract({ ..., functionName: "results", args: [id] })
```

---

## 4. Reference

### 4.1 Status enum

| value | name | meaning | what to do |
|---|---|---|---|
| 0 | NONE | id never requested | nothing |
| 1 | WAITING | reveal block not mined / not yet passed | wait, keep polling |
| 2 | READY | can reveal now | call `reveal(id)` |
| 3 | EXPIRED | past the 256-block window, blockhash gone | **forfeit / treat as a loss.** Do NOT auto re-roll the same stake. A brand-new, independently-funded request is fine |
| 4 | FULFILLED | already revealed | read `results(id)` |

> A request can go WAITING → EXPIRED **without ever showing READY** if nobody settles within ~256
> blocks (~21 min on KUB). Always make your reveal path permissionless, and break polling loops on
> `status >= 2 || status == 3`.

### 4.2 Revert reasons & idempotency

| string | when |
|---|---|
| `no request` | unknown id |
| `too early` | reveal before `block.number > revealBlock` (also what blocks atomic grinding) |
| `expired` | reveal after the 256-block window closed |
| `block overflow` | `request` if `block.number + 1` exceeds uint64 max (unreachable in practice) |

`reveal` is **idempotent**: a second call on a FULFILLED id returns the same number and emits **no new
`Revealed` event** (only `results`/`status` reflect it). Don't wrap it in a try/catch expecting a
revert on double-settle.

### 4.3 Gas (local estimate at an assumed 25 gwei — on-chain may vary)

| op | gas | KUB |
|---|---|---|
| `request` (steady) | ~53,400 | ~0.0013 |
| `reveal` | ~53,700 | ~0.0013 |
| request + reveal | ~107,000 | ~0.0027 |

The caller pays gas. The beacon holds no funds and charges no fee.

---

## 5. Security model (read before using with value)

- **Atomic grinding is impossible.** Request and reveal cannot share a transaction.
- **Single-block-producer trust (irreducible).** The producer of the reveal block can bias its
  blockhash. Combining multiple blocks does **not** fix this. There is no on-chain way to remove it
  without a real VRF, which KUB does not have.
- **Mitigation is economic: cap value.** Size per-round caps so manipulation is never worth its cost.
- **Aggregation.** Every request in the same block shares one blockhash, so a producer who grinds that
  block biases every game settling on it at once. Cap against the **aggregate** value resting on a
  single block, not per-round.
- **Not for high-stakes.** Ideal for low-stakes games and randomized mints with value caps.

This is **blockhash commit-reveal randomness, not a VRF.** Provably fair = recomputable (§3.4), not
cryptographically unbiasable.

---

## 6. Integration checklist

- [ ] Point at the canonical `DurianEntropy` at `0xf2DF5c645d84Deb994979d07C0b02410D718754E` on chainId 96.
- [ ] Commit funds/intent in the `request` transaction; capture `id` from the `Requested` event.
- [ ] Poll `status(id)` to `2` (READY) before revealing; break on `3` (EXPIRED). Never reveal one block after request.
- [ ] Expose a **permissionless** settle path inside the 256-block window.
- [ ] Treat expiry as a forfeit; add a path to clear expired pending state. No refund-and-re-roll.
- [ ] Cap per-round and aggregate value to your bankroll.
- [ ] Derive multiple outputs with `EntropyLib` (inlined), not multiple requests.
- [ ] Let users recompute results off-chain (§3.4) and link the reveal block: `https://www.kubscan.com/block/<revealBlock>`.
