Skip to content
Testnet
Docs

The program, in full.

The raffle program on Rialo testnet as bytes: the account, the five instructions, the hashes, the payouts and the refusals, and what the design does not guarantee.

What runs on chain

Drawbook’s raffle program runs on Rialo testnet at EQGb5xL2bgRuEFxY2FN25eFrgKDhTpZjRUbEQtYmoLLR. Deploying a raffle creates a real raffle account. Buying, revealing, drawing and claiming are real transactions, signed by a burner wallet that this browser generates and keeps.

It is testnet only. The coins are testnet RLO with no value, and Rialo can reset testnet, which would erase every raffle on it. The burner key and the secrets behind your tickets live in this browser’s storage, so clearing it loses both.

The program’s deployer can still upgrade it during the testnet period. Its upgrade authority is 8oM9XHmeYniw7T4vNFm8br9BfVvL6EfV2ryM81Dgs4M9, and an upgrade would replace the code at the same id, so what this page describes is the code deployed there now. Each raffle account also keeps its rent reserve after every claim is paid: about 0.0035 RLO for a raffle of 2 tickets, more for a longer account.

Drawbook’s first program, 74LNM1Hn6BCQpHyzHYkqrQP4H6N1At3CsiZ6CH4UsMG6, ran the same code and was retired on 8 October 2026, so that the raffle list could start empty. Its raffles are still on chain at their own addresses, but Drawbook no longer lists them, and a link to one says it is retired.

The five raffles numbered 1 to 5 are something else: fixed sample data, not on chain, kept to show the method. They are described at the end, under the sample raffles.

Program idEQGb5xL2bgRuEFxY2FN25eFrgKDhTpZjRUbEQtYmoLLR
NetworkRialo testnet
Unitsu64 kelvin, 1 RLO = 1,000,000,000 kelvin
Timesu64 milliseconds, from the chain clock
Tickets per raffle2 to 200
Sale lengthat most 31 days from Create
Reveal window1 minute to 7 days after the sale deadline
Draw sent byRialo at reveal deadline + 5 s, or anyone once ready
Upgradeableyes, by its deployer, during the testnet period
Integers in the accountlittle-endian unless marked

The raffle account

One account per raffle. The browser makes a fresh keypair for it, and in one transaction the System program creates the account, owned by the raffle program, and Create fills it in. The raffle key is thrown away afterwards: only the program can change the account or pay out of it. It holds the prize, the ticket money and the bonds, on top of its rent reserve.

Its length is 208 + 84 × supply bytes: a 208-byte header, then one 84-byte record per ticket.

  at  size  field              notes
   0     8  magic              "DRWBOOK1"; 0 = unset
   8     1  version            1
   9     1  status             0 open, 1 drawn, 2 void
  10     2  supply             u16
  12     2  winners            u16, as configured
  14     2  sold               u16
  16     2  revealed           u16
  18     2  effective_winners  u16, set at the draw
  20     4  last_reveal_slot   u32, slot of last Reveal
  24    32  creator            address
  56     8  prize              u64 kelvin, at Create
  64     8  ticket_price       u64 kelvin
  72     8  reveal_bond        u64 kelvin
  80     8  commit_deadline    u64 ms, sale closes
  88     8  reveal_deadline    u64 ms, reveals close
  96     8  created_at         u64 ms, chain clock
 104     8  drawn_at           u64 ms, 0 until draw
 112     8  chain_seed         u64, get_random_seed()
 120    32  seed               0 until the draw
 152     8  pool               u64 kelvin, at draw
 160     8  per_winner         u64 kelvin, at draw
 168     1  creator_refunded   u8, void raffles only
 169     7  reserved           zero
 176    32  title              UTF-8, zero padded
 208   84n  tickets            84 bytes per ticket

Ticket i starts at byte 208 + 84 × (i - 1). The sale closes at commit_deadline or when the last ticket sells, whichever comes first, and effective_winners is min(winners, revealed). last_reveal_slot is the low 32 bits of the slot the most recent Reveal landed in; Draw uses it to wait a block, as described there. Each ticket record:

  at  size  field       notes
   0    32  holder      zero while unsold
  32    32  commitment  see Commitment
  64    16  nonce       zero until revealed
  80     1  flags       1 revealed, 2 won, 4 paid
  81     1  reserved    zero
  82     2  win_rank    u16, 1-based; 0 if not won

Tickets are sold in order: the next ticket is always sold + 1.

Instructions

Instruction data starts with a one-byte tag. Every refusal is a custom program error whose code is in the table below, and the program logs the error’s name, so the page can say in words why the chain said no.

TagInstructionWho signs
0Createthe creator, and the new raffle key
1Buythe buyer
2Revealthe ticket's holder
3Drawanyone; Rialo’s Subscriber sends it as the creator
4Claimnobody needs to: it pays only the recorded owner

0 Create

data      tag 0
          ‖ prize u64 ‖ ticket_price u64
          ‖ reveal_bond u64 ‖ supply u16
          ‖ winners u16 ‖ commit_deadline u64
          ‖ reveal_deadline u64 ‖ title [32]
          = 77 bytes
accounts  0 creator         signer, writable
          1 raffle          writable
          2 system program

The system program is 11111111111111111111111111111111. Create is sent after a System CreateAccount in the same transaction (new account = the raffle key, rent-exempt kelvin for the length, owner = the program), signed by the creator and by the raffle key. Refused unless the account is the program’s, exactly the right length and unused; supply is 2 to 200; winners is 1 to supply; the ticket price is at least 1 kelvin; a full sale could not overflow a u64; and the deadlines sit inside these bounds:

now < commit_deadline
    <= now + 31 days
commit_deadline + 1 minute
    <= reveal_deadline
    <= commit_deadline + 7 days

The ceilings stop one unrevealed ticket from locking every kelvin in the account for years. The one-minute floor stops a creator who holds a ticket from closing reveals before anyone else can reveal and then collecting their bonds. Anything outside is refused with BadConfig. The prize then moves from the creator into the raffle account.

1 Buy

data      tag 1 ‖ ticket_index u16
          ‖ commitment [32]
          = 35 bytes
accounts  0 buyer           signer, writable
          1 raffle          writable
          2 system program

Refused unless the raffle is open, the commit deadline has not passed and a ticket is left. ticket_index must be exactly sold + 1, because the index is inside the commitment: a commitment made for the wrong ticket could never be revealed, so it is refused rather than left to strand a bond. The buyer pays ticket_price + reveal_bond into the raffle account, the holder and commitment are written, and sold goes up by one.

2 Reveal

data      tag 2 ‖ ticket_index u16
          ‖ nonce [16]
          = 19 bytes
accounts  0 holder          signer, writable
          1 raffle          writable

Accepted only after the sale has closed (the commit deadline passed, or every ticket sold) and before the reveal deadline, from the ticket’s holder, once per ticket, and only if the nonce hashes to the stored commitment. The nonce is stored, the revealed flag set, last_reveal_slot updated, and the bond goes straight back to the holder from the raffle account.

3 Draw

data      tag 3
          = 1 byte
accounts  0 caller          signer
          1 raffle          writable
          2 instructions    the instructions sysvar

Anyone may send it, and on a scheduled raffle Rialo’s Subscriber program sends it in the creator’s name five seconds after the reveal deadline (see the draw trigger). It is ready when the reveal deadline has passed, or when the sale has closed and every sold ticket is revealed; a raffle that sold nothing is ready as soon as its sale closes. With nothing revealed, the raffle becomes void and no money moves. Otherwise the program reads chain_seed from get_random_seed() at that moment, derives the seed and the winners as below, and writes the won flags, win_rank, chain_seed, seed, effective_winners, pool, per_winner and drawn_at. No kelvin moves at the draw; winners collect with Claim.

Two more rules stop a caller from shopping for a result. Draw must be sent directly by the transaction, not through another program, and must be the only instruction in it, or it is refused with DrawMustRunAlone; the third account is the instructions sysvar, Sysvar1nstructions1111111111111111111111111, which is how the program counts the instructions. Otherwise something in the same transaction could read the winners and make the whole transaction fail, and the caller could retry block after block until the draw suited them. And Draw is refused with TooSoon in the same block as the latest Reveal: every transaction in a block reads the same chain value, so a reveal sent alongside the draw could be decided after working out the result. One block later it cannot.

4 Claim

data      tag 4 ‖ ticket_index u16
          = 3 bytes
accounts  0 raffle          writable
          1 recipient       writable

No signature is required. The money can only go to the address recorded on the ticket, or to the creator, so anyone may push a payout. Several Claims can share one transaction, which is how the page collects everything a wallet is owed with one signature. What each case pays is under payouts and refunds.

Commitment

Buying ticket i publishes a digest, never the nonce. The preimage binds the nonce to this raffle account, this ticket and this holder, so a commitment cannot be lifted from one ticket and replayed on another. Every part is fixed-width bytes, joined with nothing between them:

commitment = sha256(
    "drawbook-v2|commit|"   19 bytes, ASCII
  ‖ raffle address          32 bytes
  ‖ ticket_index            u16 little-endian
  ‖ holder address          32 bytes
  ‖ nonce                   16 bytes
)

The nonce is 16 bytes from the browser’s CSPRNG. It is saved in this browser’s storage before Buy is sent, so a reload cannot lose it, and it leaves the browser only in Reveal. A ticket can therefore be revealed only from the browser that bought it. If another buyer takes the same ticket first, the program refuses with WrongTicket and the browser tries once more for the next one, with a fresh nonce.

Seed and winners

At the draw the revealed nonces are sorted and hashed together with the chain’s value. Sorting is what stops the order in which people revealed from changing the result.

seed = sha256(
    "drawbook-v2|seed|"     17 bytes, ASCII
  ‖ raffle address          32 bytes
  ‖ revealed nonces         16 bytes each, sorted
                            ascending bytewise
  ‖ chain_seed              u64 BIG-endian
)

chain_seed is the u64 that rialo_s_random_seed::get_random_seed() returns when Draw executes. The account stores it at offset 112 little-endian, like every other integer there; only inside this hash is it big-endian. It is mixed in, never relied on alone, and its limit is under what does not hold.

Winner selection

The eligible tickets are the revealed ticket indices in ascending order. Winners are drawn from them without replacement, by a keystream over the seed’s 32 raw bytes:

block(k) = sha256(seed ‖ "|draw|" ‖ k)
           k = 0, 1, 2, ... as ASCII decimal
sample   = next 6 bytes of the block, big-endian
           (5 per block; with fewer than 6 left,
            the next block is made)
pick     = sample mod (tickets left in the pool)
winner   = the ticket at position pick, counting
           from 0; it is removed and the rest keep
           their order
win_rank = 1, 2, 3, ... in the order drawn

The number drawn is effective_winners = min(winners, revealed), so a raffle set for five winners in which one ticket was revealed has one winner, not five. Removing the pick and keeping the rest in order is Fisher and Yates’ original strike-out method. The later swap-based variant gives different winners from the same seed, so a recomputation has to remove, not swap. The program does exactly what deriveWinnerTrace in lib/raffle.ts does, which is how the page can check an on-chain draw with the same code.

Payouts and refunds

pool       = prize
           + sold × ticket_price
           + (sold - revealed) × reveal_bond
per_winner = pool / effective_winners   (floor)
remainder  = pool - per_winner × effective_winners

A revealed ticket’s bond came back at Reveal. An unrevealed ticket’s bond stays in the account and joins the pool. Once drawn, the account holds its rent reserve plus exactly the pool, and once every winner has claimed it is back to its rent reserve.

That reserve stays behind. The program has no instruction that closes a raffle account, so the rent-exempt minimum the creator paid at Create, about 0.0035 RLO for 2 tickets, is still in the account after every claim, drawn or void. It is not part of any prize or refund.

  • Drawn. A winning ticket not yet paid pays per_winner to its holder, and the ticket with win_rank 1 also gets the remainder. The ticket is then marked paid.
  • Void, ticket 0. The prize goes back to the creator, once.
  • Void, ticket 1 or more. ticket_price + reveal_bond goes back to that ticket’s holder, once.

A raffle is void only when nobody revealed, so on a void raffle nobody forfeits anything: every bond comes back along with the ticket money.

Error codes

Each refusal is ProgramError::Custom(code), and the transaction log carries the line Drawbook refused: Name.

CodeRefusal
1BadInstructionunknown tag or wrong data length
2BadAccountwrong owner, wrong size, a missing signer or writable flag, or an account in the wrong place
3AlreadyInitialisedCreate on an account that is already a raffle
4NotInitialisedthe account is not a raffle
5BadConfigsupply, winners, price or deadlines out of range, or a sale total that would overflow
6SaleClosedBuy after the commit deadline, when sold out, or on a settled raffle
7WrongTicketthe Buy index is not sold + 1, or a ticket index is out of range
8RevealsNotOpenReveal before the sale closed
9RevealsClosedReveal at or after the reveal deadline, or on a settled raffle
10NotHolderthe signer or recipient is not the ticket's holder, or not the creator
11AlreadyRevealedthat ticket is already revealed
12CommitmentMismatchthe nonce does not hash to the commitment
13NotReadyDraw before it is ready
14AlreadySettledDraw on a raffle that is already drawn or void
15NothingToClaimnot a winner, already paid, or the raffle is not settled
16Arithmetican overflow, or an account would go below zero
17DrawMustRunAloneDraw sent through another program, alongside other instructions, or without the instructions sysvar
18TooSoonDraw in the same block as the latest Reveal; it works one block later

Checking a draw

On a live raffle, everything needed is in the account once it is drawn: every revealed nonce, chain_seed, the stored seed and each winner’s win_rank. The raffle page’s recompute button rebuilds the seed from those bytes, re-runs the selection, and compares both with what the program wrote. That is a real check that can fail: if the account disagreed with its own public data, the page would say so.

To do it yourself, read the account with getAccountInfo, take the nonces of the tickets whose revealed flag is set, hash them as in seed and winners, run the keystream, and compare the result with the tickets whose won flag is set, in win_rank order.

What a recomputation proves is the arithmetic: that the winners follow from the published nonces and chain_seed. It cannot show that chain_seed itself was not chosen. On the sample raffles, recomputing re-runs the same code that made the record, so there it shows the method and cannot fail.

What does not hold

Note

The draw is randomized and checkable. It is not a proof that nobody influenced it, and the two ways to influence it are stated here rather than left out.

  • The draw block. chain_seed is read when Draw executes, after every nonce is public. Whoever produces that block may be able to influence what get_random_seed() returns, and if they can, they can try candidate values and keep one that suits them, with no ticket and without revealing last. Rialo publishes no proof of where the value comes from, so the program cannot close this, whether a person or Rialo’s Subscriber sends the Draw. An ordinary caller has no such lever: Draw runs alone in its transaction and at least one block after the latest reveal, so nothing alongside it can read the result and back out.
  • Withholding a reveal. A holder who has seen the other revealed nonces can choose not to reveal, which changes the seed. It costs their bond, which joins the pool, and it is blind, because chain_seed is not known until the draw. The bond is a fixed amount the creator sets, not a penalty sized to the prize; in the sample raffles it is under 1% of the pool.

The draw trigger

The sale closes through the program’s own checks, at the commit deadline or the moment the last ticket sells, because Buy refuses after either; that needs no trigger. The draw does, and Rialo sends it. Create goes out with a third instruction, a Subscribe to Rialo’s Subscriber program signed by the creator, which registers a one-shot clock subscription whose only action is this raffle’s Draw. Five seconds after the reveal deadline, Rialo sends that Draw by itself, in the creator’s name, with nobody pressing anything. Anyone can still send Draw sooner, once every sold ticket is revealed.

Subscribe, the 3rd instruction of Create
program   Subscriber111111111111111111111111111111111
accounts  0 creator        signer, writable; pays
          1 subscription   writable
          2 system program
nonce     first 32 characters of the raffle
          address, as UTF-8 bytes
address   PDA ["rialo_subscribe", creator, nonce]
          of the Subscriber program
data      u32 0 (Subscribe) ‖ nonce [32]
          ‖ subscriber = creator
          ‖ topic "clock", event account
            SysvarC1ock11111111111111111111111111111111
          ‖ time from reveal_deadline + 5000 ms
            to u64::MAX
          ‖ one action: Draw on this raffle
            (creator, raffle, instructions
             sysvar; data 03)
          ‖ OneShot ‖ commits 0 to u64::MAX
          = 310 bytes, legacy bincode, LE

The byte offsets are in program/SPEC.md. The action carries no Subscriber Destroy, although the crate’s own helper appends one to every one-shot: with it, the transaction Rialo sends would hold two instructions and Draw would refuse it with DrawMustRunAlone. Without it, the triggered transaction is byte for byte the Draw anyone could send by hand, so both of Draw’s guards apply to it unchanged and nothing in the program had to change.

Five seconds, not zero, because a triggered transaction reads a clock slightly behind the block that matched it. On a local network a window opening exactly at the deadline was refused as NotReady, and a one-shot is never sent again. On testnet the Draw recorded a drawn_at 23 ms before its window opened, still 4,977 ms after the deadline.

It has run on testnet. On 8 October 2026 raffle AtVDa7C3qhFpjivN7vaqgsXP8p2UzKbSd6Z1zfqZmE9w, on the first program, with 2 tickets and a one-minute reveal window, was drawn by Rialo 5.1 seconds after its reveal deadline: one transaction, Draw alone, the creator its only signer and fee payer, 4,411 compute units. Nobody pressed Draw, and recomputing the result matched. getTriggeredTransactions on the subscription account lists that one transaction.

Schedule deposit2,797,920 kelvin, about 0.0028 RLO
Held asrent on a 274-byte account
Given backat reclaim, less a 5,000 kelvin fee
Fee when Rialo sends Draw5,000 kelvin, from the creator
Sentonce, at reveal deadline + 5 s

The creator pays for it. The deposit is the subscription account’s rent; the raffle page offers it back to the creator once the raffle is settled, as one transaction of Destroy and Unsubscribe. Rialo charges the firing fee whether Draw succeeds or not.

  • Drawn by hand first. Rialo’s later Draw is refused as AlreadySettled and changes nothing; the creator still pays its fee, unless they reclaimed the deposit before it was due, which cancels it.
  • Rialo refuses the schedule. If a later Rialo build rejects the Subscribe, the browser creates the raffle again without it and says the draw is not scheduled. No deposit is taken, and Draw is a button.
  • It does not arrive. A one-shot is not sent twice. Thirty seconds after its time, the raffle page brings the Draw button back for anyone to press.
  • A raffle without a schedule. If Rialo refuses the Subscribe, the raffle is deployed without it and has no subscription. Its page says the draw is not scheduled, and it is a button anyone can press once the raffle is ready.
  • Not yet observed. Whether a subscription survives weeks of waiting across node restarts or upgrades, and what happens if the creator cannot pay the fee when it is sent. The Draw button covers both.

The page calls a raffle scheduled only after decoding its subscription account and finding exactly this form, and asking the node whether it still holds it; anything else is shown as not counted, and Draw stays a button. The Subscriber program is closed source, so all of this is behaviour observed on Rialo’s 0.21.0-alpha.0 build, not documented.

Rialo cannot send the draw the moment the last holder reveals. That would need an event topic, which the program does not emit, so an early draw is somebody pressing Draw. Predicates carry no value comparison either: they hold a topic, an optional event account and an optional timestamp range, and nothing else. There is no interval or calendar scheduling, so a recurring schedule has to re-arm itself with a fresh nonce each time.

The sample raffles

Raffles 1 to 5 are fixed demonstration data, built into the site and not on chain. Their clock starts at 30 Jul 2026 and ticks from there, and nothing pressed on them is signed or sent. The commitments are genuine SHA-256 digests of the listed nonces, and the settled sample was settled by running the real draw code.

They use the earlier v1 formulas, which hash text where the program hashes fixed-width bytes. The same nonce is text in the commitment and raw bytes in the seed:

commitment_v1 = sha256( UTF-8 text of
  "rialo-raffle-v1|commit|" raffleId "|"
  ticketIndex "|" holder "|" nonce )

  raffleId, ticketIndex   decimal
  holder                  base58
  nonce                   32 lowercase hex chars

seed_v1 = sha256(
    UTF-8 "rialo-raffle-v1|seed|" raffleId "|"
  ‖ each revealed nonce, sorted:
      16 raw bytes ‖ UTF-8 "|"
  ‖ chainSeed: 8 raw bytes, in the order
      its hex reads
)

Winner selection is the same keystream as above. Recomputing a sample re-runs the code that produced its record, so it demonstrates the method and cannot fail; only a live raffle gives the check something independent to disagree with.

What is tested

pnpm verify runs three suites offline, with nothing sent to any network:

ChecksSuite and what it asserts
20scripts/verify-sha256.tsSHA-256 against the FIPS 180-4 vectors, including a million “a”, plus 400 random cross-checks against node:crypto.
75scripts/verify-raffle.tsThe draw in lib/raffle.ts: determinism, distinct winners, independence from reveal order, the winner cap, payouts summing to the pool for 1, 2, 3 and 7 winners, an audit that rejects a tampered winner list, uniformity, and the sample data.
224scripts/verify-chain.tsThe browser library: transactions byte for byte against @rialo/ts-cdk fixtures, the v2 hashes against node:crypto, account decoding, a draw check that fails on altered data, payouts and refusals as the spec states them, and the schedule: the Subscribe bytes against the official Subscriber crate and a CLI transaction, the check that only the exact form counts as scheduled, and the states the raffle page shows.

Uniformity is a chi-square test over fresh random seeds: one winner of 20 across 60,000 seeds, and five winners of 20 across 20,000. The statistic changes on every run, so no single value is quoted here; the test passes when it stays under the critical value of 43.82 (19 degrees of freedom, p = 0.001).

Against a running network, program/tests/e2e.mjs drives the deployed program through a full raffle, a partial reveal, a void raffle, deliberate refusals checked for their codes (the Draw guards among them) and a 200-ticket worst case, checking balances to the kelvin and recomputing every draw in JavaScript. scripts/smoke-chain.ts does the same through the browser library. Both run against a local network; neither will ask a public faucet for funds. scripts/auto-draw-check.ts creates a scheduled raffle, buys and reveals two tickets, presses nothing, and checks that Rialo’s Draw arrives alone, runs, and recomputes to the same winners, then reclaims the deposit. It pays from a funded wallet file, never a public faucet; an adaptation of it that reads the browser’s key form passed all 39 of its checks on testnet on 8 October 2026.

Network reference

Established by probing the node directly. The published documentation lists three different devnet endpoints and one of them does not resolve, so treat these as observed values rather than documented ones.

Testnet RPChttps://testnet.rialo.io:4101
Subscriber programSubscriber1111… sends the scheduled Draw
SchedulesgetSubscription{subscriber, nonce}, getTriggeredTransactions[account, limit]
FaucetrequestAirdrop{pubkey, kelvins}, max 1 RLO per call, rate limited per IP
BalancegetBalance{address}, in kelvin
Raffle accountsgetAccountsByOwner, by program id
Clockunix_timestamp in milliseconds
Replay protectionconfigHashPrefix and validFrom, not a blockhash
Pubsubnone among the 39 methods, so state is polled
CORSaccess-control-allow-origin: *

Rialo is not Solana-RPC compatible. Parameters are a single-element array holding a struct, there is no getLatestBlockhash, and the Wallet Standard namespace is rialo:*, so Solana wallets are not discovered. A configHashPrefix is a u64 and has to be read from the raw response text: JSON.parse rounds it, and the node then rejects the transaction.