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.

beacon source realm

Package beacon is SPLASH's on-chain record of the drand evmnet randomness beacon.

Overview

Package beacon is SPLASH's on-chain record of the drand evmnet randomness beacon.

WHAT CHANGED, AND WHY IT IS NOT A REFACTOR (2026-09-28). This realm used to verify signatures from a beacon SPLASH itself ran: one compiled-in BLS key, held by this project, publishing a round every five minutes. The key holder could sign ANY round at will and therefore knew every future round's randomness permanently, which is r/splash/arcade/raffle/AUDIT-2026-09-28.md findings 4 and 10 and was the single worst property of the product. It now verifies drand evmnet, a threshold beacon this project does not control. Two consequences run through every comment below:

  • NOBODY CAN SIGN A FUTURE ROUND. Round r's signature does not exist before its time, and no party below the DKG threshold can make it exist. That is what the games' round bindings are for and it is now true rather than assumed.
  • NOBODY CAN DESTROY A PAST ROUND. Every round from 1 to the head is served forever by at least three independent relays, and Publish is permissionless, so anything deleted here is re-creatable by anyone. "The randomness is lost" stopped being a reachable state, which is why Prune, PruneWindow and the raffle's resolvable() are gone -- see DRAND-MIGRATION.md section 3.

THIS REALM IS NOT A MIRROR OF DRAND AND MUST NEVER BECOME ONE. At a three-second cadence, keeping up would mean one Publish every three seconds at 16,970,375 gas each: 0.62% of EVERY BLOCK on the chain, for ever, for data that is read about fifty times a year (DRAND-MIGRATION.md sections 2 and 9.5 have the measured arithmetic; 9.5 also records that this said 2.4% until 2026-09-30, off by the same 4.4x as every other gas figure derived from the pre-optimisation VerifyDrand, and that at 0.62% the STORAGE cost rather than the gas share is what rules mirroring out). It is an ON-DEMAND RECORD: a round is stored when something on this chain has a reason to look at it, and nothing else is stored at all.

There is no owner, no pause and no upgrade path. Once a round is published its randomness is fixed for ever.

Constants 2

const ChainHash, PeriodSeconds, RoundsPerDay

 1const (
 2	// ChainHash names the drand chain these signatures belong to.
 3	ChainHash = bls254.DrandEvmnetChainHash
 4
 5	// PeriodSeconds is drand evmnet's cadence: one round every three seconds.
 6	//
 7	// ANY REALM EXPRESSING A DURATION IN ROUNDS MUST MULTIPLY BY THIS AND COMPARE
 8	// AGAINST SECONDS. A round count is not a duration. The five-minute beacon
 9	// this replaces made every round-denominated window in the repository 100x
10	// too short at a stroke, and a guard comparing rounds to rounds cannot see
11	// it: the cadence appears on both sides and cancels. See
12	// DRAND-MIGRATION.md section 6, and r/splash/arcade/raffle's seconds guard,
13	// which is the one assertion in the system that would have caught it.
14	PeriodSeconds = bls254.DrandEvmnetPeriodSeconds
15
16	// RoundsPerDay is 86400 / PeriodSeconds = 28800. Exported because the raffle
17	// and the duel both need it and both used to restate it as 288 -- the same
18	// number in two files, wrong in both.
19	RoundsPerDay = 86400 / PeriodSeconds
20)
source

ChainHash, PeriodSeconds and RoundsPerDay are re-exported from p/splash/bls254, which holds the ONE copy of drand evmnet's identity. They are re-exported rather than restated so that a realm needing the cadence does not have to import a crypto package to get it, and so that there is still exactly one place the numbers are written down.

const MaxEstimateStaleBlocks

1const MaxEstimateStaleBlocks = maxEstimateStaleBlocks
source

MaxEstimateStaleBlocks is maxEstimateStaleBlocks, exported so that a consumer can state its own tolerance as a fraction of the backstop, assert a relation to it at COMPILE TIME (r/splash/arcade/raffle does), or tell a user how fresh the beacon has to be. A const, so no caller can move it.

Functions 12

func AnchorHeight

Action
1func AnchorHeight() int64
source

AnchorHeight is the chain height of the last relay that raised the estimate's anchor. Panics if no round has ever been published. See anchorHeight and StaleBlocks.

func EstimateIsUsable

Action
1func EstimateIsUsable() bool
source

EstimateIsUsable reports whether EstimatedLatestRound would return rather than panic. A read, so a caller can check before committing to a transaction that would revert.

func EstimatedLatestRound

Action
1func EstimatedLatestRound() int64
source

EstimatedLatestRound is this realm's best estimate of the round drand is currently on. Panics if no round has ever been published.

IT IS AN ESTIMATE AND THE NAME SAYS SO. There is no way to know the true current round on-chain: proving "round r is not signed yet" needs an UPPER bound on elapsed real time, published rounds give only a LOWER bound, and chain height gives none at all because a halted chain advances no height while drand advances 28800 rounds a day. Every round-number pin in this system is therefore an assumption about this chain keeping pace with drand, and this function is where that assumption lives instead of being spread across three realms.

THE CONSTRUCTION: take anchorScaled — the highest round-versus-height intercept any relay has ever evidenced — and read this height off that line. It is exactly latestRound at the moment latestRound is relayed. READ anchorScaled BEFORE CHANGING THIS: extrapolating from the highest round's own publication height instead is the obvious simplification, and it is exploitable.

WHY IT IS STRICTLY BETTER THAN Latest() FOR A PIN. A consumer pinning Latest() + offset measures its offset from whenever somebody last happened to relay something, so the pin drifts backwards in real time as the beacon goes quiet — and the raffle's offset is exactly one day larger than a week, so a single day of staleness puts the pinned round's randomness on the public API BEFORE the week's ticket sales close, which is finding 10's last-buyer attack verbatim. The correction removes that failure entirely in the honest case.

THE ERROR IS BOUNDED BY REFUSAL, AND THAT IS NEW AS OF THE STALENESS BOUND. The conversion assumes A = 86400/blocksPerDay = 3.29998 s blocks, so its error is S*(A - B) seconds of drand time for S blocks since the anchor was last raised and a real average block time B — 2.4 hours at one day of S and a 10% drift in B, NOT the ~14 minutes this comment claimed until the figure was re-derived (that number divided by the period twice; see maxEstimateStaleBlocks and AUDIT-2026-09-28.md finding 13). Unbounded S makes it unbounded in BOTH directions, so this function now REFUSES above maxEstimateStaleBlocks rather than returning a number nobody can put a bound on. What the refusal costs, and why it is nevertheless right, is at maxEstimateStaleBlocks and in AUDIT-2026-09-28.md finding 12.

MONOTONE IN HEIGHT AND NEVER BELOW latestRound: ChainHeight() only rises and anchorHeight/anchorScaled never move except upward, so the correction term is never negative.

func EstimatedLatestRoundWithin

Action
1func EstimatedLatestRoundWithin(maxStaleBlocks int64) int64
source

EstimatedLatestRoundWithin is EstimatedLatestRound with the caller's own staleness tolerance, in blocks, for a consumer whose real-time margin is finer than the backstop's. r/splash/arcade/duel uses it: its bound round is 300 SECONDS out, and the backstop admits 2.4 hours of error.

IT CAN ONLY EVER ASK FOR LESS. A tolerance above maxEstimateStaleBlocks is refused, so this is not a way around the backstop — a consumer can tighten the bound and cannot loosen it. Read maxEstimateStaleBlocks for why the two-level shape exists at all.

Panics if no round has ever been published, if the tolerance is not positive, if it exceeds the backstop, or if the beacon is staler than the tolerance allows.

func Latest

Action
1func Latest() (int64, string)
source

Latest returns the highest published round and its randomness. Panics if no round has been published yet.

"HIGHEST PUBLISHED", WHICH IS NOT "CURRENT". Under an on-demand record nothing relays routinely, so this can be arbitrarily stale — a week old, a year old. A CONSUMER PINNING A FUTURE ROUND MUST NOT USE THIS AS "now": use EstimatedLatestRound, which corrects for the staleness using a clock no relayer controls. Reading this as the current round is exactly how the raffle's round pin would silently re-open AUDIT-2026-09-28.md finding 10.

func LatestPublishedAtHeight

Action
1func LatestPublishedAtHeight() int64
source

LatestPublishedAtHeight returns the chain height at which the highest published round was relayed. Panics if no round has been published yet.

func Publish

crossing Action
1func Publish(cur realm, round int64, sigHex string)
source

Publish verifies sig for round against drand evmnet's group key and stores it. PERMISSIONLESS: anyone may relay any round, because validity is what matters and not the sender. Panics if the round is already published or the signature does not verify — including if sigHex is not valid hex, which is just another shape of "not a valid signature".

THE ROUND NUMBER IS THE MESSAGE and the caller supplies nothing else: the signed digest is keccak256 of the round as a big-endian uint64 (bls254.DrandRoundMessage), so a relayer cannot choose what a round's randomness is, only whether to relay it. The chain is UNCHAINED, so no previous signature is needed and any round verifies on its own.

bls254.VerifyDrand, NOT bls254.Verify. The two implement different schemes and each rejects every signature of the other; Verify is the retired SPLASH try-and-increment scheme and is left untouched, still exercised by p/splash/bls254's own suite.

COST: 16,970,375 gas measured on this toolchain (zz_gas_test.gno), 0.57% of the 3e9 block limit — about 176 to a block. Of it, 13,396,451 is bls254.VerifyDrand and 12,004,207 of THAT is the RFC 9380 hash-to-curve rather than the pairing: 90% of the verification, 71% of the whole Publish. (CORRECTED 2026-09-30: this said "98%", which was the pre-optimisation ratio; DRAND-MIGRATION.md section 9.5.) It fits with a wide margin, and it is also the reason this realm cannot mirror drand.

A RELAYER CANNOT PUSH latestRound INTO THE FUTURE, and a great deal now rests on that. Under the SPLASH key one Publish of a far-future round moved latest arbitrarily far in a single transaction, which was the operator-triggered half of AUDIT-2026-09-28.md finding 7. drand cannot sign a round whose time has not come, so latestRound is bounded above by drand's real head, and that attack is dead.

func Published

Action
1func Published(round int64) bool
source

Published reports whether a round has been published.

WHAT IT MEANS NOW: "somebody has relayed this round to this chain". NOT "this round's randomness exists" (it may exist on drand and simply not be here yet) and NOT "this round's randomness is obtainable" (it always is, from drand). Every caller that treats false as "lost" is wrong; see DRAND-MIGRATION.md section 3.

func PublishedAtHeight

Action
1func PublishedAtHeight(round int64) int64
source

PublishedAtHeight returns the chain height at which a round was relayed. Panics if the round has not been published.

func Randomness

Action
1func Randomness(round int64) string
source

Randomness returns sha256(signature) for a published round, hex-encoded. Panics if the round has not been published.

This is drand's own derivation, not one of ours: the "randomness" field /public/<r> serves is exactly sha256 of the signature it serves beside it, which makes every real round an independent oracle for this function. testdata's fixtures were checked against it.

func Render

1func Render(path string) string
source

Render implements the realm's human-readable view.

func StaleBlocks

Action
1func StaleBlocks() int64
source

StaleBlocks is how far the chain has moved since the last relay that RAISED the estimate's anchor: the one distance every error in this realm's extrapolation is proportional to. Panics if no round has ever been published.

IT IS NOT "BLOCKS SINCE THE LAST Publish", and the difference is the whole soundness argument — read anchorHeight. A relay that does not raise the anchor does not reduce the error, and "blocks since the last Publish" is resettable by anyone for the cost of relaying any one of drand's ~2.1e7 public past rounds.

EXPORTED SO A CONSUMER OR A FRONT END CAN SEE THE REFUSAL COMING rather than discover it as a panic in the middle of a settlement, and so a caller can decide to relay a round first. Clamped at zero: a negative distance is unreachable on chain (height only rises, anchorHeight only moves to a then-current height) and arises only under `gno test`, where ChainHeight() resets between test functions while realm state persists — DRAND-MIGRATION.md section 9.7.

Dependencies

Source Files 2

Directories 1