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/v2 package

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.

Functions

AnchorHeight

func AnchorHeight() int64
Ask AI Claude ChatGPT

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.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.AnchorHeight()"

Result

CurrentRound

func CurrentRound() int64
Ask AI Claude ChatGPT

CurrentRound is the drand round that exists NOW, computed from the BLOCK's own clock rather than extrapolated from whatever a relayer last chose to deliver.

WHY IT IS BETTER THAN EstimatedLatestRound, WHICH IT IS INTENDED TO REPLACE. The estimate's input is the last RELAYED round, and this realm's own TestKnownWeaknessRelayingTheRoundJustAboveTheEstimateBuysTheWholeStalenessBudgetBack shows that relaying the round just above it resets the staleness clock for ~0.017 GNOT while advancing the estimate by one round. No arithmetic over relayed rounds can fix that: what a caller needs is an UPPER bound on drand's head ("round R is not signed yet"), a signature only ever proves a LOWER bound, and the ABSENCE of a signature is not provable on chain at all because withholding is free and invisible.

A CLOCK HAS NO SUCH ASYMMETRY. drand round r is signed at genesis + period*(r-1), so the round that exists at time t is pure arithmetic over t. No relayer is involved, so there is nothing to withhold and nothing to poison.

WHAT IT COSTS, SAID PLAINLY RATHER THAN DISCOVERED LATER: it trusts the chain's block time. Under Tendermint that is the voting-power-weighted median of the validators' own timestamps, so moving it requires colluding validators rather than a tenth of a cent. That is a WEAKER assumption than cryptography and a far STRONGER one than "no relayer withheld anything". For a caller that previously trusted the estimate it is an improvement on both axes; for one that trusted nothing, it is a new assumption and should be chosen deliberately.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.CurrentRound()"

Result

EstimateIsUsable

func EstimateIsUsable() bool
Ask AI Claude ChatGPT

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

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.EstimateIsUsable()"

Result

EstimatedLatestRound

func EstimatedLatestRound() int64
Ask AI Claude ChatGPT

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

A NEW CALLER SHOULD REACH FOR CurrentRound INSTEAD, AND THIS FUNCTION IS KEPT ONLY FOR WHAT ALREADY CONSUMES IT. This estimate extrapolates from the last RELAYED round, and this realm's own TestKnownWeaknessRelayingTheRoundJustAboveTheEstimateBuysTheWholeStalenessBudgetBack proves that extrapolation is poisonable for the cost of one Publish of a freely-fetched PAST round: relaying est+1 resets the staleness clock to zero while moving the estimate forward by exactly one round. CurrentRound instead reads the block's own clock and computes drand's round by arithmetic, so there is no relayed round to poison. See CurrentRound's comment for the full account, including what trusting the block's clock costs in its place. Nothing that calls EstimatedLatestRound today has been switched over -- that is a separate change -- so this function must keep working exactly as it does.

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.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.EstimatedLatestRound()"

Result

EstimatedLatestRoundWithin

func EstimatedLatestRoundWithin(maxStaleBlocks int64) int64
Ask AI Claude ChatGPT

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.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.EstimatedLatestRoundWithin()"

Result

Latest

func Latest() (int64, string)
Ask AI Claude ChatGPT

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.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.Latest()"

Result

LatestPublishedAtHeight

func LatestPublishedAtHeight() int64
Ask AI Claude ChatGPT

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

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.LatestPublishedAtHeight()"

Result

Publish

func Publish(cur realm, round int64, sigHex string)
Ask AI Claude ChatGPT

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.

Params

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/beacon/v2" -func "Publish" -args $'' -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/beacon/v2" -func "Publish" -args $'' -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
  

Published

func Published(round int64) bool
Ask AI Claude ChatGPT

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.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.Published()"

Result

PublishedAtHeight

func PublishedAtHeight(round int64) int64
Ask AI Claude ChatGPT

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

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.PublishedAtHeight()"

Result

Randomness

func Randomness(round int64) string
Ask AI Claude ChatGPT

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.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.Randomness()"

Result

Render

func Render(path string) string
Ask AI Claude ChatGPT

Render implements the realm's human-readable view.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.Render()"

Result

RoundAt

func RoundAt(unixSeconds int64) int64
Ask AI Claude ChatGPT

RoundAt is CurrentRound's arithmetic made callable at an arbitrary time, so the round-versus-time relationship is testable directly and so a caller can reason about a round in the future (or the past) rather than only the current one. Panics under the same condition as CurrentRound: see there for why.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.RoundAt()"

Result

StaleBlocks

func StaleBlocks() int64
Ask AI Claude ChatGPT

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.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.StaleBlocks()"

Result

TimeOfRound

func TimeOfRound(round int64) int64
Ask AI Claude ChatGPT

TimeOfRound is RoundAt's inverse: the unix second at which round is signed. Needed so a caller can say "this round is signed N seconds from now" (or ago), and so the round<->time relationship can be checked in both directions rather than trusted in one.

Unlike RoundAt, this does not panic for round < 1: a round number is just an integer here, and the arithmetic is exact and total in both directions. A caller asking for round 0 or below is asking "what time would this (non-existent) round have been", which is an arithmetic question with a well-defined answer, not a claim that such a round exists.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2.TimeOfRound()"

Result