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.

arena/v1 package

Overview

Package arena is Gnotchi's sealed-plan battle realm: commit-reveal challenges between two gnomes, resolved deterministically on chain against app/js/rules.js's own battle math. This is the realm the project's strongest on-chain claim rests on. Everywhere else in this project, "it's on chain" buys permanence and ownership. Here it buys something harder: neither player has to trust the other, or Gnotchi's own server, about the result of a battle that gets resolved -- because the challenger's plan is cryptographically sealed before the opponent plays, and the chain, not either player's client, is the one that checks the seal and resolves the fight.

THE PRECISE LIMIT OF THAT CLAIM, stated here rather than left for someone to discover, because this file cannot be edited once deployed. The guarantee is about TAMPERING, not about COMPLETION. The opponent's plan is posted in the clear at Accept (see below for why that asymmetry is correct), and resolveBattle is fully deterministic, so from the moment of Accept the challenger can compute the exact outcome and only then decide whether to call Reveal at all. A challenger who would lose is best off going quiet. The cost of going quiet is a forfeit -- but ClaimForfeit is the OPPONENT's call and costs them a transaction, so a challenge nobody claims simply sits accepted forever with no result recorded. Nothing here can be made to lie about a battle; a battle can be made not to finish. The recorded history is therefore selection-biased toward results challengers were happy to publish, which matters to anything that later reads `ranked` or `winner` in aggregate -- see NOTES.md, and see there too for why the obvious fix (making ClaimForfeit permissionless) was rejected rather than overlooked.

WHETHER THIS IS DEPLOYED IS DELIBERATELY NOT RECORDED HERE. This file ships on chain exactly as written and cannot be edited afterwards, so any deploy status stated in it is frozen at authoring time and becomes a lie the moment it changes. garden/v2/gnomod.toml said "NOT deployed" in just such a line, was then deployed, and now carries that false claim on chain permanently. Live status lives in the repo root's STATUS.md and DECISIONS.md, which can still be corrected; the authority above both is the chain itself (`vm/qfile` on this path).

============================================================================ WHY THIS NEVER STORES A GNOME, A TYPE OR A LEVEL ============================================================================

A gnome's type (Gardener/Explorer/Tinkerer, the input the battle's type-edge bonus needs) and level (the input ranked eligibility needs) already live in garden/v2, already deployed, already the single source of truth for "whose gnome is this and what is it". Duplicating either field into this package's own storage would create two places that could disagree, and only one of them -- garden/v2 -- is the one players actually see on their gnome's own page. So every battle resolution here reads both participants' current type and level directly from garden/v2's own exported GnomeByAddress, by importing it like any other package, at the moment the battle is actually resolved (Reveal or ClaimForfeit) -- never cached, never copied, never taken as a caller-supplied argument. That import is what makes "any realm can read a gnome without a deal" (see repo root DECISIONS.md, 2026-10-09, "Other realms can read a gnome without permission") a property rather than a sentence in a document: no API key, no permission, no agreement with garden/v2, and no way for garden/v2 to withdraw the access. This file is the other realm.

The import below is written against garden/v2's exact exported GnomeByAddress signature, and this realm cannot resolve a single battle unless garden/v2 is live at precisely the path imported below. So whoever deploys this must confirm that path on chain first -- not from this file, and not from a document, but with a `vm/qfile` read. See this package's own gnomod.toml and NOTES.md.

============================================================================ THE HASHING, AND WHY IT MUST MATCH app/js/rules.js EXACTLY ============================================================================

app/js/rules.js's own commitHash is `sha256(planString + ':' + saltHex)`, hex-encoded, using Node's crypto.createHash('sha256'). A plan sealed in the browser with that function must verify here with the IDENTICAL bytes hashed the IDENTICAL way, or every sealed challenge ever made in the app would fail to reveal on chain. Gno's own crypto/sha256 (gnovm/stdlibs/crypto/sha256, wrapping Go's stdlib sha256.Sum256) plus encoding/hex give exactly that: verified directly, not assumed -- a scratch package computing CommitHash("S,G,Q", "deadbeef") in Gno via sha256.Sum256(...)+hex.EncodeToString and the equivalent crypto.createHash('sha256').update(...).digest('hex') in Node produce the byte-for-byte identical 64-character hex string (dbce29c54438799e885fd3712ade046d71cd387649c1f14e837ab5f351a38c86). So the brief's own worry -- "if gno's crypto makes an exact match impossible, say so loudly" -- does not apply: an exact match is possible, and commitDigest below is it. See NOTES.md for the verification command.

============================================================================ THE COMMIT-REVEAL STATE MACHINE ============================================================================

Every challenge is exactly one of four states, and every exported call here is exactly one state transition, enforced by checking the CURRENT status before doing anything else (validate-then-commit, same discipline as garden/v2's SaveDay):

Example
1pending -> accepted    (Accept, by the opponent, within challengeExpiresHours)
2accepted -> resolved   (Reveal, by the challenger, within revealWindowHours of accept)
3accepted -> forfeited  (ClaimForfeit, by the opponent, once revealWindowHours has passed)

There is no path back from resolved or forfeited, and no path from pending straight to resolved or forfeited -- a plan can only ever be revealed against a challenge the named opponent has actually accepted. Whichever of Reveal/ClaimForfeit executes first, while the challenge is still "accepted", locks the result; the other call then fails against the now-changed status (a late Reveal fails its own deadline check regardless, and a too-early ClaimForfeit fails its own deadline check regardless -- see each function's own comment). This makes the state machine race-free without needing a lock beyond the single mutable record already in the avl.Tree.

============================================================================ SECURITY: NO recover(), SAME RULE AS garden/v2 AND hollowmere/v1 ============================================================================

Nothing in this file calls recover(). Every mutating function here validates completely against a value copy before ever touching the stored *arenaChallenge, so an uncaught panic at any point before the final commit leaves the persisted record exactly as it was, with no partial effect to undo -- the same validate-then-commit discipline garden/v2's SaveDay uses, for the same reason: a recover() anywhere in this realm, even one added later for an unrelated reason, could convert a rejected transition into a silently successful one.

============================================================================ SCOPE ============================================================================

Built: Challenge, Accept, Reveal, ClaimForfeit, the ranked-eligibility fairness rules (level 3 to rank, 3 ranked battles a gnome a day, 2 ranked results a pair a week -- all enforced structurally, never bought, because this realm accepts no coins at all), and a public Render/ChallengeStatus read surface.

NOT built, on purpose: Elo ratings and season points. Brief 5.2 puts both in `season/v1`, a realm that reads arena's own results -- it does not exist yet and this assignment did not ask for it. Nothing here computes a rating or a season point, and there is no admin, no owner and no upgrade hatch, matching every other deployed realm in this project. See NOTES.md for what a future season realm would need from here, and for the cases where app/js/rules.js itself leaves the fairness rules' exact mechanics unspecified.

Function

ChallengeProof

func ChallengeProof(challengeID string) ( commit string, challengerPlan string, salt string, opponentPlan string, found bool, )
Ask AI Claude ChatGPT

ChallengeProof returns everything a third party needs to re-derive a finished battle from scratch, without trusting this realm, either player, or Gnotchi's server.

This realm's entire reason to exist is that nobody has to be trusted about the result. ChallengeStatus returns the result -- which is this realm ASSERTING a winner. That is exactly the kind of claim the realm was built to avoid having to make. Given commit, challengerPlan, salt and opponentPlan, anyone can check sha256(challengerPlan + ":" + salt) == commit, feed both plans through app/js/rules.js's resolveBattle, and get the same winner and the same puff -- or catch this realm being wrong. Without these four fields the "you don't have to trust us" claim is itself something you'd have to take on trust, and since a realm cannot be edited, a getter missing at deploy time stays missing until a v2 and a migration.

Before a Reveal, challengerPlan and salt are empty strings, so this getter cannot leak a plan early: there is no plan here to leak until the challenger has published it themselves. commit is readable from the moment of Challenge, but it always was -- it is a public argument of that transaction -- so exposing it here reveals nothing new.

THE SEAL'S STRENGTH IS THE SALT'S, AND THIS REALM CANNOT ENFORCE THAT. Stated plainly because the honest version is narrower than it first looks. There are only 27 possible plans: battleRounds is 3 and there are 3 legal moves, so 3^3. commitDigest is a single bare sha256 with no KDF and no iteration. So a commit whose salt is known, reused, or derived from anything predictable falls to 27 hashes -- a complete break of the only thing this realm exists to guarantee. Reveal deliberately imposes no minimum salt length, because a length check added here would permanently reject any challenge already sealed with a shorter salt and force its challenger to forfeit. The entropy requirement therefore lives entirely in the client: app/js/battle.js seals with 32 fresh crypto.getRandomValues bytes per plan, never reused. Any other client MUST do the same; 27 candidates is not a margin to be casual with.

The preimage also binds neither the challenge ID nor the challenger's address, so the same commit can be posted in several challenges. No advantage was found in doing so -- a replayer commits to a plan they will not learn until the original reveals, which is strictly worse than choosing one -- but it is recorded here because the preimage has to match app/js/rules.js byte for byte and so can never be changed.

A forfeited challenge has no challengerPlan by definition -- the challenger never revealed one, which is why it was forfeited. `found` is false for an unknown ID.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arena/v1.ChallengeProof()"

Result