Demosino
Draw calculator
Replay the weighted tile-elimination draw from a minor seed, draw block hash, and final ticket CSV.
Slow public randomness with a suspense reveal
The draw is deliberately split into two jobs. First, Luckotto commits to the exact final ticket list and minor seed hash before the random block exists. Second, once a future Bitcoin block supplies the public chain entropy and the minor seed is revealed, the winner is revealed through intentionally slow tile eliminations that anyone can replay from public data.
Threat Model And Rationale
A normal verifier does not need hidden server state. They need the final CSV bytes, the committed CSV hash, the commitment transaction, the committed minor seed hash, the revealed minor seed, the commitment block height, and the draw block hash at commitment_block_height + 2. The operator cannot change the ticket list after seeing that later block without breaking the CSV hash commitment.
The remaining randomness attack worth designing around is block withholding. If checking a candidate block were instant, a miner with a ticket could find a block, privately run the draw, publish favorable blocks, and discard unfavorable ones. The slow SHA-256 chain raises the cost of that decision. The miner must spend scarce time after finding a candidate block before knowing whether withholding is useful, while the rest of the network may find and publish a competing block.
Why The Reveal Is Incremental
A slow draw creates a UX problem: a direct one-shot draw would leave the page silent until the full computation finished. Luckotto instead uses the slow work to eliminate losing tiles. Each eliminated tile is a real part of the settlement calculation. The live ticket set stays as large as the odds allow until the process leaves exactly one public ticket.
The incremental reveal is therefore not decorative. It is the public shape of the draw algorithm: the same slow eliminations that create suspense are the eliminations that determine the winner.
Weight Invariant
Each ticket's weight is its fair value: the confirmed sats credited to that ticket minus the partner's posted fee, exactly as committed in the round CSV. On-time confirmations credit the selected ticket; late confirmations roll forward before the next unlocked round's CSV is committed. Ticket IDs, partner IDs, CSV order, and player metadata are public identity data; they do not add probability. At every step, the live candidate set only contains tickets that have not lost a tile, and every next-elimination branch is weighted by the draw weight behind the tickets that would remain if that tile were eliminated.
seed = hex(bytes(minor_seed) || bytes(draw_block_hash)) tickets = committed_csv_rows_with_positive_weight eliminated = [] candidates = tickets while count(unique_tile_sets(candidates)) > 1: choices = next_elimination_choices(candidates, eliminated) digest = sha256_chain(seed, position, counter, hashes_per_elimination) tile = rejection_sample(digest, choices) eliminated.append(tile) candidates = tickets in candidates not containing eliminated tiles winner = weighted_pick_by_ticket_id(candidates) if count(candidates) > 1 else candidates[0] draw_tiles = winner.tiles
Rejection sampling removes modulo bias: a 256-bit digest is accepted only if it falls inside the largest exact multiple of the current total choice weight. If it falls outside that range, the calculator increments the counter and runs another slow chain. If duplicate tickets share the final tile set, the next chain sample picks one ticket UUID by weight.
The calculator is not a second protocol.
The draw calculator runs the same public replay path as the round auditor: final CSV bytes, minor seed, draw block hash, the fixed hash count, and the canonical draw core. It is useful for inspecting intermediate state, but the byte-level contract lives in the technical specification. A separate implementation should reproduce the spec vectors, then replay a real round using the same CSV and block hash shown here.
1. Parse committed CSV rows in file order. 2. Drop zero-weight tickets; reject duplicate ids. 3. Seed one SHA-256 chain with '<draw_seed_hex>:draw:chain'. 4. For each elimination, advance the same chain draw_hashes_per_elimination steps. 5. Rejection-sample the digest across weighted elimination choices. 6. Remove tickets containing the eliminated tile. 7. Stop when one tile set remains; if multiple tickets remain, weighted-sample by ticketId.