Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

speed package

Overview

Package speed is the SPLASH HOURLY ticket draw: a draw every hour instead of every week.

WHY IT IS A SEPARATE REALM AND NOT A PARAMETER ON r/splash/arcade/raffle. The weekly raffle's cadence is woven into eight constants, four build guards and the refund grace window, and every one of them had to be re-derived from first principles here (see "the two clocks" below). A shared realm with a cadence dial would have had to satisfy BOTH sets of constraints simultaneously, which is how a margin gets shaved: the hourly window's pin offset is 2400 rounds and the weekly one's is 230400, and no single guard can assert both. Two realms, each with its own derivation and its own asserted margins, is the honest shape. The mechanics below are the raffle's, deliberately: the ticket index, the Commit/Capture/Resolve three-phase cycle, the FROZEN-TOTAL property, the one-way refund exit, and the pin-at-record-creation rule. Read r/splash/arcade/raffle/AUDIT-2026-09-28.md before changing any of them; this file's job was to re-scale the timing without re-introducing one of its fourteen findings.

========================================================================== V1 IS SELF-CONTAINED: THERE IS NO SPONSORSHIP AND THERE IS NO FundPot. ==========================================================================

The pot is ENTRY MONEY ONLY. Nothing outside this realm can put a coin into a window's pot, and this realm calls no other realm's code except r/gnoland/wugnot (to move money) and r/splash/beacon (to read a round). There is no cross-realm feed in either direction, on purpose, in both directions:

  • NOTHING FEEDS THIS POT. r/splash/arcade/duel's rake goes to the weekly raffle and stays there. Adding a second consumer of that rake would make the duel's settlement depend on two realms' bookkeeping instead of one, and the duel's Collect depending on the raffle's bookkeeping was AUDIT-2026-09-28.md finding 14 — the worst liveness defect of the day, in which any raffle panic trapped a resolved duel's whole pool in both realms. It was severed that evening (Collect books, FlushPot delivers) and this realm does not reinstate the shape.
  • THIS REALM FEEDS NOTHING. No rake, no share, no callback. Its only outbound transfers are wugnot to a winner's wallet, wugnot to a refunding buyer's wallet, and wugnot to opsVaultAddr.

AND THE PROPERTY THAT MAKES THE ABSENCE OF SPONSORSHIP A SECURITY FEATURE RATHER THAN A MISSING ONE — THE SOLE-ENTRANT PROPERTY:

Example
 1WITH NO HOUSE MONEY IN THE POT, A SOLE ENTRANT MERELY WINS BACK THEIR OWN STAKE
 2MINUS THE HOUSE CUT. The weekly raffle still has the sole-entrant attack, and it
 3is a real one there: the duel's rake is sponsored into the live week's pot, so
 4somebody who buys one ticket into a quiet week with a large sponsored balance
 5collects the sponsorship for the price of the fee on that ticket. Here Pot is
 6exactly sum(spendOf) by construction, so a sole entrant's pot IS their own
 7post-cut spend: they pay 100,000 ugnot, and if they are the only entrant they win
 895,000 back. THERE IS NO POSITIVE-EXPECTATION SOLO PLAY, so the attack cannot
 9exist — not mitigated, not made expensive, ABSENT, as an arithmetic consequence
10of the pot's definition.
11
12(A SOLE ENTRANT IS NO LONGER INDIFFERENT BETWEEN Resolve AND Refund, AND THIS
13PARAGRAPH SAID THEY WERE. It read: "The same arithmetic makes a sole entrant
14indifferent between Resolve and Refund: both return exactly their own 95,000."
15Since 2026-09-29 the fee is ESCROWED per window and earned only by a draw
16(AUDIT-2026-09-28.md finding 25), so a refund returns the whole 100,000 and a
17resolve returns 95,000: THE SOLE ENTRANT STRICTLY PREFERS THE DRAW TO FAIL, by
18exactly the fee. The conclusion this paragraph was supporting is unaffected —
19solo play is still never positive-expectation, which is what kills the
20sole-entrant attack — but the incentive it describes is now the general one the
21owner is weighing: with a refund at 100% of stake, every non-winner prefers a
22failed window to a drawn one. That is a product decision and is deliberately NOT
23fixed here; it is recorded in AUDIT-2026-09-28.md so that the next reader of this
24paragraph does not mistake it for a property of this realm's design. The refund
25grace window below is therefore not only a multi-entrant concern any more.)

SO DO NOT ADD FundPot, A SPONSORED FIELD, A ROLLOVER OR A JACKPOT WITHOUT RE-AUDITING THIS PARAGRAPH. Every one of them puts money in the pot that belongs to no entrant, which (a) resurrects the sole-entrant attack, (b) turns the invariant Pot == sum(spendOf) into Pot == sum(spendOf) + Sponsored with everything that follows from it (see the raffle's Draw.Sponsored, Refund's sponsorship transition and SweepDrawlessWeek — three separate mechanisms that exist only because house money belongs to nobody), and (c) makes this realm's liveness someone else's problem again.

========================================================================== THE ONE-HOUR CADENCE, AND THE TWO CLOCKS ==========================================================================

Every duration in the weekly raffle shrinks by roughly 168x here, and THE FAILURE MODE OF GETTING ONE WRONG IS SILENT — it is not a panic, it is a draw whose winning randomness was public on api.drand.sh before entries closed (finding 10). So each constant below is derived in place, and each is asserted by a build guard that obeys the doctrine that finding 11 cost three attempts to learn:

Example
1A GUARD THAT PROTECTS A REAL-WORLD DURATION MUST HAVE SECONDS ON BOTH SIDES,
2SOURCED FROM TWO DIFFERENT CLOCKS, OR IT CANNOT FAIL.

The two clocks available to a realm on this toolchain, neither of which it can measure at runtime (there is no block-timestamp getter):

  • DRAND'S PERIOD: beacon.PeriodSeconds = 3 s, EXACT. A round count multiplied by it is real seconds by definition of the beacon.
  • THE CHAIN'S BLOCK TIME: assumed 86400/blocksPerDay = 3.29998 s, written as the integer 33/10 in guards because 3.3 is not an integer constant. MEASURED ON LIVE gno.land MAINNET ON 2026-09-28: 3.306 s/block over 200,000 blocks and 3.311 s over 20,000. So reality is very slightly SLOWER than this file assumes, by about 0.2%. That is the benign direction for every margin here (a slower chain means the beacon's estimate LAGS drand rather than outrunning it, and lagging is correctable by any relay of drand's head — see speedMaxStaleBlocks), but the guards below are stated against a ±10% band rather than against the measurement, because the measurement is of the past.

Nothing in this realm is a timestamp. A window is a sequence number derived from chain height; no ticket, spend or draw is dated.

Function

Commit

func Commit(cur realm, window int64) int64
Ask AI Claude ChatGPT

Commit commits an ALREADY-FINISHED window to its pinned beacon round. Returns that round.

THE WINDOW IS A PARAMETER AND IT MUST BE OVER — window < currentWindow(). Both halves are load-bearing, and each closes a defect the raffle's first version had:

  • IT TAKES A WINDOW, so any past window can be committed at any time, by anyone. A window whose clock ran out before anybody called it would otherwise be unreachable for ever — Resolve and Refund both require Committed — so its whole pot would be lost while heldPots went on counting it and both solvency checks stayed green over money nobody could ever get back. That is AUDIT-2026-09-28.md finding 1, and at an hourly cadence it would happen 24 times a day rather than once a week: MOST WINDOWS OF A QUIET DAY WILL BE COMMITTED LATE BY SOMEBODY WHO WANTS THEIR MONEY, and that path has to work.
  • THE LIVE WINDOW CAN NEVER BE COMMITTED, so a commit can no longer cut a window's sales short. Otherwise anyone could Buy(1) and immediately Commit: that makes them the sole owner of a FrozenTotal of 1 — the guaranteed winner — and, because Buy refuses a committed window, shuts the window's sales for the rest of its hour. That is AUDIT-2026-09-28.md finding 2. Note that the PRIZE half of that attack is already dead here by the sole-entrant property (winning a one-ticket window returns the attacker's own 95,000 ugnot out of the 100,000 they staked), so what remains is pure griefing — and refusing the live window closes it at the root anyway, rather than with a minimum-tickets or minimum-elapsed heuristic. THIS USED TO SAY THE GRIEFER'S WIN "costs them the 5,000 cut", AND IT COSTS THEM NOTHING NOW: since finding 25 the fee is escrowed and earned only by a draw, so the griefer who DOES NOT resolve pays nothing at all and reclaims the whole 100,000 through Refund. The reason the attack is dead is the refusal below, not a 5,000 ugnot price on it. AUDIT-2026-09-28.md finding 29.

PERMISSIONLESS: there is no keeper and no admin. Anyone who wants the draw to happen — every ticket holder, for a start — can call it once the window is up.

It freezes two numbers and CHOOSES NOTHING:

  • FrozenTotal = the window's final ticket total. The window is over and Buy only ever sells into currentWindow(), so this total can no longer grow BY CONSTRUCTION; it is still assigned explicitly, because Resolve reads this field and not the live total.
  • CommittedAt, the height the refund grace window counts from.
  • AND IT USES Draw.Round EXACTLY AS PINNED, which was written when this window's record was created and is not this function's business.

THE SECURITY RULE, AND THE ONLY REASON THIS IS THREE PHASES: the ticket set must be frozen BEFORE the randomness is knowable. If the total were read at resolve time, anybody who saw the published round could compute the winning unit and then buy precisely enough tickets to own it — which turns a draw into a purchase.

THERE IS DELIBERATELY NO "THE ROUND MUST STILL BE IN THE FUTURE" CHECK HERE. A reader who expects one has the pin backwards. A window is committed at least an hour after its round was pinned, so by now the pinned round MAY LEGITIMATELY ALREADY BE SIGNED AND RELAYED — that is the ordinary case for a window whose record opened early in its hour, and it is harmless precisely because nobody CHOSE it. Requiring it to be unpublished would refuse the honest case and, worse, would reintroduce round selection through the back door, since the only way to satisfy such a check would be to recompute the round here. The int64 wrap case lives where the arithmetic is, at maxPinnableLatest.

WHY FREEZING THE TOTAL ALSO FREEZES WHO OWNS UNIT k — the part r/splash/game got wrong first time, and the reason Buy appends:

  • Every purchase APPENDS A NEW ticket index (appendTickets). No index's value is ever increased afterwards, and fenwick.Add is not called from this realm at all.
  • An appended index therefore starts at a prefix equal to the whole total at the moment it was appended, so anything appended after Commit lies at or beyond FrozenTotal — outside the frozen range by construction, not by convention.
  • And because no earlier index's value changes, no earlier prefix moves: the owner of every unit below FrozenTotal is fixed from the instant that unit was sold.

WITH ONE INDEX PER BUYER INSTEAD, a further purchase would grow that buyer's existing value, widening their range IN PLACE, from its start, inside the frozen range, and pushing every later buyer along. The earliest buyer (prefix 0 for ever) could then wait for the round to publish, compute k = randomness mod FrozenTotal, buy until their own value exceeded k, and call Resolve to take the pot: a free option on the whole pot. That exploit was real in r/splash/game.

Panics if the window is not over yet, if it is already committed, or if it has NO TICKETS AT ALL. The last is not a formality: there is no rollover, so a drawless window must never be allowed to exist — there is no unit to draw and no owner to pay. It reads no beacon state at all; the "beacon must exist and be fresh" refusal lives at the pin, which is to say in Buy, where a sale is refused rather than accepted into a window that could never be drawn.

Param

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed" -func "Commit" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed" -func "Commit" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx