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:

┌─────────────────────────────────┐ │ PLAYER BROWSER │ │ │ │ React Frontend (Vite + TS) │ │ ┌───────────┐ ┌──────────────┐ │ │ │ BetForm │ │ useQtumWallet│ │ │ │ component │ │ (Snap hook) │ │ │ └─────┬─────┘ └──────┬───────┘ │ │ │ │ │ └────────┼───────────────┼─────────┘ │ HTTP/WS │ wallet_invokeSnap ▼ ▼ ┌────────────────┐ ┌──────────────────┐ │ BACKEND │ │ METAMASK │ │ Express + │ │ + qtum-wallet │ │ WebSocket │ │ │ │ │ │ Signs & broadcasts│ │ Game Manager │ │ transactions │ │ Contract Mon. │ └────────┬─────────┘ │ QTUM RPC │ │ └───────┬────────┘ │ │ RPC calls │ Signed TX ▼ ▼ ┌──────────────────────────────────┐ │ QTUM BLOCKCHAIN │ │ │ │ ┌────────────────────────────┐ │ │ │ DiceGame Contract │ │ │ │ │ │ │ │ placeBet() │ │ │ │ revealAndResolve() │ │ │ │ claimRefund() │ │ │ │ │ │ │ │ Events: BetPlaced, │ │ │ │ BetResolved │ │ │ └────────────────────────────┘ │ └──────────────────────────────────┘

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

1

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.

backend/src/qtum.ts
export function generateSeed(): string {
  const randomBytes = crypto.randomBytes(32);
  return '0x' + randomBytes.toString('hex');
}

export function hashSeed(seed: string): string {
  return keccak256(seed);
}
2

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 seed
  • target — the target number (2-98)
  • isOver — betting over or under
  • houseSeedHash — the hash received from the backend
  • QTUM value — the bet amount attached to the transaction
frontend/src/utils/crypto.ts
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('');
}
3

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.

QTUM searchlogs Quirk

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].

4

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.

5

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;
6

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 BetPlaced events 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 searchlogs RPC to find BetPlaced and BetResolved events
  • 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() and recordBetResolved() on the game manager

QtumClient (qtum.ts)

A wrapper around QTUM's JSON-RPC interface:

MethodRPC CallPurpose
callContract()callcontractRead-only contract calls (view functions)
sendToContract()sendtocontractExecute state-changing contract functions
searchlogs()searchlogsFind contract events in block range
getBlockchainInfo()getblockchaininfoCurrent 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 hash
  • pending-reveals.json — Bets awaiting resolution
  • resolved-bets.json — History of resolved bets
Design Decision: File-Based Storage

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 balance
  • sendContractTransaction() — Sends encoded contract calls via the snap
  • refreshBalance() — Updates the displayed wallet balance
  • State: isConnected, address (base58), addressHex, balance

useContract

Handles ABI encoding for smart contract interaction:

  • placeBet() — Encodes placeBet(bytes32,uint8,bool,bytes32) and sends via snap
  • calculateMultiplier() — Client-side multiplier preview
  • calculatePayout() — Client-side payout preview

Betting State Machine

The BetForm component manages a state machine that tracks the bet through its lifecycle:

┌──────┐ user clicks ┌─────────┐ tx submitted ┌────────────┐ │ idle │ ──────────────> │ placing │ ─────────────> │ confirming │ └──────┘ └─────────┘ └─────┬──────┘ ^ │ │ │ on-chain │ ┌──────────┐ bet resolved ┌───────────┐ │ confirmed └──────── │ resolved │ <───────────── │ resolving │ <──┘ └──────────┘ └───────────┘
  • 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

VariableTypePurpose
betsmapping(uint256 => Bet)All bets indexed by ID
houseSeedHashToBetIdmapping(bytes32 => uint256)Prevents house seed reuse
nextBetIduint256Auto-incrementing bet counter
minBet / maxBetuint256Betting limits (in satoshis)
houseEdgeBpsuint256House 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

StepFromToDataChannel
1FrontendBackendRequest house seedGET /api/game/house-seed
2BackendFrontendhouseSeedHashHTTP response
3FrontendBlockchainplaceBet() + QTUMMetaMask Snap TX
4BlockchainBackendBetPlaced eventsearchlogs polling
5FrontendBackendplayerSeedPOST /api/game/submit-player-seed
6BackendBlockchainrevealAndResolve()sendtocontract RPC
7BlockchainBackendBetResolved eventsearchlogs polling
8BackendFrontendResult (roll, win/loss)GET /api/game/bet/:id polling