Skip to content

Instantly share code, notes, and snippets.

@RobinLinus
Last active May 8, 2026 09:50
Show Gist options
  • Select an option

  • Save RobinLinus/01d4f40a1d0785fd2d16f7e84f20332d to your computer and use it in GitHub Desktop.

Select an option

Save RobinLinus/01d4f40a1d0785fd2d16f7e84f20332d to your computer and use it in GitHub Desktop.
Roulette in a Bitcoin Payment Channel

Roulette in a Bitcoin Payment Channel

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.


1. Design Goals

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.


2. Parties and Notation

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.


3. Variable Payout Accounting

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.


4. Channel Structure

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.


5. Randomness via Preimage-Length Commitments

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.

Optional conservative parameter

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.


6. Script Macro: Length Commitment

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.


7. Computing the Roll in Script

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

8. Betting Pattern and Payout Table

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

9. Payout Classes

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.


10. Taproot Leaf per Payout Class

A settlement leaf for payout class j does:

  1. Verify both preimages.
  2. Compute roll.
  3. Check that roll belongs to outcomes_j.
  4. 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.


11. Boolean Lookup for Outcome Sets

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.


12. Taproot Tree Optimization

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.


13. On-Chain Dispute Graph

Each side can force the game on-chain.

A starts the dispute

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.

B starts the dispute

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.


14. Why Reveal Transactions Are Needed

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.


15. Game Outputs and Timeouts

game_A0

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.

game_A1

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.


16. Revocation Safety for Descendants

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.


17. Cooperative Off-Chain Round Flow

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.


18. Revocation Protocol

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.


19. Settlement Transaction Inventory

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.


20. Fee, Dust, and Pinning Considerations

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.


21. Correctness Invariants

Before signing a state, both parties should verify all invariants below.

Balance invariants

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

Payout invariants

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

Randomness invariants

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

Reveal-timeout invariants

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.

Revocation invariants

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.

Script-generation invariants

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.

22. Security Properties

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.

23. Summary

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.


References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment