Winner Selection
Detailed algorithm, edge cases, and examples for how Daily Lottery winners are chosen.
Purpose
This page documents the exact winner selection rules used by the on-chain finalize_winners
instruction. Auditors can reproduce winners deterministically from on-chain data.
Inputs and Canonical Ordering
Selection depends on these on-chain fields:
lottery.id,participants_count,total_tickets,attested_count(Lottery)- Per-participant:
wallet,tickets_bought,proof_of_chance_hash,voted_number_of_winnersflags (Participant) selected_number_of_winners(Lottery, resolved from attester votes)service_charge_bps(Config, for payout math)
Eligible pool = participants with tickets_bought > 0 and reveal_included == true.
Canonical ordering = sort eligible participants by wallet public key bytes ascending.
Step 1: Determine Winner Count (k)
Winner count is resolved from all attested participants:
- Participants submit
voted_number_of_winnersduringattest_uploaded. - Votes are weighted by
tickets_bought. - Highest total weight wins.
- Tie-breaks: earlier
attested_at_unix, then smaller winner count.
finalize_winners recomputes this deterministically and stores selected_number_of_winners.
k must be <= MAX_WINNERS.
Step 2: Build Deterministic Seed (Reveal-Plaintext Draw v2)
Rule version: reveal-plaintext-draw-v2
The program builds a seed with SHA-256:
S0 = SHA256(0x494B494741495F5250445F56325F53454544 || lottery_id_le64 || eligible_count_le64 || total_revealed_tickets_le64 || poc_aggregate_hash_32)
For each participant p in canonical order:
S_next = SHA256(S_prev || p.wallet_32 || p.tickets_bought_le64)
Final seed = S
poc_aggregate_hash is reveal-derived on-chain entropy input accumulated during upload_reveals.
Step 3: Draw Winners Without Replacement (Ticket-Weighted)
For round j = 0..k-1:
Dj = SHA256(0x494B494741495F5250445F56325F44524157 || S || j_le64)
rj = LE_u128(Dj[0..16]) mod total_remaining_tickets
Then:
- Walk cumulative ticket ranges in canonical order.
- Wallet whose range contains
rjis selected. - Remove selected wallet from pool.
- Repeat.
This keeps weighted ticket probability while preventing duplicate winners.
Step 4: Payout and Merkle Root
After winners are selected:
service_fee = total_funds * service_charge_bps / 10_000
total_payout = total_funds - service_fee
per_winner_payout = total_payout / winners_count
Merkle leaf per winner:
leaf = SHA256(index || recipient || amount)
Merkle root is stored on-chain and used by settle_payout_batch proof verification.
Edge Cases and Settlement Paths
- No buyers (
total_tickets == 0): lottery settles immediately withNoBuyersConcluded. - No attesters (
attested_count == 0):FinalizeWinnersis rejected.FinalizeNoAttestersrefund path is required.
- Single participant: managed via refund/special settlement flow.
- Missing reveals after deadline:
- If at least one reveal exists: finalization continues with reveal-included subset.
- If zero reveals exist: refund path is used.
- Winner cap:
kcannot exceedMAX_WINNERS.
Worked Example (Compact)
Assume sorted wallets A < B < C, tickets A=2, B=5, C=3, and reveal-included commitments HA, HB, HC.
- Compute
S0from lottery header fields. - Fold
A, thenB, thenCinto the seed chain to get finalS. - Round 0:
D0 = SHA256(0x494B494741495F5250445F56325F44524157 || S || 0),r0 = D0[0..16] % 10. - Walk ranges
A:[0,2), B:[2,7), C:[7,10)to pick winner. - Remove winner, recompute modulo over remaining tickets for round 1.
Anyone with the same on-chain inputs gets the same winners.
Audit Checklist
- Confirm canonical sort order by wallet bytes.
- Confirm reveal-included flags and rebuild eligible pool.
- Recompute seed chain from lottery fields +
poc_aggregate_hash+(wallet, tickets)tuples. - Recompute each round hash and modulo selection on remaining tickets.
- Recompute merkle root from
(index, recipient, amount).