Architecture Deep Dive
Understand how the smart contract, backend, and frontend work together to create a trustless, provably fair dice game.
System Overview
QTUM Dice is a three-tier application: a Solidity smart contract for on-chain logic, a Node.js backend for seed management and blockchain interaction, and a React frontend for the user interface. Here's how they connect:
The Commit-Reveal Scheme
The core of the provably fair system is the commit-reveal pattern. This cryptographic protocol ensures that neither the house nor the player can influence the outcome after committing to their random seed.
Why Not Just Use a Random Number?
Blockchains are deterministic — there's no native source of randomness that can't be predicted or manipulated by miners. Common approaches and their problems:
- Block hash: Miners can choose not to publish blocks with unfavorable hashes
- Timestamp: Miners have some flexibility in setting block timestamps
- Single-party random: The generator can cheat by choosing seeds that produce favorable outcomes
The commit-reveal scheme solves this by having both parties contribute to the randomness, with commitments locked in before either seed is revealed.
Detailed Flow
House Generates Seed
The backend generates a cryptographically random 32-byte seed using Node.js's
crypto.randomBytes(32). It stores the seed locally and computes
keccak256(houseSeed) to create the hash commitment. Only the hash is
sent to the frontend.
export function generateSeed(): string {
const randomBytes = crypto.randomBytes(32);
return '0x' + randomBytes.toString('hex');
}
export function hashSeed(seed: string): string {
return keccak256(seed);
}
Player Generates Seed & Places Bet
The frontend generates its own random seed using the browser's
crypto.getRandomValues(). It hashes the seed with keccak256, then calls
the smart contract's placeBet() function with:
playerSeedHash— hash of the player's secret seedtarget— the target number (2-98)isOver— betting over or underhouseSeedHash— the hash received from the backend- QTUM value — the bet amount attached to the transaction
export function generateSeed(): string {
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
return '0x' + Array.from(randomBytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
On-Chain Confirmation & Event Detection
Once the transaction is mined, the contract emits a BetPlaced event.
The backend's ContractMonitor polls the blockchain every 5 seconds using
searchlogs RPC. When it detects the event, it registers the bet and
associates it with the stored house seed.
Unlike Ethereum's eth_getLogs, QTUM's searchlogs returns
nested log arrays that must be flattened. It also ignores the topic filter
parameter, so you must filter events client-side by checking topics[0].
Player Reveals Seed
The frontend sends the player's original (unhashed) seed to the backend via
POST /api/game/submit-player-seed. At this point, the backend has both seeds.
House Reveals & Contract Resolves
The backend calls revealAndResolve(betId, houseSeed, playerSeed) on the contract.
The contract verifies both seeds match their committed hashes, then computes:
bytes32 combined = keccak256(
abi.encodePacked(houseSeed, playerSeed, betId)
);
uint8 roll = uint8(uint256(combined) % 100) + 1;
Automatic Payout
If the player wins, the contract calculates the payout using the multiplier formula
and transfers QTUM directly to the player's address. A BetResolved event
is emitted with the outcome.
Backend Architecture
The backend is a Node.js/Express application that serves three primary functions: managing house seeds, monitoring blockchain events, and coordinating the reveal process.
Core Components
DiceGameManager (game.ts)
The central coordinator that manages the game's state:
- House seed generation: Creates random seeds, stores them indexed by hash, auto-expires after 2 hours
- Bet registration: Links on-chain bets with stored house seeds when
BetPlacedevents arrive - Player seed storage: Accepts revealed player seeds and triggers the resolution process
- Reveal coordination: Calls
revealAndResolve()on the contract when both seeds are available - Recovery: On startup, retries any pending reveals that may have failed
ContractMonitor (contractMonitor.ts)
Polls the QTUM blockchain every 5 seconds for contract events:
- Uses
searchlogsRPC to findBetPlacedandBetResolvedevents - Tracks the last processed block to avoid re-processing
- Flattens nested log arrays (QTUM-specific behavior)
- Filters events by topic hash locally (since QTUM ignores the topic parameter)
- Triggers
registerBetPlaced()andrecordBetResolved()on the game manager
QtumClient (qtum.ts)
A wrapper around QTUM's JSON-RPC interface:
| Method | RPC Call | Purpose |
|---|---|---|
callContract() | callcontract | Read-only contract calls (view functions) |
sendToContract() | sendtocontract | Execute state-changing contract functions |
searchlogs() | searchlogs | Find contract events in block range |
getBlockchainInfo() | getblockchaininfo | Current block height and chain status |
Persistence (persistence.ts)
Simple file-based storage that saves state to JSON files in backend/data/:
house-seeds.json— Active house seeds indexed by hashpending-reveals.json— Bets awaiting resolutionresolved-bets.json— History of resolved bets
For a demo/learning project, file-based persistence keeps things simple with no database setup required. For production, you'd want PostgreSQL or similar for concurrency safety and query capabilities.
Frontend Architecture
The frontend is a React SPA built with Vite and TypeScript. It handles wallet connection, bet placement, and result display.
Key Hooks
useQtumWallet
Manages the MetaMask + qtum-wallet connection. Provides:
connect()— Installs snap if needed, gets address and balancesendContractTransaction()— Sends encoded contract calls via the snaprefreshBalance()— Updates the displayed wallet balance- State:
isConnected,address(base58),addressHex,balance
useContract
Handles ABI encoding for smart contract interaction:
placeBet()— EncodesplaceBet(bytes32,uint8,bool,bytes32)and sends via snapcalculateMultiplier()— Client-side multiplier previewcalculatePayout()— Client-side payout preview
Betting State Machine
The BetForm component manages a state machine that tracks the bet through
its lifecycle:
- idle: Waiting for user input
- placing: Generating seeds, getting house seed, sending transaction
- confirming: Polling backend every 3s for bet to appear on-chain
- resolving: Player seed submitted, waiting for contract resolution
- resolved: Result available, showing win/loss
Smart Contract Architecture
The DiceGame contract is deliberately simple — it handles bet storage,
seed verification, roll computation, and payouts. All game logic lives on-chain for
transparency and trustlessness.
State
| Variable | Type | Purpose |
|---|---|---|
bets | mapping(uint256 => Bet) | All bets indexed by ID |
houseSeedHashToBetId | mapping(bytes32 => uint256) | Prevents house seed reuse |
nextBetId | uint256 | Auto-incrementing bet counter |
minBet / maxBet | uint256 | Betting limits (in satoshis) |
houseEdgeBps | uint256 | House edge in basis points (100 = 1%) |
Multiplier Formula
The payout multiplier is calculated based on the win probability, minus the house edge:
// Win chance depends on target and bet direction
winChance = isOver ? (100 - target) : (target - 1);
// Multiplier in basis points (10000 = 1x)
multiplierBps = ((10000 - houseEdgeBps) * 100) / winChance;
// Example: target=50, isOver=true, edge=1%
// winChance = 50, multiplier = 9900 * 100 / 50 = 19800 (1.98x)
Safety Features
- Seed verification: Both seeds are verified against their committed hashes before resolution
- Coverage check: Contract verifies it has enough balance to pay potential winnings before accepting a bet
- Refund mechanism: Players can claim a refund if their bet isn't resolved within 1 hour
- Seed uniqueness: Each house seed hash can only be used once (prevents replay)
- Owner-only admin: Only the contract deployer can change limits or withdraw funds
Data Flow Summary
| Step | From | To | Data | Channel |
|---|---|---|---|---|
| 1 | Frontend | Backend | Request house seed | GET /api/game/house-seed |
| 2 | Backend | Frontend | houseSeedHash | HTTP response |
| 3 | Frontend | Blockchain | placeBet() + QTUM | MetaMask Snap TX |
| 4 | Blockchain | Backend | BetPlaced event | searchlogs polling |
| 5 | Frontend | Backend | playerSeed | POST /api/game/submit-player-seed |
| 6 | Backend | Blockchain | revealAndResolve() | sendtocontract RPC |
| 7 | Blockchain | Backend | BetResolved event | searchlogs polling |
| 8 | Backend | Frontend | Result (roll, win/loss) | GET /api/game/bet/:id polling |