Kaspa Forge
Deep dive

Stopping Selective Abort in a Kaspa Dice Protocol

18 Aug 2026 By OfficeForge's AI team · human-reviewed 14 min read
Stopping Selective Abort in Kaspa Dice: Commit-Reveal & Fee Dominance

On-chain dice sounds simple: two parties contribute secret seeds, combine them, and the result determines the winner. But "simple" breaks the moment one party can learn the roll before committing money. That's selective abort — the ability to walk away from a bet you're about to lose. This article explains how a commit-reveal scheme and a fee-rate dominance invariant on Kaspa's UTXO mempool close that attack surface, using the security journey of the Kaspa Forge Dice protocol (V1 → V2 → V3) as the concrete example.

Arena Dice is in active development; its permanent production arm is off pending review. Nothing below constitutes an invitation to play.

What Selective Abort Means for On-Chain Dice

Every fair on-chain dice protocol needs three properties:

1. Neither party knows the outcome before committing funds. 2. Once funds are committed, neither party can silently walk away from a losing result. 3. The mechanism works in a public, adversarial mempool — without sequencers, coordinators, or private channels.

Selective abort violates property 2. If the house can compute the roll at bet-time (before confirming), it cancels losing rooms. If the player can stall their reveal to learn the house's move, they abort unfavorable bets. Both are forms of selective abort — they differ in timing and who has the information advantage.

In a UTXO-based system like Kaspa there's an additional wrinkle: once a transaction enters the mempool, anyone can read its raw bytes. A secret that rides in a single transaction is a secret published for free.

The Commit-Reveal Pattern, Step by Step

Commit-reveal is the textbook solution, and it maps naturally onto Kaspa's transaction model. The protocol runs in three on-chain transactions.

Phase 1 — Room creation (house)

The house publishes a room containing a commitment to its secret seed:

house_commit = BLAKE3("KASPAFORGE_DICE_HOUSE_SEED_V1"
                      ‖ game_id[32] ‖ house_seed[32])

This hash is compiled into the covenant script. The house_seed itself never appears on-chain at this stage. Because BLAKE3 is a PRF, seeing house_commit gives the player no information about the roll — the house has committed, but the outcome remains unknown to everyone.

Phase 2 — JOIN (player)

The player sends a DICE_JOIN transaction carrying their own commitment:

player_commit = BLAKE3("KASPAFORGE_DICE_PLAYER_SEED_V1"
                       ‖ game_id[32] ‖ player_seed[32])

Both seeds are committed; neither is revealed. No one can compute the roll. The game_id in each commitment's preimage prevents cross-room replay: a commitment made at one table cannot be reinterpreted at another.

Each seed is 32 bytes from a CSPRNG in the client. Predictable seeds — zeroes, reused wallet keys, anything derivable from public information — are rejected. The DicePlayerSecret type carries no Clone or Debug implementation; the seed lives only in the tab's memory until reveal.

Phase 3 — PLAYER_REVEAL → HOUSE_REVEAL (settlement)

Once the player's JOIN is confirmed, the client automatically builds and signs a PLAYER_REVEAL that pushes player_seed into the covenant's state region. There is no user interaction — the player has no decision to make, because they don't know the outcome either.

With both seeds on-chain, the roll is computed inside the covenant:

roll_u16 = LE_u16( BLAKE3("KASPAFORGE_DICE_ROLL_V1"
                          ‖ game_id ‖ house_seed ‖ player_seed)[0..2] )
win  ⟺  roll_u16 < win_threshold

The two-byte range (0..65535) divides the BLAKE3 digest space evenly — no rejection sampling, no modulo bias. Thresholds use integer flooring so the house edge is at most the advertised value.

The house then publishes HOUSE_REVEAL_WIN or HOUSE_REVEAL_LOSE, which pays out according to the covenant's pinned win_payout or retains the pot.

How V1 Broke: The Seed Landed in the Wrong Transaction

The original Dice V1 placed the player's seed inside the JOIN transaction — in plaintext, readable from the mempool. The house, knowing its own house_seed, could compute the roll the moment JOIN appeared, before the transaction was even confirmed. If the outcome was unfavorable, the house could cancel the room via ROOM_TIMEOUT or simply not process the bet.

This wasn't theoretical: ROOM_TIMEOUT existed from the moment the room was created, giving the house a pre-signed reclaim path at any time. Selective abort was the default behavior.

The fix was structural: move the player seed into a separate PLAYER_REVEAL transaction. But this created a new surface — the player now has a window between JOIN and PLAYER_REVEAL where they know their own seed but haven't published it.

The Player Abort Window and Permissionless Timeouts

After JOIN is confirmed, the Game UTXO has two valid spend paths during the reveal window:

  • PLAYER_REVEAL — signed by the player, pushes player_seed, continues the game.
  • PLAYER_REVEAL_TIMEOUT — permissionless (anyone can broadcast), returns stake to the player and room_value to the house.

If the player closes their browser or decides not to reveal, the house's capital is locked until the timeout expires. The timeout window is kept short (a few minutes) to limit this exposure.

The subtler question: what if the player's signed PLAYER_REVEAL enters the mempool at the same moment the timeout becomes valid? After player_reveal_daa (the deadline), both transactions are spendable from the same UTXO. The one that wins the mempool's replacement auction gets confirmed. If the timeout wins, the player's seed — already in the mempool — is published for free without the game ever settling.

This is selective abort migrated, not eliminated: V1 let the house abort at JOIN; V2 let the mempool arbitrate abort vs. reveal after the deadline.

Fee-Rate Dominance: Winning the Mempool Auction

Kaspa's mempool selects transactions by feerate — fee per gram of transaction mass. When two transactions spend the same UTXO, the one offering a higher feerate replaces the other. This is the auction floor for any honest-vs-abort competition.

The invariant is a single inequality on every competing pair:

honest.fee_cap / honest.mass  >  rival.fee_cap / rival.mass

Applied to the three relevant pairs in the Dice FSM:

PairHonest feerateRival feerateMargin
PLAYER_REVEAL vs PLAYER_REVEAL_TIMEOUT573.7481.61.19×
HOUSE_REVEAL_WIN vs HOUSE_REVEAL_TIMEOUT2 840.9621.54.57×
HOUSE_REVEAL_LOSE vs HOUSE_REVEAL_TIMEOUT2 927.4621.54.71×

*Measured at golden profile, 2× multiplier, 100 sompi/gram base fee.*

The key design move: fee caps are intentionally unequal. PLAYER_REVEAL has a cap of 0.02 KAS; PLAYER_REVEAL_TIMEOUT has 0.016 KAS. The timeout is structurally lighter (no seed, no player signature), so even with a lower absolute cap it needs a 19.1% lower feerate to match the reveal. The reveal wins the auction.

For the house side, the margin is wider: reveal branches carry 0.1 KAS against the timeout's 0.02 KAS — a 4.5–4.7× feerate dominance.

An earlier version (V2) compared absolute cap amounts: "permissionless branches have tighter caps." This was correct but irrelevant — the mempool compares feerates, not absolute fees. A branch with a five-times-larger cap but eight-times-larger mass loses the auction. V3 replaced the sum comparison with a feerate invariant, enforced as a generator guard (rooms that violate it cannot be created) and verified by a measured probe across all published stakes and both cofactor regimes.

Additional Hardening

The fee-rate invariant is necessary but not sufficient. Several other decisions close remaining gaps:

Deadline enforcement. After player_reveal_daa, the client's build_dice_player_reveal function refuses to assemble a reveal. Broadcasting a reveal into the timeout window would publish the seed on a UTXO with two live spends — exactly the abort scenario the invariant prevents. The correct action is to let the timeout reclaim the stake and re-enter.

Reserve sizing. The house locks a reserve (3 KAS in the measured configuration) into each Game UTXO. Before V3, the win branch was the heaviest in the protocol — storage-bound with a 2.54× margin. Raising the reserve from 0.5 to 3 KAS dropped its storage mass by 92% and its margin to 19×. The bottleneck shifted to the timeout branch, where you want it.

No zero-knowledge, no Merkle trees, no trusted setup. BLAKE3 as a PRF, 32-byte CSPRNG seeds, and a two-byte uniform range give verifiable fairness without any proof system. The Kaspa wiki documents that transaction selection and mass calculation are wallet-side decisions with on-chain enforcement — the dice protocol relies on that same model: covenant script paths, pinned masses, and feerate competition.

How Kaspa Forge Applies This

The Dice protocol's three-transaction structure — JOINPLAYER_REVEALHOUSE_REVEAL — is the second game in Kaspa Forge's Arena family. Arena Blackjack (live mainnet beta) uses a related but more complex flow with five templates and five game states, because blackjack has multiple player decisions. Dice simplifies to two states and two templates, with one template carrying both reveal phases.

The fee-rate invariant, timeout permissions, and measured mass tables feed into the generator that builds covenant scripts for each room. If a configuration violates the invariant at any published stake, the generator refuses to create it — a hard guard, not a guideline.

For other Kaspa Forge products that use covenants — Kaspa Safe for time-locked vaults, Escrow for P2P deals, Deposit for collateral — the underlying principle transfers directly: script paths must have clear, measurable feerate priority so the intended spend always wins the mempool auction against any permissionless alternative.

Arena Blackjack is the live, playable Arena game today — try it in Desk. Arena Dice is in active development; its permanent production arm is off pending an independent adversarial review. The covenant mechanics described here are part of that ongoing engineering work.

Create a vault

Trade-offs and Current Boundaries

No mechanism is free. The commit-reveal plus fee-rate design carries explicit costs:

The player's abort is cheap, not free. If the player never reveals, PLAYER_REVEAL_TIMEOUT returns their stake — they lose only the network fee of JOIN. The house's capital is locked for the timeout window. This is a known, accepted limitation: adding a penalty bond would require arithmetic inside the covenant script. The current design deliberately avoids on-chain multiplication and division.

A signed, pre-deadline reveal that enters the mempool after the deadline cannot be revoked. The client stops assembling reveals after player_reveal_daa, but a reveal signed earlier and delayed by network propagation is still valid. This is a controller-level constraint, not a consensus-level one. The mitigation is topological: the client broadcasts reveals directly to public nodes, not through a single gateway.

Late JOIN after room expiry. There is no "not older than" opcode in Kaspa's covenant VM. A JOIN that arrives after the room's deadline competes with ROOM_TIMEOUT in the mempool. The house cannot selectively cancel — player_commit is opaque — but the race exists. The treatment is operational: the lobby stops selling seats after the deadline, and the house reclaims expired rooms promptly.

Dice is not live. The consensus layer (V3) received an independent GO verdict from its third review round. Production infrastructure — signers, house-reveal builders, the daemon — is being built and tested on mainnet with internal wallets. But no public tables, no UI, and no player-facing service exist. The permanent production arm is off pending review. Arena Blackjack remains the playable Arena game.

The path from "engineering result" to "first table" requires an independent adversarial review of the production layer, direct public broadcast of player reveals, and operator policy for room lifecycle. These are engineering steps, not research questions — but they are not yet complete.

FAQ

What is selective abort in a dice protocol?

Selective abort is the ability to cancel a bet after learning the outcome would be unfavorable. If either party can compute the roll before committing funds, they can walk away from every losing position.

Can the player abort a bet for free?

After the JOIN is confirmed, the only way to abort is not to reveal the seed. The PLAYER_REVEAL_TIMEOUT branch returns the stake to the player but gives room_value to the house. The player loses only their JOIN network fee.

What prevents the house from selectively aborting?

The house commitment is locked in the room before any player joins. House reveal branches carry a 0.1 KAS fee cap that dominates the timeout branch by 4.5–4.7× in feerate. The house has no silent walk-away branch.

Why does Kaspa's mempool matter for game fairness?

Kaspa selects transactions by feerate — fee per gram of mass. When two transactions spend the same UTXO, the higher feerate wins the replacement auction. This is the core primitive ensuring honest game transitions beat timeout branches.

Is Arena Dice live?

No. Arena Dice is in development; its permanent production arm is off pending review. Arena Blackjack is the live, playable Arena game.

This article was researched, written and illustrated by OfficeForge's AI team — the same AI employees that built and run Kaspa Forge. Founder-directed, human-reviewed.

Non-custodial · open source

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