A protocol sketch for a two-party roulette game between a player and a casino. It uses a revocable 2-of-2 payment channel, hash-preimage-length commitments for randomness, Taproot script paths for unilateral dispute resolution, and payout-class settlement transactions for arbitrary roulette betting patterns.
This is a design document, not production-ready consensus-critical code. Every script fragment below is pseudocode and must be compiled, simulated, and tested on regtest before use with real value.
The protocol should provide:
- Fast off-chain rounds with normal web UX latency.
- Trustless unilateral dispute resolution on-chain.
- Arbitrary betting patterns, such as “red plus all primes plus 16”.
- Variable payouts depending on the roulette result.
- No on-chain interaction during cooperative play.
- Revocation of old channel states, similar in spirit to Lightning.
- A covenant-less construction using only standard Bitcoin/Taproot mechanisms.
The protocol does not use standard Lightning HTLCs. It is a custom two-party game channel.
We use generic party names:
A = player
B = casino
Most mechanisms are symmetric. txA_N is the unilateral commitment transaction broadcastable by A; txB_N is the symmetric transaction broadcastable by B.
Channel balances before a round:
balance_A
balance_B
For one round:
S = total stake committed by A
Gmax = maximum possible gross payout to A over all roulette outcomes
L = max(0, Gmax - S) # casino maximum gambling liability
bond_A = cooperation bond posted by A
bond_B = cooperation bond posted by B
risk_pot = S + L
game_pot = S + L + bond_A + bond_B
The round locks only the risked funds and cooperation bonds into the game pot:
base_A = balance_A - S - bond_A
base_B = balance_B - L - bond_B
game_pot = S + L + bond_A + bond_B
The cooperation bonds are refunded in normal settlement and forfeited on reveal-withholding timeouts. They should exceed the expected on-chain cost and nuisance value of forcing the counterparty to wait for a timeout.
For every roulette result r ∈ [0,36], both parties compute:
gross_payout[r] = amount paid to A from the gambling risk_pot
This is a gross payout: it includes return of winning stakes.
Normal settlement for result r pays:
A receives: gross_payout[r] + bond_A
B receives: risk_pot - gross_payout[r] + bond_B
So final balances are:
final_A = base_A + gross_payout[r] + bond_A
= balance_A - S + gross_payout[r]
final_B = base_B + risk_pot - gross_payout[r] + bond_B
= balance_B + S - gross_payout[r]
The bonds cancel out during honest settlement, but create a strict penalty for withholding.
If a party times out after refusing to reveal, the other party sweeps the entire game_pot, including both bonds. This makes reveal withholding strictly worse than settlement.
All funds are locked in a funding output:
funding_output = 2-of-2(A_funding_key, B_funding_key)
For each channel state N, both parties hold two fully signed unilateral commitment transactions:
txA_N = commitment transaction broadcastable by A
txB_N = commitment transaction broadcastable by B
If a round is pending, txA_N contains:
base_A output # delayed/revocable, because A is the broadcaster
base_B output # direct or less delayed to B
game_A0 output # A-started on-chain roulette dispute path
Symmetrically, txB_N contains:
base_B output # delayed/revocable, because B is the broadcaster
base_A output # direct or less delayed to A
game_B0 output # B-started on-chain roulette dispute path
Old states are revoked using a Lightning-like revocation mechanism. Once state N+1 is safely signed by both parties, the parties exchange revocation secrets for state N. Broadcasting a revoked state allows the counterparty to sweep revoked outputs.
Revocation protects against old states. It does not solve randomness withholding inside the latest active round; that requires reveal timeouts inside the game outputs.
Each party contributes one roulette share in {0, ..., 36} by committing to a SHA256 preimage whose length encodes the value.
Default base length:
BASE = 12 bytes
For party A:
a_share ∈ [0,36]
len(R_A) = BASE + a_share
C_A = SHA256(R_A)
For party B:
b_share ∈ [0,36]
len(R_B) = BASE + b_share
C_B = SHA256(R_B)
With BASE = 12, valid preimage lengths are:
12..48 bytes inclusive
In script this is checked as:
12 <= len(R) < 49
The roulette result is:
roll = (a_share + b_share) mod 37
Since a_share + b_share ∈ [0,72], modulo 37 is implemented by one conditional subtraction.
If at least one party chooses its share uniformly at random, the final roll is uniform over 0..36.
BASE = 12 gives 96 bits of preimage entropy. This may be acceptable when the economic value at risk is far below the cost of brute force. A conservative production variant can use:
BASE = 32
valid lengths = 32..68 bytes inclusive
script range = <32> <69> OP_WITHIN
This costs 20 extra witness bytes per revealed preimage.
Pseudo-Bitcoin-Script macro:
# LENCOM(C, BASE, BASE_PLUS_37)
# Input: <R>
# Output: value = len(R) - BASE
OP_DUP
OP_SHA256 <C> OP_EQUALVERIFY
OP_SIZE
OP_DUP <BASE> <BASE_PLUS_37> OP_WITHIN OP_VERIFY
<BASE> OP_SUB
OP_SWAP OP_DROP
For BASE = 12, BASE_PLUS_37 = 49.
This verifies the preimage against the commitment and leaves the encoded roulette share on the stack.
# witness provides R_A and R_B
LENCOM(C_A, 12, 49) # -> a_share
OP_TOALTSTACK
LENCOM(C_B, 12, 49) # -> b_share
OP_FROMALTSTACK
OP_ADD # -> sum in [0,72]
OP_DUP <37> OP_GREATERTHANOREQUAL
OP_IF
<37> OP_SUB
OP_ENDIF # -> roll in [0,36]
The player may submit an arbitrary betting pattern. For example:
1000 bitcoins on red
1000 bitcoins on all prime numbers
500 bitcoins on 17
Off-chain, both parties deterministically compute:
gross_payout[0..36]
Example:
A bets 1000 bitcoins on red and 1000 bitcoins on 17.
roll is red but not 17:
gross_payout = 2000
roll is 17:
gross_payout = payout(red if applicable) + payout(straight 17)
roll is neither red nor 17:
gross_payout = 0
The maximum casino liability is:
Gmax = max(gross_payout[0..36])
L = max(0, Gmax - S)
Before signing the round, both parties must verify:
balance_A >= S + bond_A
balance_B >= L + bond_B
Bitcoin Script cannot take a number from the stack and dynamically enforce transaction output amounts.
Instead, group roulette outcomes by equal payout amount:
payout_class_j:
payout_amount_j
outcomes_j = { r | gross_payout[r] == payout_amount_j }
For each payout class, create one Taproot script leaf and one pre-signed settlement transaction.
The script leaf verifies only:
roll ∈ outcomes_j
The pre-signed settlement transaction enforces the concrete output amounts:
settle_j:
input: game_A1 or game_B1
output: payout_amount_j + bond_A to A
output: risk_pot - payout_amount_j + bond_B to B
Because the settlement transaction is signed with an output-committing sighash, the signatures enforce the concrete payout.
This avoids parallel games and avoids dynamic output amounts in Script.
A settlement leaf for payout class j does:
- Verify both preimages.
- Compute
roll. - Check that
rollbelongs tooutcomes_j. - Verify signatures for the exact settlement transaction
settle_j.
Witness order below is written bottom-to-top. R_A is consumed first, then R_B. After both preimages are consumed, sig_B is on top of sig_A, so the script checks B's signature first.
# settle_class_j leaf
# witness bottom -> top:
# sig_A_for_settle_j
# sig_B_for_settle_j
# R_B
# R_A
LENCOM(C_A, 12, 49)
OP_TOALTSTACK
LENCOM(C_B, 12, 49)
OP_FROMALTSTACK
OP_ADD
OP_DUP <36> OP_GREATERTHAN
OP_IF
<37> OP_SUB
OP_ENDIF
# stack: sig_A sig_B roll
IS_IN_SET(outcomes_j)
OP_VERIFY
# stack: sig_A sig_B
<B_settlement_key_j>
OP_CHECKSIGVERIFY
<A_settlement_key_j>
OP_CHECKSIG
The keys should be per-state and preferably per-leaf to avoid replay or accidental signature reuse.
The membership check can be generated statically.
For a small set such as {1, 3, 5}:
# input: roll
# output: boolean, with roll removed
OP_DUP <1> OP_NUMEQUAL
OP_OVER <3> OP_NUMEQUAL OP_BOOLOR
OP_OVER <5> OP_NUMEQUAL OP_BOOLOR
OP_NIP
Do not use OP_SWAP OP_DUP ... OP_BOOLOR for this pattern; it can consume the roll value into the boolean accumulator and produce incorrect behavior. OP_OVER preserves the original roll below the accumulator, and OP_NIP removes it at the end.
For a one-element set {k}:
OP_DUP <k> OP_NUMEQUAL
OP_NIP
For large sets, it may be cheaper to check the complement and negate:
roll ∈ outcomes
can be implemented as:
NOT(roll ∈ complement(outcomes))
A compiler should choose the cheaper form.
Each script-path spend reveals a Taproot control block:
control_block_size = 33 + 32 * depth bytes
Therefore each additional Merkle level costs 32 witness bytes.
The tree should be weighted by expected use probability:
probability(class_j) ≈ |outcomes_j| / 37
Frequent payout classes should be closer to the root; rare classes can be deeper. A Huffman-like tree over payout-class probabilities minimizes expected Merkle proof size.
Typical roulette betting patterns usually produce few payout classes:
single bet: often 2 classes
red + straight-up: often 3 classes
red + even: up to 4 classes
several normal bets: often 4..8 classes
pathological patterns: up to 37 classes
So payout-class leaves are usually more efficient than one leaf per roulette number.
Each side can force the game on-chain.
funding_output
└─ txA_N
└─ game_A0
├─ revoked-state sweep by B
├─ A-no-reveal timeout -> B gets game_pot
└─ A_reveal_tx, revealing R_A
└─ game_A1
├─ revoked-state sweep by B
├─ settle_class_0
├─ settle_class_1
├─ ...
├─ settle_class_k
└─ B-no-reveal timeout -> A gets game_pot
txA_N is A's unilateral commitment transaction. A_reveal_tx spends game_A0, reveals R_A, and creates game_A1.
If A broadcasts txA_N, A must reveal within the game_A0 CSV window. If A does not reveal, B sweeps the game pot.
Once A reveals, B must reveal R_B and use the correct payout-class settlement transaction. If B does not reveal before the game_A1 timeout, A sweeps the game pot.
Symmetrically:
funding_output
└─ txB_N
└─ game_B0
├─ revoked-state sweep by A
├─ B-no-reveal timeout -> A gets game_pot
└─ B_reveal_tx, revealing R_B
└─ game_B1
├─ revoked-state sweep by A
├─ settle_class_0
├─ settle_class_1
├─ ...
├─ settle_class_k
└─ A-no-reveal timeout -> B gets game_pot
This ensures either party can force a final outcome and neither party can hold the game pot hostage by broadcasting a commitment and then refusing to reveal.
The commitment transactions txA_N and txB_N are pre-signed before the preimages are revealed.
Therefore the commitment transaction itself cannot contain the unknown preimage in its witness.
The structure is:
txA_N broadcasts the current channel state.
A_reveal_tx reveals R_A and moves game_A0 into game_A1.
The reveal transaction must be constrained so it creates exactly the expected game_A1 output. Since Bitcoin has no general covenants, this is done with pre-signed transactions and output-committing signatures.
The game_A0 script should not allow A to reveal and spend the pot arbitrarily. It should allow only the exact pre-signed A_reveal_tx, or an equivalent construction that commits to the expected game_A1 output.
Logical paths:
1. Revocation path:
If txA_N is revoked, B can sweep.
2. A reveal path:
A reveals R_A and spends into the exact pre-signed A_reveal_tx.
3. A-no-reveal timeout:
If A broadcasts txA_N but does not reveal R_A within the CSV window,
B sweeps the entire game_pot.
Logical paths:
1. Revocation path:
If txA_N was revoked, B can still sweep.
2. Settlement path:
B reveals R_B, the script computes roll, verifies the correct payout class,
and the settlement transaction pays A and B accordingly.
3. B-no-reveal timeout:
If B does not reveal in time, A sweeps the entire game_pot.
game_B0 and game_B1 are symmetric.
The timeout paths are intentionally punitive. The party that refuses to reveal loses the entire game pot, including its cooperation bond.
Revocation must protect not only the first commitment transaction but also any descendants created from it.
A malicious A might broadcast an old revoked txA_N and already know enough old data to produce:
txA_N -> A_reveal_tx -> settle_class_j
possibly in the same block. If settle_class_j immediately pays A, B may not have time to use the revocation path before the child spends are confirmed.
Therefore, every path descending from txA_N that pays A, the broadcaster, must pay A through a delayed/revocable output:
A_payout_from_txA_descendant:
├─ revocation path: B can sweep immediately with revocation secret
└─ A delayed path: A can spend after to_self_delay
Symmetrically, every path descending from txB_N that pays B, the broadcaster, must pay B through a delayed/revocable output.
This applies to:
- base outputs to the broadcaster,
- settlement outputs to the broadcaster,
- timeout outputs to the broadcaster,
- any second-stage or later outputs reachable from the broadcaster's commitment transaction.
Outputs paying the non-broadcaster may be direct, subject to dust and fee policy.
This is the same design principle as Lightning-style delayed local outputs: a revoked state must remain punishable even if the cheating party immediately moves funds through child transactions.
A normal round stays entirely off-chain.
1. B chooses R_B and sends C_B = SHA256(R_B).
2. A chooses R_A and sends C_A = SHA256(R_A).
3. Both derive the payout table and payout classes from A's betting pattern.
4. Both compute S, Gmax, L, bonds, risk_pot, and game_pot.
5. Both construct txA_N, txB_N, reveal transactions, timeout paths, settlement transactions, and delayed/revocable descendant outputs.
6. Both exchange all required signatures and enforcement data for state N.
7. Only after both can enforce state N, the previous state is revoked.
8. A reveals R_A off-chain.
9. B reveals R_B off-chain.
10. Both compute roll and the resulting balances.
11. Both sign state N+1 with updated balances.
12. State N is revoked.
If both cooperate, no transaction is broadcast.
For each state N, both unilateral commitment transactions are revocable:
txA_N must be revoked by A
txB_N must be revoked by B
State transition rule:
1. Create and sign state N+1.
2. Verify that local enforcement data for N+1 is complete.
3. Exchange revocation secrets for state N.
4. State N becomes punishable if broadcast.
Revocation paths must also protect descendants such as game outputs, reveal outputs, settlement outputs, and timeout outputs. Otherwise, a party could publish an old state and try to escape punishment by quickly moving into a child output.
For each active round and for each unilateral side, both parties must have enough pre-signed data to enforce the round.
Required data includes:
Commitment transactions:
txA_N
txB_N
Reveal transactions:
A_reveal_tx: game_A0 -> game_A1
B_reveal_tx: game_B0 -> game_B1
Settlement transactions:
settle_A_class_0..k
settle_B_class_0..k
Timeout transactions or spend paths:
A-no-reveal timeout from game_A0 -> B sweeps
B-no-reveal timeout from game_A1 -> A sweeps
B-no-reveal timeout from game_B0 -> A sweeps
A-no-reveal timeout from game_B1 -> B sweeps
Revocation information:
revocation keys/secrets for stale states and descendants
Fee-bumping data:
anchor outputs, CPFP inputs, or equivalent fee-bump mechanism
The exact transaction set depends on whether the implementation uses explicit pre-signed child transactions, direct script-path spends, Taproot key-path cooperative spends, or a mix.
Because reveal and settlement transactions are pre-signed, fee policy is security-critical.
Recommended design choices:
- Include anchor outputs or another reliable CPFP fee-bumping mechanism on all dispute transactions.
- Define fee responsibility deterministically per transaction type.
- Ensure timeout windows are long enough for confirmation under fee spikes.
- Avoid creating dust outputs; fold dust into fees or the counterparty output.
- Include fee reserves in channel balance accounting.
- Use per-state and per-transaction keys to avoid signature reuse across fee variants.
- Test mempool package behavior under fee-spike and pinning scenarios.
If an honest party cannot get its reveal or settlement transaction confirmed before a timeout, the protocol can fail economically even if the scripts are logically correct.
Before signing a state, both parties should verify all invariants below.
base_A = balance_A - S - bond_A
base_B = balance_B - L - bond_B
risk_pot = S + L
game_pot = risk_pot + bond_A + bond_B
base_A + base_B + game_pot = channel_total - fees
for every r in 0..36:
exactly one payout class accepts r
and:
for every class_j:
all r in outcomes_j have the same gross_payout[r]
settle_j pays gross_payout_j + bond_A to A
settle_j pays risk_pot - gross_payout_j + bond_B to B
C_A = SHA256(R_A)
C_B = SHA256(R_B)
len(R_A), len(R_B) ∈ [BASE, BASE+36]
roll = (len(R_A)-BASE + len(R_B)-BASE) mod 37
If A broadcasts txA_N, A must reveal R_A or B can sweep game_pot.
If B broadcasts txB_N, B must reveal R_B or A can sweep game_pot.
After A reveals, B must reveal R_B or A can sweep game_pot.
After B reveals, A must reveal R_A or B can sweep game_pot.
No old state may be safe to broadcast.
All descendant outputs of old states remain punishable or delayed enough to punish.
Any payout to the broadcaster from its own commitment branch is delayed/revocable.
Membership checks preserve roll until the final boolean is produced.
Witness ordering matches signature-check ordering.
All generated branches leave exactly one truthy stack item on success.
No branch relies on disabled or upgrade-reserved opcodes.
The protocol aims to provide:
- Fair randomness: if either party chooses its share uniformly, the roll is uniform.
- No hostage pot: a party that broadcasts a commitment must reveal or lose the game pot after timeout.
- No profitable withholding: after one party reveals, the other must reveal or lose the game pot and its bond after timeout.
- Unilateral enforceability: either party can force the round on-chain.
- Old-state protection: revocation makes stale channel states punishable, including descendants.
- Arbitrary betting patterns: the payout table supports any combination of roulette bets.
- Efficient common case: cooperative rounds require only off-chain messages and signatures.
Remaining engineering risks:
- Fee volatility, mempool pinning, and package relay behavior.
- Dust outputs for small payouts.
- Correct generation of payout-class scripts.
- Revocation safety for all second-stage descendants.
- Watchtower or online requirement during dispute windows.
- Extensive regtest and adversarial testing required before real value.
The protocol is:
2-of-2 funding output
-> two revocable unilateral commitment transactions per state
-> one game pot per pending roulette round
-> length-commitment randomness with configurable BASE, default 12 bytes
-> arbitrary betting pattern compiled into payout_table[37]
-> payout_table grouped into payout classes
-> one Taproot leaf and one settlement transaction per payout class
-> reveal timeouts at both game_A0/game_B0 and game_A1/game_B1
-> cooperation bonds to discourage spite withholding
-> delayed/revocable broadcaster payouts to prevent same-block revocation evasion
-> anchors or equivalent fee-bumping for all dispute transactions
The core design principle is:
Script decides which payout class is correct.
Pre-signed settlement transactions enforce the concrete outputs.
Revocation and delayed descendants make old states unsafe.
Timeouts and bonds make reveal withholding unprofitable.
This keeps the on-chain script simple while supporting arbitrary roulette betting patterns and variable payouts inside a fast off-chain payment channel.
- Bitcoin Script overview: https://en.bitcoin.it/wiki/Script
- BIP341 Taproot: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
- BIP342 Tapscript: https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki
- Lightning BOLT #3 transactions: https://github.com/lightning/bolts/blob/master/03-transactions.md
- 31-bit commitment reference idea: https://raw.githubusercontent.com/coins/bitcoin-scripts/refs/heads/master/31-bit_commitment.md
- Betcoins discussion/reference: https://raw.githubusercontent.com/coins/bitcoin-scripts/refs/heads/master/betcoins.md