Dice Protocol is a commit-reveal randomness oracle consisting of two main components:
- DiceEntropy — An immutable Solidity smart contract deployed on Robinhood Chain
- Tyche — A Rust-based off-chain keeper service that auto-reveals randomness
contracts/src/
├── DiceEntropy.sol # Main oracle contract (entry point)
├── DiceState.sol # Storage layout and state struct
├── DiceStructsV2.sol # Struct definitions (ProviderInfo, Request, etc.)
└── interfaces/
├── IEntropy.sol # Core interface for requesting randomness
└── IEntropyConsumer.sol # Interface consuming contracts implement
The main contract handles:
- Provider registration (admin-only via
registerFor()) - Auto-registration in constructor (for exclusive provider model)
- Request handling through
requestV2(address,bytes32,uint32); three legacy overloads remain in the ABI but revert - Reveal verification (
reveal(),revealWithCallback()) - Fee accounting (single-fee model, 100% to vault)
- Admin functions (
setFee(),withdrawFees(),setDefaultGasLimit())
| Decision | Rationale |
|---|---|
| Immutable (no proxy) | Maximum trust — logic can never change |
| V2 API only | Simplified surface area, no legacy support |
| Single fee model | 0.000025 ETH flat, no protocol/provider split |
| Exclusive provider | Admin controls who can provide randomness |
| 500k hash chain (live v10) | ~5,000 days at 100 requests/day |
| No blockhash in result | Simplifies verification, uses 2-party commit-reveal |
| Auto-register in constructor | Provider active at deployment, no separate tx |
tyche/src/
├── main.rs # CLI entry point (run, setup-provider, generate)
├── command/
│ ├── run.rs # Main keeper loop — watches blocks, reveals
│ └── setup_provider.rs # Provider registration command
├── keeper/
│ └── block.rs # Block processing, event filtering
├── chain/
│ ├── reader.rs # Contract read interface (getProviderInfo, etc.)
│ └── submitter.rs # Transaction submission (revealWithCallback)
├── state.rs # Hash chain state management (PebbleHashChain)
├── config.rs # YAML configuration parser
├── api/ # Optional operational API
│ ├── explorer.rs # Request/reveal explorer endpoints
│ └── revelation.rs # Revelation query endpoints
├── history.rs # Durable request history
└── lib.rs # Shared types
- Startup: Tyche reads private config → connects to RPC → fetches public provider info from the contract → reconstructs the hash chain in memory from the private secret plus public chain context. Onchain metadata is not the raw hash-chain secret.
- Backlog processing: Scans from last-processed block to current block
- Live mode: Polls for new blocks in 100-block batches
- Event handling: For each
Requestedevent, computes the reveal value and submits arevealWithCallbacktransaction - Persistence: Request and reveal progress is stored for recovery and observability
The hash chain uses Keccak256 in an S/KEY-like construction:
seed → Keccak256(seed) = h₁ → Keccak256(h₁) = h₂ → ... → h₅₀₀₀₀₀ = commitment
Reveals happen in reverse: h₅₀₀₀₀₀ first, then h₄₉₉₉₉₉, etc. Each reveal hashes to the previous commitment, proving chain membership.
Tyche stores hash-chain samples/values in memory proportional to configured chain length for O(1) lookup by sequence number.
Admin (Cold) Vault (Cold) Keeper (Hot)
0x4ACD... 0x918E... 0x8741...
│ │ │
├── setFee() │ ├── revealWithCallback()
├── withdrawFees() ────────►│ │ (auto, via Tyche)
├── registerFor() │ │
├── setDefaultProvider() │ │
└── proposeAdmin() │ │
- Admin: Cold wallet, controls all contract parameters
- Vault: Cold wallet, receives all withdrawn fees
- Keeper: Hot wallet, only submits reveals. Funded with minimal ETH for gas. Cannot steal fees or modify contract state.
- Cryptographic: Keccak256 hash chain — provider can't precompute, user can't bias
- Economic: Flat fee covers gas + margin, keeper funded separately
- Operational: monitored restart and role-separated wallets
- Contract-level: Immutable, gas-capped callbacks,
excessivelySafeCallpattern - Infrastructure: Private keys never committed, config gitignored, separate Git identity