A fair dice protocol must guarantee one thing: neither party knows the outcome when the bet is placed. On a centralized server you take this on trust. On-chain you can enforce it with cryptography. Kaspa Forge Arena Dice uses a two-seed commit-reveal scheme over Kaspa's covenant system — no zero-knowledge proofs, no Merkle trees, no off-chain oracle. The entire protocol runs in three covenant-validated transactions, using BLAKE3-256 as its sole hash primitive.
Status boundary: the protocol described here is the reviewed and remediated V3. The third independent review round returned GO on 4 August 2026. The permanent production arm is not armed — there is no public UI, no open tables, and no mainnet public bets. Arena Blackjack is the live Kaspa Forge game today. This article explains the audited design, not a shipping product.
The Problem: Selective Cancellation
Consider the simplest possible on-chain dice: one party picks a secret, commits a hash, the other reveals, and the roll is computed. The problem is ordering. If the player reveals first, the house — knowing its own seed — can compute the exact roll. If the roll is unfavorable, the house simply never reveals, and the bet times out. Selective cancellation turns a "provably fair" game into one where the house wins every time it matters.
Reverse the order: the player reveals after the house. Now the player can compute the roll before committing to the bet. A rational player wagers only when they know they win. Same vulnerability, different actor.
The first version of Arena Dice (V1) shipped with the player's seed riding in plaintext inside the DICE_JOIN transaction. Since the house knows its own seed and can read any pending transaction in the mempool, it could compute the roll *before the bet was even confirmed*. Against a predictable or reused seed, the house could brute-force its own seed in two attempts and publish a room where the player cannot win. The V1 audit (two independent lenses, 2 August 2026) returned NO-GO on two orthogonal High findings for exactly these reasons.
The fix is two-seed commit-reveal: neither seed is visible when the commitment is made, and neither party reveals before the other has already committed a hash.
The Two-Seed Commit-Reveal Protocol
The scheme has four phases, each corresponding to an on-chain state transition. Throughout, game_id is a replay-preventing identifier derived from the room's anchor transaction and the program version — two rooms with the same game_id would share every derived commitment, so uniqueness is enforced at construction time.
Phase 1 — House Commits
When the operator opens a dice table, the covenant room is created with the house commitment compiled into the script as a constant:
house_commit = BLAKE3(KASPAFORGE_DICE_HOUSE_SEED_V1
‖ game_id ‖ house_seed)
The 32-byte house_seed is generated from the platform's CSPRNG. The commitment is public the moment the room exists, but the seed is not — BLAKE3 is a pseudorandom function, so the hash reveals nothing about the preimage. The covenant enforces this by comparing the revealed seed against the compiled constant at settlement time.
Phase 2 — Player Commits at JOIN
A player sitting down sends a DICE_JOIN transaction carrying:
- the commitment (not the seed):
player_commit = BLAKE3(KASPAFORGE_DICE_PLAYER_SEED_V1
‖ game_id ‖ player_seed)
- the wagered stake locked in a Game UTXO.
The client library mints the 32-byte player_seed from the browser's crypto.getRandomValues. The DicePlayerSecret type has no Clone or Debug implementation — the compiler literally prevents the seed from being duplicated or printed. The seed lives in page memory only; it is never written to disk, localStorage, or sent over the network. (The Kaspa wiki covers the UTXO transaction model this builds on.)
At this moment both commitments are on-chain. Neither party knows the other's seed. No one can compute the roll. The covenant — checking only the hash — admits any player, and the commitment is tied to this specific game_id so it cannot be replayed at another table where the seed might already be known.
Phase 3 — Player Reveals
The player sends a PLAYER_REVEAL transaction that pushes their player_seed into the covenant's data region. The script hashes it with the same domain string and game_id, then compares the result against the commitment stored at JOIN. If the hashes match, the Game UTXO transitions to its second state.
Reveal must happen before the player_reveal_daa deadline. The client automates this — the player has no meaningful choice to make, since they don't know the outcome. Not revealing means cancelling your own bet. If the deadline passes without a reveal, a permissionless PLAYER_REVEAL_TIMEOUT branch becomes spendable: the player gets their exact stake back and the house gets the room value. The player forfeits only the network fee of their original JOIN.
Phase 4 — House Reveals and Settlement
With the player's seed now on-chain, the house sends a HOUSE_REVEAL_WIN or HOUSE_REVEAL_LOSE transaction, pushing the house_seed into the witness. The covenant opens the commitment with OpBlake3 (opcode 0xd9), hashes both seeds, and computes the roll — all inside the script. The seed width is verified by OpSize in every fragment that reads it, not just once.
If the house misses its deadline, a permissionless HOUSE_REVEAL_TIMEOUT branch gives the player the entire Game UTXO. The house has no rational incentive to withhold.
The Roll: BLAKE3 Over 2¹⁶
The roll formula is deterministic and minimal:
roll_u16 = LE_u16(
BLAKE3(KASPAFORGE_DICE_ROLL_V1 ‖ game_id
‖ house_seed ‖ player_seed)[0..2]
)
win ⟺ roll_u16 < win_threshold
The first two bytes of a BLAKE3 digest give a uniform integer in 0–65 535. This range is a power of two, so it divides the hash space on the table — no remainder, no bias, no rejection sampling. A mod 10 000 approach would leave 5 536 biased remainders (65 536 = 6 × 10 000 + 5 536), producing a measured 16.67 % overweight on low values. The power-of-two discipline is inherited from the same rule that governs Kaspa Forge's card shuffling.
A win threshold T is computed from the target payout multiplier M and the house edge in basis points:
T = floor(65 536 × (10 000 − edge_bps) / (10 000 × M))
The floor biases toward the house. The realized edge overshoot is measured at under 500 parts per million for all three multiplier steps:
| Multiplier | Threshold | Win probability | Realized edge |
|---|---|---|---|
| 2× | 32 112 | 0.489990 | 2.002 % |
| 5× | 12 845 | 0.195999 | 2.000 % |
| 10× | 6 422 | 0.097992 | 2.008 % |
Payout is an exact sompi amount pinned as a compiled constant — the covenant performs no multiplication or division at runtime, eliminating rounding edge cases entirely.
One subtlety worth noting: Kaspa reads stack elements as signed little-endian. A raw two-byte value with its high byte ≥ 0x80 would be interpreted as negative, making rolls above 32 767 automatically "less than" any positive threshold — the player wins for free on half the outcomes. The fix is a zero-byte append (substr(0,2) ‖ 0x00) producing a three-byte non-negative encoding. Under covenants_enabled, the engine accepts this redundant form.
On-Chain Topology: Seven Branches, Two States
The state machine is deliberately compact — seven branches, two Game states, one covenant template for both phases. PLAYER_REVEAL reconstructs the successor as the same script with a new data region prepended, so the script code is never passed in the witness. Compare this with Arena Blackjack, which uses a five-template chain and twenty branches.
Room
├── DICE_JOIN (player sig) → Game T0
└── ROOM_TIMEOUT (permissionless) → house
Game · T0 (awaiting player reveal)
├── PLAYER_REVEAL (player sig) → Game T1
└── PLAYER_REVEAL_TIMEOUT (permissionless) → stake to player, room_value to house
Game · T1 (awaiting house reveal)
├── HOUSE_REVEAL_WIN (house sig) → player gets win_payout
├── HOUSE_REVEAL_LOSE (house sig) → house gets game_value
└── HOUSE_REVEAL_TIMEOUT (permissionless) → player gets entire Game UTXO
Three transactions per bet. No separate service output — the house margin is its share of the UTXO. Permissionless timeout branches mean *anyone* can unstick a game where a party disappears.
Fee-Rate Dominance: The Mempool Safety Invariant
Kaspa nodes replace pending transactions by fee rate (sompi per gram). Each honest action — reveal, settlement — must outbid the permissionless timeout branch that competes for the same UTXO. The invariant is:
honest.fee_cap / honest.mass > rival.fee_cap / rival.mass
In V1/V2, the code compared sums of fee caps — technically correct but uninformative, because mempools compare rates, not totals. A branch with a five-times-larger cap could lose the auction if it weighed eight times more. That was the mechanism behind both High findings in the V2 audit. V3 replaced the sum comparison with a single feerate-dominance implementation, a generator guard that refuses to build a room violating it, and a probe section that prints both feerates at every bet size and multiplier. Measured minimum margins: 1.19× on the player-reveal pair, 4.57× and 4.71× on the house settlement pairs.
How Kaspa Forge Uses It
The Arena product family on Kaspa Forge is built on Kaspa's Toccata covenants — the same op-code layer that powers Kaspa Safe vaults, Escrow deals, and Deposit collateral. Arena Blackjack is the live mainnet beta: money stays in covenant-controlled UTXO, keys never leave the device, and proofs are independently verifiable. Dice shares this architecture — the same non-custodial model, the same Desk encrypted browser profile, the same open-source contracts.
The consensus layer of dice (V3) is built, reviewed, and remediated. The permanent production arm is not armed (ARENA_DICE_ARMED = 0). What remains: the controller layer for UI, rooms, and operator policy (designated P3 in the internal build plan); an independent adversarial audit of the controller; direct public broadcast of player reveals without routing through the Forge gateway; and a fee-escalation decision. Until these are complete, there are no public tables.
Arena Blackjack is the live Kaspa Forge game — provably fair, non-custodial, and playable now from Desk. Dice shares the same covenant and key architecture but is not yet armed for public play. The protocol design and open-source contracts are available in the Kaspa Forge documentation.
Trade-offs and Honest Limitations
Three transactions per bet. Unlike a centralized dice game (one HTTP call), on-chain dice requires three confirmed Kaspa transactions — JOIN, PLAYER_REVEAL, and HOUSE_REVEAL. The measured cost is roughly 0.0116 KAS per bet at 2× with the base network fee rate. That is about 18.5× cheaper per hand than Arena Blackjack, but it is not zero, and the house reserves locked capital in the UTXO for the lifetime of the table.
Seed entropy is a trust boundary, not a cryptographic guarantee. The DicePlayerSecret type enforces that the seed is minted from the platform's CSPRNG and cannot be cloned or inspected. But a from_entropy constructor exists for wallets with their own hardware source — it accepts any 32 bytes and rejects three recognizable "nobody generated this" patterns. Against a predictable or reused seed, the house can compute the roll from the commitment alone. This is measured: the consensus probe runs a dictionary of predictable seeds against every commitment and confirms the attack succeeds for bad entropy and returns nothing for good entropy.
Player reveal is irreversible once signed. If a player signs a PLAYER_REVEAL before the deadline and the transaction is not included before the deadline passes, the signed transaction still exists and could be broadcast by anyone who has it. The client library stops constructing reveals once the deadline window closes, but it cannot recall one already signed. This is a controller-level contract, not a consensus-level guarantee — it is acknowledged in the go-checklist.
Late JOIN after room expiry. Kaspa has no "not older than" opcode, so a DICE_JOIN arriving after the room deadline still competes with ROOM_TIMEOUT for the same UTXO. The house cannot distinguish a winning late join from a losing one (the seed commitment hides the outcome), so selective cancellation is not possible — but the race exists. The mitigation is operational: the lobby does not sell seats after expiry and the house reclaims expired rooms promptly.
No ZK overhead — and no ZK properties. Dice uses no zero-knowledge guest program, no image_id, no prover. The BLAKE3 commitment is sufficient because neither party has an informational advantage once both seeds are committed. This keeps the covenant small and the per-bet cost low, but once PLAYER_REVEAL lands on-chain, the roll is publicly computable by anyone — there is no computational privacy for the outcome.
FAQ
What makes on-chain Kaspa dice provably fair?
Both parties commit BLAKE3 hashes of their secret seeds before either sees the other's value. The roll is derived from both seeds via BLAKE3, so neither party can compute or influence the outcome until both seeds are on-chain.
How many transactions does one dice bet require?
Three. The player sends JOIN (carrying their seed commitment) and PLAYER_REVEAL (exposing the seed); the house sends either HOUSE_REVEAL_WIN or HOUSE_REVEAL_LOSE, which computes the final roll inside the covenant.
What happens if the player closes their browser before revealing?
The secret seed lives only in page memory — it is never persisted. After the reveal deadline, a permissionless PLAYER_REVEAL_TIMEOUT returns the player's exact stake. The player loses only the network fee of their JOIN transaction.
What happens if the house refuses to reveal?
After the house deadline, a permissionless HOUSE_REVEAL_TIMEOUT sends the entire game pot to the player. Withholding a reveal costs the house its full reserve.
Is Kaspa Dice live and playable?
No. The protocol has been reviewed and remediated (V3, third review round returned GO), but the permanent production arm is not armed. There is no public UI and no open tables. Arena Blackjack is the live Kaspa Forge game on mainnet.
Why use a power-of-two range instead of modulo division?
A hash mod 10 000 is biased because 2¹⁶ = 65 536 is not evenly divisible by 10 000. A raw two-byte hash yields 0–65 535 with perfect uniformity — no rejection sampling, no discarded values, and an exact fractional house edge.
Put your KAS where theft can be cancelled
A covenant vault on Kaspa mainnet: your keys, your rules, our tooling. Free on-chain, forever.
Create a vault
