Verifiable commit-reveal randomness infrastructure for Robinhood Chain. Designed so neither requester nor provider can unilaterally choose the output of a completed reveal.
- Overview
- Features
- How It Works
- Architecture
- Quickstart
- Contract Addresses
- Network Configuration
- API Reference
- Tyche Keeper
- Security
- Documentation
- Contributing
- License
Dice Protocol is a commit-reveal randomness oracle deployed on Robinhood Chain (chain ID 4663), an Arbitrum Nitro-based Layer 2. It delivers verifiable onchain random numbers to any smart contract via a hash-chain commitment scheme.
The protocol combines user-contributed randomness with provider-revealed values using Keccak256, producing results that are verifiable onchain. Neither party can unilaterally choose the output of a completed reveal; a provider can still withhold service, after which eligible requests become refundable.
Dice Protocol is a live RNG oracle on Robinhood Chain with a flat per-request fee of 0.000025 ETH. The onchain contract is immutable (no proxy, no upgrade path). The Tyche keeper, a Rust auto-reveal service, submits reveals automatically.
- Commit-reveal RNG — Hash-chain commitment scheme with Keccak256. Provider pre-commits to a hash chain (live v10 currently registered with 500,000 values; longer chains supported); each request reveals the next.
- Two-party contribution — Neither requester nor provider can unilaterally choose the output of a completed reveal.
- Verifiable onchain - Every reveal is independently checkable via
keccak256(reveal) == previousCommitment. - Fast reveal - Built for sub-second keeper fulfillment. Typical observed request-to-reveal is about 1-3 seconds; not guaranteed.
- Cheap, predictable fee - Exact 0.000025 ETH per request. No hidden costs, no gas subsidies, no protocol fee splits.
- Agent-friendly - x402
$0.05USDG,@diceprotocol/sdk, SKILL.md. - Immutable contract — No proxy, no governance, no upgrade mechanism. Logic is permanent once deployed.
- Auto-reveal — The Tyche keeper service monitors for
Requestedevents and submits reveals automatically. - Callback delivery — Random numbers are delivered directly to consumer contracts via
entropyCallback()in the same reveal transaction. - TypeScript SDK — Full-featured SDK for off-chain integration, event listening, and utility functions.
- Review status — Internal and automated review completed. No independent third-party v10 audit report has been published. See docs/security-audit.md.
Consumer Contract DiceEntropy (on-chain) Tyche Keeper (off-chain)
│ │ │
│── requestV2(provider, │ │
│ userRandom, gasLimit) ──→ │ │
│ {value: 0.000025 ETH} │ │
│ │── emit Requested(...) ───────→ │
│ │ │
│ │ Tyche detects │
│ │ event, computes
│ │ next hash-chain
│ │ reveal value │
│ │ │
│ │←── revealWithCallback( │
│ │ seq, userRandom, │
│ │ providerReveal) ──────────│
│ │ │
│ Contract verifies: │ │
│ keccak256(providerReveal) │ │
│ == currentCommitment │ │
│ │ │
│ randomNumber = │ │
│ keccak256(userRandom, │ │
│ providerReveal)│ │
│ │ │
│←── entropyCallback( │ │
│ seq, provider, │ │
│ randomNumber) ──────────│ │
│ │ │
Security model: The provider cannot predict the user's random value at request time. The user cannot choose the provider contribution, which is locked in the hash chain. For a completed reveal, neither party unilaterally chooses the output. The provider can still withhold a reveal; eligible requests become refundable after the configured delay.
dice-protocol/
├── contracts/ # Solidity smart contracts (Foundry)
│ ├── src/
│ │ ├── DiceEntropy.sol # Core RNG contract (commit-reveal + hash chain)
│ │ ├── DiceState.sol # Storage layout
│ │ ├── TestConsumer.sol # Reference consumer implementation
│ │ └── sdk/ # Interfaces & libraries
│ │ ├── IEntropy.sol # Core entropy interface
│ │ ├── IEntropyV2.sol # V2 request/reveal interface
│ │ ├── IEntropyConsumer.sol # Consumer base contract
│ │ ├── DiceStructsV2.sol # Struct definitions
│ │ ├── DiceErrors.sol # Custom error definitions
│ │ ├── DiceEventsV2.sol # Event definitions
│ │ ├── DiceStatusConstants.sol
│ │ └── PRNG.sol # PRNG utility
│ ├── script/
│ │ └── DeployDiceV6.s.sol # Deployment script
│ ├── test/ # Foundry test suite
│ └── foundry.toml
│
├── tyche/ # Rust auto-reveal keeper service
│ ├── src/
│ │ ├── api/ # Optional operational API
│ │ ├── chain/ # Blockchain reader/adapter
│ │ ├── keeper/ # Reveal loop & tx submission
│ │ └── command/ # CLI verbs (run, setup-provider)
│ ├── config/ # Configuration files
│ ├── migrations/ # SQLite schema migrations
│ └── Dockerfile
│
├── sdk/ # TypeScript SDK
│ ├── src/
│ │ ├── index.ts # DiceProtocol class + exports
│ │ ├── abi.json # Contract ABI
│ │ └── test.ts # SDK tests
│ ├── dist/ # Compiled output
│ └── package.json
│
├── docs/ # Full documentation
│ ├── whitepaper.md # Protocol specification
│ ├── ARCHITECTURE.md # System design deep-dive
│ ├── INTEGRATION.md # Developer integration guide
│ ├── DEPLOYMENT.md # Deployment pipeline
│ ├── developer-docs.md # API reference & quickstart
│ ├── mainnet-deployment.md # Mainnet deployment record
│ ├── testnet-deployment.md # Testnet deployment record
│ ├── security-audit.md # Internal security review notes
│ ├── project-status.md # Current project status
│ └── ROADMAP.md # Development roadmap
│
├── data/ # Tyche SQLite state (gitignored)
├── keeper/ # Keeper wallet management scripts
├── monitor/ # Uptime & health dashboards
└── README.md
┌─────────────────────────────────────────────────────────────┐
│ Robinhood Chain L2 (4663) │
│ │
│ ┌──────────────────┐ requestV2() ┌─────────────────┐ │
│ │ Consumer dApp │──────────────────→│ │ │
│ │ (Game / NFT / │←─────────────────│ DiceEntropy │ │
│ │ Lottery / ...) │ entropyCallback │ Contract │ │
│ └──────────────────┘ │ │ │
│ │ · Hash chain │ │
│ │ verification │ │
│ │ · Fee accounting│ │
│ │ · Callback │ │
│ │ dispatch │ │
│ └────────┬────────┘ │
│ │ │
└──────────────────────────────────────────────────┼────────────┘
│
Requested events│
revealWithCallback txs
│
┌──────────────────────────────────────────────────┼────────────┐
│ Off-chain keeper │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Tyche Keeper (Rust) │ │
│ │ │ │
│ │ · Monitors Requested events via JSON-RPC polling │ │
│ │ · Computes reveal values from precomputed hash chain │ │
│ │ · Submits revealWithCallback transactions │ │
│ │ · State persisted in SQLite │ │
│ │ · REST API on :34000 for monitoring │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
- Foundry (
forge,cast) — for Solidity development - Node.js 18+ — for the TypeScript SDK
- Rust — for the Tyche keeper (operators only)
- ETH on Robinhood Chain for gas + request fees
Consumer contracts inherit IEntropyConsumer and implement entropyCallback():
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;
import { IEntropyConsumer } from "@diceprotocol/sdk/solidity/IEntropyConsumer.sol";
import { IEntropy } from "@diceprotocol/sdk/solidity/IEntropy.sol";
contract MyGame is IEntropyConsumer {
IEntropy public immutable dice;
address public immutable provider;
mapping(uint64 => address) public pendingPlayers;
// DiceEntropy mainnet address
constructor(address _dice, address _provider) {
dice = IEntropy(_dice);
provider = _provider;
}
/// @notice Request a dice roll. Caller pays the protocol fee.
function rollDice() external payable {
// Generate user's random contribution
bytes32 userRandom = keccak256(abi.encodePacked(msg.sender, block.timestamp, block.prevrandao));
// Request randomness — fee is 0.000025 ETH
uint64 seq = dice.requestV2{value: msg.value}(provider, userRandom, 200_000);
pendingPlayers[seq] = msg.sender;
}
/// @notice Called by DiceEntropy with the verified random number
function entropyCallback(
uint64 sequence,
address,
bytes32 randomNumber
) internal override {
address player = pendingPlayers[sequence];
uint256 result = uint256(randomNumber) % 6 + 1; // 1–6
// ... game logic here
delete pendingPlayers[sequence];
}
function getEntropy() internal view override returns (address) {
return address(dice);
}
}Deploy with Forge:
# Deploy to Robinhood Chain mainnet
forge create MyGame \
--rpc-url https://rpc.mainnet.chain.robinhood.com \
--private-key $DEPLOYER_KEY \
--constructor-args 0xd8a0680e7699526b57140ed4eafdcc7219dc0a0c 0x8741b8a825644D9Ef18Faf2DAB5e9b47B900F2b6From a script or dApp frontend, call rollDice() with the fee:
# Using cast
cast send $CONTRACT_ADDRESS "rollDice()" \
--rpc-url https://rpc.mainnet.chain.robinhood.com \
--private-key $USER_KEY \
--value 0.000025etherThe Tyche keeper detects the Requested event, computes the reveal, and submits revealWithCallback(). The callback typically arrives within ~1–3 seconds, but integrations must treat fulfillment as asynchronous.
Install the SDK:
npm install @diceprotocol/sdk
# or
yarn add @diceprotocol/sdkRequest randomness and listen for reveals:
import { DiceProtocol, ethers } from '@diceprotocol/sdk';
// Initialize
const dice = new DiceProtocol({
rpcUrl: 'https://rpc.mainnet.chain.robinhood.com',
contractAddress: '0xd8a0680e7699526b57140ed4eafdcc7219dc0a0c',
});
// Load your wallet (never hardcode keys in production)
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!);
// Get the default provider address
const provider = await dice.getDefaultProvider();
console.log('Provider:', provider);
// Check the fee
const fee = await dice.getFee(provider);
console.log('Fee:', ethers.formatEther(fee), 'ETH');
// Generate a random user contribution
const userRandom = DiceProtocol.generateUserRandom();
console.log('User random:', userRandom);
// Request randomness
const seq = await dice.requestRandom(wallet, provider, userRandom);
console.log('Request sequence:', seq);
// Listen for the reveal (random number delivered)
dice.onReveal((event) => {
if (event.sequenceNumber === seq) {
console.log('Random number:', event.randomNumber);
console.log('Callback failed:', event.callbackFailed);
dice.removeAllListeners();
}
});
// You can also query request status at any time
const request = await dice.getRequest(provider, seq);
console.log('Request status:', request);| Component | Address |
|---|---|
| DiceEntropy | 0xd8a0680e7699526b57140ed4eafdcc7219dc0a0c |
| Keeper (Tyche) | 0x8741b8a825644D9Ef18Faf2DAB5e9b47B900F2b6 |
| Admin | 0x4ACD2C88a239a924E47Fc4995114ca1Bb0CA3CaD |
| Vault | 0x918EAF0b2589710B0D85ef48C12a343E68263841 |
| Component | Address |
|---|---|
| DiceEntropy | 0x43c8A7B1a85384cabf3D3Fd45a15C01F5b51A42D |
Note: The current testnet fee is exactly
0.000025 ETH; its registered end sequence is10000. See docs/testnet-deployment.md.
| Parameter | Value |
|---|---|
| Chain ID | 4663 |
| Chain Name | Robinhood Chain |
| RPC URL | https://rpc.mainnet.chain.robinhood.com |
| Block Explorer | https://robinhoodchain.blockscout.com |
| L2 Type | Arbitrum Nitro |
| Request Fee | 0.000025 ETH (25,000,000,000,000 wei) |
| Hash Chain Length | 500,000 values registered on live v10 (end sequence 500003); longer chains can be registered later via registerFor |
| Hash Algorithm | Keccak256 |
| Example callback gas | 200,000 |
| Max Gas Limit | 655,350,000 (uint16.max × 10,000) |
The DiceEntropy contract exposes a V2 API. All randomness requests go through requestV2() and reveals through revealWithCallback().
| Function | Description |
|---|---|
requestV2() |
Request randomness from the default provider with auto-generated user random and default gas limit. Payable. |
requestV2(uint32 gasLimit) |
Request with a specified callback gas limit. |
requestV2(address provider, uint32 gasLimit) |
Request from a specific provider. |
requestV2(address provider, bytes32 userRandomNumber, uint32 gasLimit) |
Full control — provider, user random, and gas limit. Recommended. |
revealWithCallback(address provider, uint64 seq, bytes32 userContribution, bytes32 providerContribution) |
Called by the keeper to reveal the provider's value and trigger the consumer's callback. |
reveal(address provider, uint64 seq, bytes32 userContribution, bytes32 providerContribution) |
Reveal without callback (requester calls manually). |
| Function | Description |
|---|---|
registerFor(address provider, uint128 fee, bytes32 commitment, bytes metadata, uint64 chainLength, bytes uri) |
Register a new provider or refresh a hash chain. Admin only. |
withdrawFees(uint128 amount) |
Withdraw accrued fees to the vault address. Admin only. |
advanceProviderCommitment(address provider, uint64 seq, bytes32 revelation) |
Advance the commitment pointer (skip leaked sequences). |
| Function | Returns | Description |
|---|---|---|
getDefaultProvider() |
address |
The default provider address |
getFee(address provider) |
uint128 |
Fee in wei for a request |
getFeeV2(address provider, uint32 gasLimit) |
uint128 |
Fee for a specific gas limit |
getProviderInfoV2(address provider) |
ProviderInfo |
Full provider state (commitment, chain, fees, etc.) |
getRequestV2(address provider, uint64 seq) |
Request |
Request details by sequence number |
| Event | Description |
|---|---|
Requested(provider, caller, sequenceNumber, userContribution, gasLimit, ...) |
Emitted when a randomness request is made |
Revealed(provider, caller, sequenceNumber, randomNumber, userContribution, providerContribution, callbackFailed, ...) |
Emitted when a reveal completes |
Registered(provider, ...) |
Emitted when a provider is registered or refreshed |
Consumer contracts must inherit IEntropyConsumer and implement:
function entropyCallback(uint64 sequence, address provider, bytes32 randomNumber) internal;
function getEntropy() internal view returns (address);The _entropyCallback() external function is called by DiceEntropy — it enforces msg.sender == getEntropy() so only the DiceEntropy contract can trigger the callback.
The @diceprotocol/sdk package provides a DiceProtocol class for off-chain interaction.
const dice = new DiceProtocol({
rpcUrl: 'https://rpc.mainnet.chain.robinhood.com',
contractAddress: '0xd8a0680e7699526b57140ed4eafdcc7219dc0a0c',
});| Method | Returns | Description |
|---|---|---|
getDefaultProvider() |
Promise<string> |
Default provider address |
getFee(provider?, gasLimit?) |
Promise<bigint> |
Request fee in wei |
getProviderInfo(provider) |
Promise<ProviderInfo> |
Full provider state |
getRequest(provider, seq) |
Promise<RequestInfo> |
Request details by sequence number |
getAccruedTreasuryFees() |
Promise<bigint> |
Total accrued fees |
getProtocolFee() |
Promise<bigint> |
Current protocol fee per request |
| Method | Returns | Description |
|---|---|---|
requestRandom(signer, provider?, userRandom, gasLimit?) |
Promise<bigint> |
Submit a randomness request, returns sequence number |
revealWithCallback(signer, seq, userRandom, reveal) |
Promise<string> |
Reveal (provider/keeper only) |
registerProvider(signer, fee, commitment, chainLen, uri?) |
Promise<string> |
Register as a provider |
withdrawFees(signer, amount) |
Promise<string> |
Withdraw accrued provider fees |
| Method | Description |
|---|---|
onRequest(callback) |
Listen for Requested events |
onReveal(callback) |
Listen for Revealed events (random numbers delivered) |
removeAllListeners() |
Stop all event listeners |
| Method | Returns | Description |
|---|---|---|
DiceProtocol.generateUserRandom() |
string |
Generate a 32-byte random hex value |
DiceProtocol.computeUserCommitment(random) |
string |
Hash a user random into a commitment |
DiceProtocol.constructProviderCommitment(numHashes, revelation) |
string |
Construct a commitment from a revelation |
DiceProtocol.generateHashChain(seed, length) |
{ commitment, revelations[] } |
Generate a full hash chain from a seed |
DiceProtocol.combineRandom(user, provider, blockHash?) |
string |
Combine random values (matches on-chain computation) |
Tyche is the Rust-based auto-reveal service that powers Dice Protocol. It runs as a systemd service and handles the full reveal lifecycle:
- Initialization — Reads public provider info from the DiceEntropy contract and reconstructs the hash chain in memory from private provider configuration plus public chain context. Onchain metadata is not the raw hash-chain secret.
- Block watching — Polls for new blocks, filtering for
Requestedevents. - Reveal computation — Indexes into the precomputed hash chain at the correct sequence number.
- Transaction submission — Sends
revealWithCallbacktransactions from the keeper wallet. - State persistence — Records all processed requests in SQLite for crash recovery.
# Build
cd tyche/
cargo build --release
# Register the provider (first-time setup)
cargo run -- setup-provider --config config/dice-config.yaml
# Start the keeper
RUST_LOG=INFO cargo run -- run --config config/dice-config.yamlTyche may expose an operational monitoring API in private deployments. Integrators should use the public contract, events, SDK, proof pages, and status surface rather than relying on an internal service endpoint:
GET /v1/chains/{chain_id}/revelations/{sequence} # Get reveal value for a sequence
GET /v1/chains/{chain_id}/requests # List pending requests
Dice Protocol enforces a three-wallet security model:
| Role | Type | Purpose |
|---|---|---|
| Admin | Cold | Contract admin — fee changes, withdrawals, provider management |
| Vault | Cold | Fee recipient — receive-only |
| Keeper | Hot | Submits reveal transactions, funded with gas ETH only |
The keeper wallet cannot withdraw fees or modify contract parameters. If compromised, the attacker can only reveal randomness early or fail to reveal — they cannot steal funds.
The DiceEntropy contract has been audited with Slither 0.11.5 and a manual review. Zero critical or high-severity vulnerabilities.
| Severity | Count | Status |
|---|---|---|
| Critical | 0 | — |
| High | 0 | — |
| Medium | 0 | — |
| Low | 2 | Mitigated (gas griefing via defaultGasLimit; block.timestamp PRNG acceptable) |
| Info | 3 | Acknowledged |
Review notes: docs/security-audit.md
| Property | Guarantee |
|---|---|
| Unpredictability | Provider cannot predict user's random value at commitment time |
| Non-biasability | User cannot influence provider's contribution |
| Verifiability | Each reveal is verifiable on-chain via Keccak256 |
| Tamper resistance | Immutable contract — no proxy, no upgrade path |
| Gas bounded | Consumers pass an explicit callback gas limit (example: 200,000) |
| Reentrancy protection | ExcessivelySafeCall pattern for all untrusted callbacks |
| Chain exhaustion | OutOfRandomness revert when hash chain depleted |
| Document | Description |
|---|---|
| docs/whitepaper.md | Full protocol specification — cryptographic design, economic model, security analysis |
| docs/ARCHITECTURE.md | System architecture and component design |
| docs/INTEGRATION.md | Developer integration guide with patterns (coin flip, NFT mint, batch) |
| docs/developer-docs.md | Quickstart, API reference, and Solidity integration paths |
| docs/DEPLOYMENT.md | Deployment pipeline and infrastructure requirements |
| docs/mainnet-deployment.md | Mainnet deployment record |
| docs/testnet-deployment.md | Testnet deployment record |
| docs/security-audit.md | Internal security review notes |
| docs/project-status.md | Current project status |
| docs/ROADMAP.md | Development roadmap |
This project uses conventional commits:
feat: add batch request support
fix: correct gas limit rounding in requestHelper
docs: update SDK API reference
chore: bump SDK version to 0.2.0
security: scrub secrets from config template
| Type | Use |
|---|---|
feat |
New feature |
fix |
Bug fix |
docs |
Documentation only |
chore |
Maintenance, dependencies, build |
security |
Security-related changes |
refactor |
Code restructuring (no behavior change) |
test |
Adding or fixing tests |
# Contracts
cd contracts/
forge build
forge test -vvv
# SDK
cd sdk/
npm install
npm run build
npm test
# Tyche keeper
cd tyche/
cargo build --workspace
cargo test
cargo clippy --all-targets --all-features -D warnings
cargo fmt --all- Never commit secrets, private keys, or seeds. Use environment variables and
config.sample.yamltemplates. - No third-party oracle dependencies. Dice Protocol is a independent Robinhood Chain implementation adapted from proven commit-reveal oracle patterns.
- Run linters before pushing —
forge fmt,cargo fmt --all,cargo clippy. - Bump versions in
package.json/Cargo.tomlfor releases.
Apache-2.0 — Dice Protocol is open-source software. Portions of the smart contract architecture and interfaces are adapted from Pyth Entropy / pyth-crosschain under Apache-2.0; see NOTICE.
If a request is not revealed within about 60–90 seconds (refundDelayBlocks = 6 L1 blocks on Robinhood/Arbitrum Nitro), the original requester can call refundRequest(provider, sequenceNumber) and reclaim the exact fee paid.