garden.gno
260.61 Kb · 4961 lines
1// Package garden is Gnotchi's pet realm, v7: the free, allowlisted, drand-rolled, hold-many
2// realm MINT.md designs end to end, now carrying Batch One's personality, the kinship web and a
3// batch of sixty. It inherits garden/v4's machinery -- the SaveDay shape, meters, garden plots,
4// levels, and the List/Buy/Cancel trading surface -- but NOT v4's invariants (MINT.md section 6).
5// garden/v4 is live, immutable and untouched by this file; its two gnomes stay exactly where
6// they are forever (MINT.md section 6, BATCH-ONE.md section 0), and so does garden/v5's one.
7//
8// ============================================================================================
9// WHAT v7 IS: v6 PLUS EXACTLY TEN CHANGES. READ THIS, THEN "WHAT v6 IS" BELOW IT.
10// ============================================================================================
11//
12// v7's specification is realms/garden/v7/DESIGN.md (1,974 lines, written 2026-10-17 and
13// reviewed before any of this was typed). This block is the summary; that document is the
14// argument, and where this file disagreed with it the disagreement is marked in the source at
15// the place it happens.
16//
17// THE RULE THIS FILE IS WRITTEN UNDER, which is v6's own rule quoted back at its successor:
18// "Every line of v5 that is not one of the two fixes below is carried forward byte-for-byte...
19// v5 survived a security audit; every unrelated change made here would be a change that has not
20// been audited, so none was made." v7 is the version that was reserved for changing things, so
21// the door is open -- but THE AUDIT ARGUMENT DOES NOT EVAPORATE BECAUSE THE DOOR IS OPEN.
22// Everything v6 carries forward is carried forward here too, byte-for-byte, unless it appears in
23// the list below. The list is complete. If a change is not in it, it is not authorised, and the
24// three defects mutation testing found in v5, along with every comment recording why something
25// is shaped as it is, are all still here on purpose.
26//
27// CHANGE 1 -- batchCap 24 -> 60. DECISIONS.md 2026-10-17, "Batch One becomes 60", superseding
28// "Batch One stays 24" from earlier the same day. ONE CONSTANT.
29//
30// CHANGE 2 -- FIVE ROLLED TRAIT AXES plus born-under, stored on the gnome. TRAITS.md sections 1
31// and 7.
32//
33// CHANGE 3 -- THE DOMINANCE ORDER, stored as five explicit slots rather than a 7-bit Lehmer
34// index. The reason is in the gnome struct's own comment: a factorial-base index needs a codec
35// on both sides of the wire, which is two things that must agree forever over a convention
36// nobody has written down, in the one function MINT.md section 9 names as needing its own audit
37// pass above all others.
38//
39// CHANGE 4 -- THE FOUR STATS on a shared budget, drawn from a 546-row legal set ENUMERATED AT
40// init() FROM THE RULE rather than transcribed, because a transcription error in a 546-row table
41// is undetectable by eye and the rule it transcribes is four comparisons. TRAITS.md section 4.2.
42//
43// CHANGE 5 -- A SECRET ID, with the first-seventeen-distinct constraint. TRAITS.md section
44// 3.2.2: without it, 36.9% of batches could not spell the acrostic of section 3.2.1.
45//
46// CHANGE 6 -- A ROLLED CARE THRESHOLD, stored, WITH NO GETTER. The one declared exception to
47// GAMES.md:38, and the only write-only field in the realm.
48//
49// CHANGE 7 -- THE 84 KINSHIP EDGES on chain, STRUCTURE ONLY, keyed by batch index. LINEAGE.md
50// section 6's OVERRULED box: "store the structure, keep the wording off chain where it can still
51// be improved."
52//
53// CHANGE 8 -- seedFor MOVES FROM FNV-1a TO SHA-256. NOT A FEATURE -- a defect DESIGN.md found
54// while working out change 3. FNV-1a's low bits are provably a parity of the input bytes' low
55// bits; v5 and v6 take % 100, % 60 and % 4 and mostly get away with it, and v7 takes % 2, % 3,
56// % 5 and % 16 for the one field TRAITS.md's whole duplicate-vector argument rests on. THIS IS
57// THE ONE TO READ FIRST IN REVIEW: it rewrites a line an audited realm carries. Proof at
58// seedFor.
59//
60// CHANGE 9 -- THE NAMED-EDITION ODDS ARE DECOUPLED FROM THE BATCH SIZE AND RAISED TEN-FOLD.
61// 2026-10-18, the owner: "make the last few have them. The rates have to come out during a mint."
62// ONE CONSTANT AND ONE LINE: namedPick's modulus was uint64(batchCap), so each of Hob, Thistle and
63// Wick was a 1-in-60 shot per claimant and there was a 74.37% chance at least one of the three
64// never existed. It is now uint64(namedPickRange), its own constant at 10 -- a 1-in-10 shot each,
65// leaving 0.54% of full batches missing one. NOTHING ELSE ABOUT THE NAMED-EDITION PATH MOVES: the
66// `namedPick < numNamedEditions` branch, the characterOwner.Has check and the
67// transaction-ordering-race comment inside it are v6's byte-for-byte. THIS IS NOT A GUARANTEE and
68// is not a replacement for the removed one; it is a probability, it fires nowhere new, and it reads
69// nothing new. The arithmetic for all three rates, why the modulus is NOT batchCap (that coupling
70// was accidental -- it made the rarest-asset odds a side effect of a batch size changed for
71// unrelated reasons), why raising a rate is safe where the removed guarantee was not, and the
72// honest cost (collisions become common; first settle wins) are at namedPickRange's declaration.
73//
74// CHANGE 10 -- THE NAMED ONE-OF-ONES GO FROM THREE TO EIGHT. 2026-10-18, the owner: "If we are
75// doing 60 there should be more specials than 3." He is right, and the reason is arithmetic that
76// change 1 produced silently: three of 24 was 12.5% of a batch, and moving the batch to 60 without
77// touching the roster quietly cut it to 5%. EIGHT OF 60 IS 13.3%, which restores the proportion the
78// collection was designed at. ONE CONSTANT (numNamedEditions 3 -> 8) AND THREE TABLES IN STEP
79// (namedEditionOrder, namedEditionDisplayNames, reservedNameKeys) -- plus List's lock message,
80// which says the count out loud and is therefore a FOURTH place the roster's size appears; see
81// numNamedEditions for the full list of what must move together.
82//
83// THE FIVE NEW NAMES ARE BATCH-ONE.md section 7's, EXTENDED IN THAT SECTION'S OWN REGISTER AND
84// JUSTIFIED THERE, NOT HERE: Tally, Glim, Vigil, Spruce, Yonder. They are EXISTING, ALREADY
85// VALIDATED ART, renamed exactly the way Hob (the gilded gnome) and Thistle (the Viking) were --
86// trowel-hand, emberwing, nightwatch, gentleman and pirate from pack/04-pixel-art/characters.json,
87// every one of them "works"/"confirmed at true 2x" in ARCHETYPES.md section 3 and section 6. NO NEW
88// ART WAS DRAWN OR MODIFIED FOR THIS CHANGE and this realm does not know what any of them look
89// like: `character` is a string, the look lives in the art pack, and that separation is unchanged.
90//
91// namedPickRange STAYS AT 10, DELIBERATELY, AT THE OWNER'S APPROVED ODDS -- a specific edition is
92// still a 1-in-10 shot per claimant and nothing about the pick, the branch or the race moves. What
93// DOES move is what the odds compose to, and the new figures replace the old ones everywhere they
94// are written down: (9/10)^60 = 0.18% per edition is unchanged, but P(at least one of EIGHT
95// missing) = 1 - (1-0.0017971)^8 = 1.43%, up from 0.54% at three. A half-filled batch of 30 is
96// 29.3%. The cost side moves much further: 48 of 60 claimants now hold a named-pointing pick
97// (60 * 8/10) for eight slots, so about 40 of them lose the race and fall through to an ordinary
98// draw. Full arithmetic, all four rows, at namedPickRange. NOTE THE HEADROOM: 10 >= 8 still holds,
99// but only just -- at eight editions the modulus is two residues from the point where an edition
100// becomes unreachable, so numNamedEditions may not grow again without raising namedPickRange, and
101// TestNamedEditionOddsAreOneInTen is the test that fails if it does.
102//
103// NOT A GUARANTEE, AGAIN AND FOR THE SAME REASONS: this adds five entries to three tables and no
104// mechanism. No forcing, no namedEditionToForce, nothing keyed on a shrinking observable (AUDIT-01
105// RED-1). "TALLY WAS NEVER BORN" is now a 0.18% permanent fact per edition exactly as "HOB WAS
106// NEVER BORN" is, and the chance that SOME edition is missing went UP, not down, because there are
107// more editions to miss. That is the accepted price of the proportion the owner asked for.
108//
109// NOT A CHANGE -- THE NAMED-EDITION GUARANTEE, WRITTEN 2026-10-17 AND REMOVED 2026-10-18. It was
110// briefly a change of its own: "when the number of unsettled slots remaining equals the number of
111// named editions still unclaimed, the remaining settles take them." AUDIT-01 RED-1 found that
112// rule positionable -- its trigger was computable from two public readers and a wallet that never
113// claimed until 57 were settled took an edition with certainty -- and the owner chose REMOVAL over
114// any replacement, so the named-edition path here is v6's byte-for-byte apart from change 9's one
115// modulus, and NO END-OF-BATCH FORCING AND NO namedEditionToForce EXISTS OR MAY BE RE-ADDED. The
116// residual chance that at least one edition is never minted is KNOWN AND ACCEPTED: 74.4% when
117// it was accepted, 0.54% after change 9, 1.43% after change 10 raised the roster from three to
118// eight (more editions, more chances for one of them to be missed), never zero. The full record of
119// the decision is in settleCore, next to the pick it would have overridden; do not re-add a
120// guarantee without reading it, and note that changes 9 and 10 are deliberately NOT one -- between
121// them they move two numbers and five table entries, not a mechanism.
122//
123// WHAT v7 MUST NEVER DO, and these are named so that leaving them out is a decision on the
124// record rather than an omission. From TRAITS.md section 7: no rarityScore(), no TraitCensus(),
125// no traitCount(axis, id), no sort-by-stat, no trait-gated entrypoint anywhere in any realm,
126// nothing that returns a distribution. From LINEAGE.md section 6.5: no KinOf(tokenID), no
127// IsKin(a, b), no LineOf(tokenID), no LineCount(line), no KinCount and nothing that returns a
128// degree, no sort/filter/gate of any kind on a line, a relation or a relative, and NO ENTRYPOINT
129// ANYWHERE IN ANY REALM THAT BEHAVES DIFFERENTLY FOR TWO RELATED GNOMES THAN FOR TWO STRANGERS.
130// Added by v7, because it is the first realm where they become reachable: no CareThreshold /
131// CareScore / Band / PointsToThreshold or any fraction of them; no SecretCount / SecretCensus /
132// HoldersOfSecret / IsFirstSeventeen and nothing exposing secretsDrawnMask; no Label / Archetype;
133// no TraitsMatching or any filter or predicate; no unique-in-batch marker; AND NO READER,
134// ANYWHERE, THAT TAKES TWO TOKEN IDS -- that last one is the structural form of all of it,
135// because a function of two gnomes is the shape every kin-bonus, related-pair and shared-quest
136// feature needs. See the shape rule stated once above the trait readers.
137//
138// THE INVARIANT THOSE PROTECT, STATED ONCE: NO FUNCTION IN v7 BRANCHES ON A TRAIT, A STAT, A
139// LINEAGE EDGE OR A SECRET. Not in a guard, not in a loop condition, not in a fee calculation,
140// not in an eligibility check. Trait data is read by this realm's own code in exactly two
141// places, and both are inside the draw writing itself down: drawRoll/assertRoll, and
142// secretWeightsFor's one comparison of boast/fear against the secret target table, which
143// produces a secret id and nothing else. TestTwoGnomesWithOppositeTraitsAreTreatedIdentically is
144// that invariant as a test a CI run can check rather than a promise a reviewer has to take.
145//
146// THE MIGRATION QUESTION, ANSWERED ONCE SO NOBODY RE-OPENS IT: v5's one gnome STAYS ON v5 and
147// v4's two stay on v4. No migration, ever -- the identical answer BATCH-ONE.md section 0 gave
148// one version earlier, for the identical reason: v5 is immutable, has no transfer-in path, and
149// nothing in v7 can reach it. BATCH ONE IS THE SIXTY GNOMES ON THIS REALM AND NOTHING ELSE; the
150// 60 is sixty, not fifty-nine plus the one already minted. v5's gnome holds no axis, no
151// dominance order, no stat line, no secret, no sign, no threshold, no batch index and no kinship
152// edge -- not because anything was withheld but because V5 HAS NO SUCH FIELDS, so it cannot be a
153// member of a batch whose defining properties did not exist when it was minted. Nothing in this
154// file knows v5 exists: not a field, not an import. No cross-realm per-wallet cap (MINT.md
155// section 6 already settles it -- "a single wallet may hold a v4 gnome and any number of v5
156// gnomes at once" -- and importing v5 would make v7 undeployable anywhere v5 is not live), no
157// batch-index offset, no legacy-holder benefit (if the owner wants the v5 holder on the v7
158// allowlist, that is one AdminAllow call and a curation decision), no cross-realm name
159// uniqueness (impossible, and already true between v4 and v5), no cumulative TotalSupply, and no
160// deprecation pointer in v5 (it is immutable). THE ONLY THING v7 INHERITS THAT IS NOT SOURCE
161// CODE IS THE NAMESPACE.
162//
163// AND ONE FACT THAT CHANGES THE SHAPE OF THIS VERSION: v7 SUPERSEDES A REALM THAT NEVER CAME UP.
164// garden/v6 is written, audited, passing its tests and NOT ON CHAIN -- vm/qpkgmeta_json answers
165// "status":"inert", three paid addpkg attempts produced nothing retrievable, and a five-line
166// probe realm with no imports failed identically, which is what proves it is the chain and not
167// the code (TESTNET.md:530-582). So v6's two fixes have never been live for anybody: SetName and
168// SettledRound arrive for the first time here, and section 1.1's "byte-for-byte" is a claim about
169// SOURCE, not about continuity of service. Before attempting to deploy this larger realm, re-run
170// the five-line probe first -- and remember the lesson that file draws: "OK! from gnokey,
171// success:true in the block, and a debited balance are together still not evidence that a deploy
172// happened. Only reading the package back is."
173//
174// ============================================================================================
175// WHAT v6 IS: v5 PLUS EXACTLY TWO FIXES, AND NOTHING ELSE. READ THIS SECOND, AND READ IT WHOLE.
176// IT IS THE RECORD OF THE AUDITED BASE THIS FILE IS A COPY OF, AND EVERY WORD OF IT STILL
177// APPLIES EXCEPT WHERE THE TEN CHANGES ABOVE SAY OTHERWISE.
178// ============================================================================================
179//
180// garden/v5 is DEPLOYED AND LIVE on the onyx testnet. v6 exists because a review on 2026-10-16
181// found a hole that the original design, the test suite and the adversarial security review
182// (ATTACK-REVIEW.md, eight attack goals) all missed -- not a vulnerability, a MISSING FEATURE
183// that contradicts something the owner asked for in his own words. This file is a DELIBERATELY
184// NARROW, SURGICAL COPY of v5. Every line of v5 that is not one of the two fixes below is
185// carried forward byte-for-byte, including every comment recording why something is shaped as
186// it is and the three real defects mutation testing found and fixed. v5 survived a security
187// audit; every unrelated change made here would be a change that has not been audited, so none
188// was made. If you are about to "also improve" something while reading this file: don't. Write
189// a v7.
190//
191// FIX 1 -- A v5 GNOME CANNOT HAVE A NAME, AND THE GAME NEEDS ONE.
192//
193// v5's gnome struct has no name field, no setter, and `character` -- the only identity string
194// it carries -- is "" for every gnome outside the three capped one-of-ones. Confirmed on the
195// LIVE CHAIN, not inferred: `GnomeByToken("0000002")` on the deployed v5 returns
196// character "". So 21 of Batch One's 24 gnomes would have been permanently anonymous, with no
197// way ever to fix it, because the realm is immutable.
198//
199// That directly contradicts the owner's own instruction that in-game characters "should
200// refer to you as your gnome name" -- the adventure's whole NPC dialogue surface is built on
201// a name existing. v5's own package doc argued the opposite ("NO PLAYER-CHOSEN NAME... the
202// app is free to let an owner caption their own gnome off chain"), and that argument was
203// WRONG in one specific, load-bearing way worth recording rather than quietly deleting: an
204// off-chain caption is not an identity. It cannot travel with the token through a sale, it
205// cannot be read by another realm, it cannot be reserved against impersonation, and the
206// adventure realm cannot greet you by it. The reasoning that was right -- that nothing
207// should be CHOSEN at mint time -- is preserved exactly: see SetName below.
208//
209// THE SHAPE, AND WHY. Naming is a SEPARATE ACT, AFTER the roll, never part of it. `Claim`
210// still takes no arguments and `Settle` still takes no name: the entire point of the mint is
211// that you do not choose what you get (MINT.md section 1a/section 2), and nothing here
212// weakens that. Naming what you already got is a different act entirely, performed by a
213// different function, at a different time, by the token's current owner only.
214//
215// FIX 2 -- v5 NEVER STORED THE DRAND ROUND THAT PRODUCED EACH GNOME.
216//
217// v5 emits the settling round in its `Settle` event and then forgets it. "Which round made
218// this gnome" was therefore a transaction-log archaeology problem rather than a state read:
219// `Provenance` returns adoptedAt/acquiredAt/saleCount and there was no getter for the round
220// at all. Since "provably unrigged" is this project's headline claim (ECONOMY.md section 1),
221// a holder being unable to verify their own gnome with one query was a gap between what the
222// project says and what the chain can answer. v6 stores the round on the gnome itself
223// (gnome.settledRound) and exposes it (SettledRound). See settleCore.
224//
225// ============================================================================================
226//
227// WHAT CHANGES FROM v4, IN ONE TABLE, EACH CITED TO THE DOCUMENT THAT DECIDED IT:
228//
229// - Adopt(name, hat, coat, eyes, nose) is GONE. Nothing is chosen by a caller any more.
230// DECISIONS.md 2026-10-15 deprecates the DIY picker as a product direction; MINT.md
231// section 1a/section 2 replace it with a roll.
232// - An ALLOWLIST gates who may ever claim (MINT.md section 11: "gate issuance, never the
233// secondary market"). Admin-managed, additive and removable, never touching the mint
234// itself once a claim has spent its roll.
235// - ONE ROLL PER WALLET, FOREVER, enforced independently of whether the resulting gnome is
236// still held (MINT.md section 1a/section 2.3). hasRolled is permanent; nothing in this file
237// clears it, and selling the gnome does not refund the roll.
238// - FREE. Claim refuses coins exactly as v4's Adopt refuses them (MINT.md section 1a/2.5,
239// GAMES.md rule 6: "random when earned, exact when bought... free: random is allowed").
240// - THE ROLL IS TWO-PHASE, SETTLED BY DRAND (MINT.md section 10, the load-bearing finding of
241// this whole file). A single-transaction roll is peekable two ways, verified against
242// gno.land's own source and binary rather than assumed from Ethereum's model: vm/qeval can
243// call a crossing function for free with no signature, and `gnokey maketx call -simulate
244// only` previews a real signed call against the last committed block with no gas and no
245// broadcast. Neither tool is the cause; the cause is that gno.land exposes NO on-chain
246// entropy at all -- chain/runtime has ChainHeight and nothing else, math/rand is PCG seeded
247// (0,0) on every node forever, and there is no shipped VRF -- so any single-transaction
248// formula, however it is dressed up, is something a caller can evaluate offline for every
249// block it might land in before ever deciding to broadcast. The fix MINT.md section 10.5
250// settles on, and this file builds, is a commit/reveal split across this project's own
251// deployed drand verifier: Claim spends the one-time roll and pins a ROUND THAT DOES NOT
252// EXIST YET; Settle, permissionless, computes the result once that round is actually
253// published. One player signature, not two -- the player signs Claim only.
254// - PARTS FROM A WEIGHTED TABLE (ECONOMY.md section 3), not a player's choice, with three
255// named one-of-ones (Hob, Thistle, Wick, Tally, Glim, Vigil, Spruce, Yonder -- BATCH-ONE.md
256// section 7; three until v7 change 10 made it eight) capped at one each.
257// - COUNTED RARITY. Every part's population is tracked on chain and readable by anyone --
258// MINT.md section 13's "nine gnomes in the world have this nose" has to be a chain fact,
259// and EyesCount/NoseCount/HatCount/CoatCount below are that fact, not an app claim layered
260// on top.
261// - MINT ONE, HOLD MANY, PLAY ONE (MINT.md section 2a). Buy's one-gnome-per-address refusal is
262// GONE -- deliberately, and only from Buy, never from Claim (see Claim's hasRolled gate,
263// which is the cap that still matters). An address may hold any number of gnomes; exactly
264// one is ever AWAKE, tracked by activeToken, and only the awake one can ever gain a cared
265// day, a streak, a level or XP. SaveDay and ChooseType resolve through activeToken, never
266// through "the" gnome for an address, because there no longer is one.
267// - SWITCHING (Wake) is free, at most once per UTC day (MINT.md section 2a), the same
268// day-boundary discipline SaveDay's own dailyCareDay already uses.
269// - THE SECONDARY MARKET IS OPEN: Buy/List/Cancel carry over v4's shape and fee (MINT.md
270// section 11), with no allowlist on buying and no hold cap.
271// - THE LOCK: the eight named one-of-ones cannot be listed until they have lockCaredDays
272// cared days; every ordinary gnome trades from day one (MINT.md section 12/13, "Option A" --
273// lock only the named one-of-ones). SETTLED 2026-10-19 AT 14, down from that section's
274// unconfirmed recommendation of 30; reasoning at lockCaredDays itself. It is still one named
275// constant and nothing else in this file assumes its value -- verified by AUDIT-03, which
276// also confirms maxListingDays' own 30 was never coupled to it.
277// A gnome's own caredDayCount never decreases and is not reset by Buy, so once
278// a named gnome unlocks it stays unlocked through every future sale.
279// - A GENESIS GNOME, minted to this realm's own address at deploy, non-transferable, never
280// listable, separate from the 60-gnome batch (DECISIONS.md 2026-10-15: "the genesis gnome
281// belongs to nobody, permanently, on purpose"). ITS NAME IS SPRIG, settled 2026-10-19 --
282// see genesisGnomeName below for why, and note it is load-bearing in FOUR places, not one:
283// the constant, reservedNameKeys, the gnome's own `character` slug, and characterOwner.
284//
285// WHAT IS DELIBERATELY OUT OF SCOPE FOR THIS FILE, EACH A SCOPING DECISION MADE BY THIS PASS,
286// NOT A GAP IN THE BRIEF THAT ASKED FOR IT. Recorded here in the open the same way MINT.md
287// records its own open questions, so a future reader finds the reasoning in one hop:
288//
289// - NO SetRule/FreezeRules TUNABLE ADMIN SURFACE. v4's ruleBand/SetRule/FreezeRules machinery
290// is real and proven, but every care-mechanic number below is a fixed constant at v4's own
291// deploy defaults instead. The brief this file was written against lists eleven concrete
292// requirements and none of them is "tunable rules"; adding v4's whole admin-ratchet surface
293// on top of everything already new here would have roughly doubled this file's size and
294// its audit surface for a feature nobody asked for in this pass. A FUTURE VERSION can add
295// it back following v4's own pattern exactly if the owner wants it. (v5 said "a v6" here;
296// v6 exists now and deliberately did not, for the same reason it added nothing else.)
297// - NO NAME AT MINT TIME. This entry REPLACES v5's own "NO PLAYER-CHOSEN NAME" entry, which
298// is the thing v6 exists to correct -- see the "WHAT v6 IS" section at the top of this file
299// for the full account of what that argument got right and what it got wrong. What survives
300// from it, unchanged and non-negotiable: NOTHING IS CHOSEN BY A CALLER AT MINT TIME,
301// INCLUDING A NAME. `Claim` takes no arguments; `Settle` takes no name. v4's
302// validNameRune/validNameCharset/hasVisibleNameContent machinery (three audit rounds' worth,
303// per v4's own comment) is carried back into this file WHOLE, not re-derived, because the
304// naming act moved rather than disappeared -- it is now SetName, callable only by the
305// token's current owner, only after the roll has already irrevocably decided what they got.
306// - NO OpenBatch ADMIN SURFACE. MINT.md section 6 floats an owner-callable OpenBatch for
307// adding future batches' rosters; the task this file was built against names a single
308// 60-gnome batch (BATCH-ONE.md, at DECISIONS.md 2026-10-17's size) and does not ask for a
309// mechanism to open a second one. The
310// batch here is sized once, at deploy, by the batchCap/numNamedEditions constants below. A
311// FUTURE VERSION is the natural place for OpenBatch if a second batch is ever wanted, the
312// same way every other irreversible-shape decision in this project has gotten its own
313// version rather than a retrofit. (v5 said "a garden/v6 is the natural place"; v6 exists
314// now and deliberately did NOT take that opportunity -- it is two fixes and nothing else.)
315//
316// A REAL FLAW FOUND AND FIXED WHILE DESIGNING Settle, RECORDED HERE RATHER THAN QUIETLY FIXED
317// AND FORGOTTEN -- the same discipline MINT.md section 10 models for its own single-transaction
318// finding. An earlier version of this design picked the named one-of-ones by drawing
319// "without replacement" from a shrinking pool: remainingTotal = batchCap - settledCount,
320// remainingNamed = 3 - namedAssignedCount, and a claimant's pick = seed % remainingTotal. THAT
321// IS PEEKABLE AND GAMEABLE, even though the seed itself is honestly drand-derived and
322// unforgeable: because remainingTotal only ever shrinks as OTHER claimants settle, and Settle is
323// permissionless and callable at any later time, a claimant whose seed is already fixed (the
324// moment their pinned round publishes) can simply precompute, for every possible remainingTotal
325// from 1 to batchCap, whether that value would win them a named edition -- and then wait,
326// costing nothing, until the rest of the batch happens to settle into a state that does. The fix
327// below removes every dependency on shrinking, observable, time-varying state from the decision:
328// a claimant's own named-edition pick is `seed % namedPickRange` (`seed % batchCap` until v7
329// change 9 moved the modulus off the batch size on 2026-10-18 -- a different constant, the same
330// and only load-bearing property), against a modulus that is a
331// COMPILE-TIME CONSTANT, not a moving count, so there is nothing left to wait for -- the only
332// residual order-dependence is which of possibly several colliding claimants (bad luck, not
333// strategy: nobody can change their own pick by choosing when to settle) gets a named edition
334// that more than one of them rolled, and that is resolved by whichever Settle call lands on
335// chain first.
336//
337// A SECOND AUDIT PASS NAMED THIS PRECISELY: "settling promptly", said above and in an earlier
338// version of Settle's own comment, OVERSTATES what the residual actually rewards. It is not
339// "whoever claimed first" or "whoever cared most" -- pendingClaim.claimedAt is never consulted
340// by the tie-break, and Settle is permissionless, so a bystander (a searcher, a bribed relayer,
341// a claimant's own bot) can win a collision for whichever side pays for transaction-ordering
342// priority, regardless of who rolled or claimed first. Considered and rejected: tie-breaking on
343// claimedAt instead. It sounds like the honest fix, but it is not reachable without a worse
344// trade-off -- Settle finalizes irrevocably (it mints a real token to a real owner, who may
345// already have acted on it), so honoring an earlier claimedAt would mean either refusing to
346// finalize ANY collision candidate until every possible earlier claimant's round has published
347// and settled (reintroducing exactly the wait-and-see window section 10 and this fix exist to
348// close, now around a deadline instead of a pool size) or reaching back to UNDO an already-
349// settled mint (which nothing in this realm, or any grc721-based realm in this project, has ever
350// claimed to be able to do, and which would be a strictly worse property than the one being
351// fixed). What actually holds, stated precisely rather than aspirationally: nobody can improve
352// THEIR OWN odds by choosing when to settle (the property section 10 is about, and the property
353// this fix actually guarantees); the residual is an ordinary transaction-ordering race over a
354// LOW-PROBABILITY, LOW-STAKES event (three slots in a batch of 24, decided among whichever
355// handful of claimants happen to collide -- WRITTEN BEFORE CHANGES 1, 9 AND 10, and "low
356// probability" and "a handful" are the two words that stopped being true; see the paragraph
357// below), the same shape of race every permissionless action on
358// every chain is already subject to, not a new hole this design opened. See Settle's own comment
359// for the arithmetic. v7 CHANGES 9 AND 10 (both 2026-10-18) MAKE THAT RACE MORE FREQUENT AND SAY
360// SO: at namedPickRange = 10 roughly 18 of 60 claimants held a named-pointing pick for three slots,
361// and at change 10's eight slots it is 48 of 60 (60 * 8/10) chasing eight, so collisions are now
362// the NORMAL CASE rather than an unlucky one -- about 40 claimants draw a named pick and fall
363// through to an ordinary draw. The race is unchanged in SHAPE -- first settle takes it, nobody can
364// move his own pick -- and it rewards promptness, not lateness. The honest statement of that cost,
365// and the reason the rate was raised anyway, are at namedPickRange.
366//
367// SETTLE CAN NOW PAY FOR ITS OWN BEACON RELAY, OWNER-APPROVED 2026-10-15, THE DAY AFTER THIS FILE
368// WAS FIRST WRITTEN. The two-phase roll above depends on beacon/v2 actually having the pinned
369// round's drand signature by the time Settle runs, and until this change that was somebody
370// else's problem: a relay has to happen, by a human or a bot, running outside this realm and
371// outside this project's control, or a claimant who has already spent their one-and-only roll is
372// stuck mid-mint through no fault of their own -- found live, not hypothetically, while building
373// this file: the project's own beacon was observed ~2 days stale. The fix the owner asked for
374// ("can we have the mint transaction pay the beacon though") is `SettleWithSignature` below: the
375// player's own app fetches the pinned round's signature from drand's public API (the same
376// `https://api.drand.sh/<chain>/public/<round>` this file's own test fixtures are fetched from)
377// and passes it straight through; Settle relays it itself, in the same transaction, before
378// computing the result. One transaction, and the player pays its gas -- TO THE NETWORK, same as
379// any other transaction's gas, NOT to this project, so it never touches the free-mint promise
380// (MINT.md section 14: "gas is still a payment, just not to us... the operator takes no stake").
381// Three things this buys, in the owner's own order of importance:
382//
383// - IT COSTS THE PROJECT NOTHING. The alternative to a player-paid relay is an operator-run one
384// -- a cron job or a bot calling beacon.Publish on a schedule, which is real recurring GNOT
385// spend with no revenue to offset it (MINT.md section 14 forecloses an unlock fee, batch one
386// is free end to end). This change deletes that cost rather than budgeting for it.
387// - A PLAYER CAN NEVER BE BLOCKED BY THIS PROJECT'S OWN HOUSEKEEPING. The stuck-mid-mint state
388// above is now unreachable for anyone willing to fetch their own round's signature: their own
389// Settle call makes the round available, with no dependency on this project, a third-party
390// relay, or anyone else's goodwill.
391// - THE BEACON GETS FRESHER AS A SIDE EFFECT, FOR EVERYONE ELSE USING IT. beacon/v2 is a shared,
392// permissionless, on-demand record (see its own package doc) -- a round this realm's own
393// claimants cause to be published is published for every other consumer of that beacon too,
394// not just for this realm.
395//
396// THREE THINGS WORKED OUT BY QUERYING THE LIVE CHAIN, NOT ASSUMED, because this is new
397// cross-realm write surface and this project's own history (the SPLASH beacon migration,
398// MEMORY.md) is built from exactly the kind of interrealm detail that gets guessed wrong:
399//
400// 1. WHAT beacon.Publish DOES ON AN ALREADY-PUBLISHED ROUND. Fetched byte-for-byte from the live
401// deployed realm itself (gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2) via
402// the public onyx-1 RPC's `vm/qfile` query, 2026-10-15, and diffed identical against this
403// project's own vendored copy at realms/_vendor/beacon/v2/beacon.gno -- it PANICS, it does not
404// no-op: `func Publish(cur realm, round int64, sigHex string) { if Published(round) { panic
405// ("beacon: round already published") } ... }`. SettleWithSignature therefore checks
406// `beacon.Published` itself BEFORE ever calling Publish, exactly once, and never calls Publish
407// at all if the round is already there -- calling it unconditionally would make this function
408// fail every time a relay (another claimant's own SettleWithSignature, or anyone else) beat it
409// to the punch, which given Settle's own permissionless, callable-by-anyone design is an
410// ordinary and expected race, not an edge case.
411// 2. THAT Publish IS REACHABLE CROSS-REALM AS `beacon.Publish(cross(cur), round, sigHex)`, and
412// that it carries no caller-identity check at all for this function to trip over. Confirmed
413// two ways: the live source pulled in point 1 shows Publish's body never reads `cur` or calls
414// `cur.Previous()` -- it is permissionless by whoever relays, not merely by whichever address
415// -- and this exact call shape is already proven working, not merely plausible, by this
416// file's OWN test harness: garden_test.gno's `publishAs` helper has called
417// `beacon.Publish(cross(cur), round, sigHex)` from this package since the day this file was
418// written, and every test that calls it passes. SettleWithSignature's own call is the
419// identical shape with `cur` being Settle's own realm parameter instead of a test helper's.
420// (`cur.Previous()` inside beacon.Publish would resolve to THIS realm, garden/v5, when called
421// this way rather than to the human wallet that called Settle -- a real interrealm fact worth
422// recording even though Publish does not currently branch on it: if beacon/v2 ever grows a
423// caller-identity check, re-read this paragraph before assuming Settle's relay still works.)
424// 3. GAS. Measured on this toolchain with r/splash/beacon/zz_gas_test.gno's own method
425// (`gno test -run TestZZGas -print-runtime-metrics=true -v`, net of this package's own fixed
426// per-test-call overhead): a bare Settle today costs ~6.2M gas; beacon.Publish alone costs
427// ~18.5M gas on THIS realm's measurement (the same order of magnitude as beacon/v2's own
428// documented 16,970,375, re-measured rather than quoted because toolchain and realm storage
429// both shift it). SettleWithSignature's worst case -- the round not yet published at all --
430// is therefore on the order of ~25M gas total, call it 1% of the 3e9 block gas limit and a
431// few hundredths of a GNOT at this chain's gas price. Reasonable: it is roughly four times a
432// bare Settle, not an order of magnitude more, it still leaves room for well over a hundred
433// such settlements in a single block, and it replaces a cost this project would otherwise have
434// paid itself, recurringly, forever, to keep the beacon fresh by some other means.
435//
436// THE SHAPE: TWO ENTRY POINTS, ONE SHARED CORE, NOT ONE FUNCTION WITH AN OPTIONAL SIGNATURE.
437// `Settle(cur realm, claimant address)` is UNCHANGED in behaviour and UNCHANGED in signature --
438// every existing test and every existing caller that already knows (or does not care) whether the
439// round is published keeps working exactly as before, at exactly its old gas cost, with no
440// signature to fetch or supply. `SettleWithSignature(cur realm, claimant address, sigHex string)`
441// is new, and is for a caller that HAS a signature and wants its transaction to publish the round
442// if nobody else already has. A Gno realm's exported functions take a fixed argument list -- there
443// is no overloading, and an optional/variadic last argument would still force every caller,
444// including every test and the app's own simpler "it's already published" path, to pass an empty
445// string they have no use for, for a case (publishing) that is the EXCEPTION, not the rule, since
446// most settlements land after some earlier claimant or relay has already published the shared
447// round. Two named entry points make the exception visible in the function name instead of buried
448// in an argument, and cost nothing extra: both share one unexported `settleCore`, so there is
449// exactly one copy of the draw-and-mint logic, not two drifting in parallel. And it is SAFE for
450// either one to be called in either actual on-chain state: Settle still refuses cleanly, with its
451// original message, if the round genuinely is not published yet; SettleWithSignature still
452// succeeds, with its supplied signature simply going unused, if the round turns out to already be
453// published by the time it runs (point 1's race) -- neither caller has to know in advance which
454// state they are in, they only have to know whether they personally HAVE a signature to offer.
455package garden
456
457import (
458 "crypto/sha256" // v7 change 8: seedFor's hash. See seedFor and the "WHY FNV-1a HAD TO GO" block.
459 "strconv"
460 "strings"
461 "time"
462 "unicode" // v6 fix 1: v4's name charset allowlist, carried back whole. See validNameRune.
463 "unicode/utf8" // v6 fix 1: rune counting, never len(). See validNameLength.
464
465 "gno.land/p/nt/avl/v0"
466 "gno.land/p/nt/grc721/v0"
467 "gno.land/p/nt/seqid/v0"
468
469 "chain"
470 "chain/banker"
471 "chain/runtime/unsafe"
472
473 beacon "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2"
474)
475
476// ============================================================================
477// THE THREE SALE CONSTANTS. CARRIED OVER, UNCHANGED, FROM garden/v4's 2026-10-11 DECISION, AS
478// PLACEHOLDERS FOR THIS NEW REALM. Nobody has asked the owner to re-confirm these specifically
479// for v5 -- re-decide or re-confirm them before ever deploying this file; see gnomod.toml's own
480// checklist item 0. init() below still gates them exactly as v4's assertFeeConfig does, so an
481// edit to a value that cannot work fails at deploy rather than succeeding silently forever.
482// ============================================================================
483const (
484 saleFeeRecipient = "g1ylpe9juk8cafgf9rt09wzuz85ug2lyfk5metyv" // gnotchi-opponent, v4's choice
485 saleFeeRateBps = int64(500) // 5% of the declared price
486 saleFeeFloorUgnot = int64(1_000_000) // 1 GNOT flat minimum per sale
487)
488
489// ============================================================================
490// Constants that are NOT tunable, and never will be: see the package doc's "out of scope"
491// section for why v5 has no SetRule surface at all.
492// ============================================================================
493
494const (
495 collectionName = "Gnotchi gnomes"
496 collectionSymbol = "GNOME"
497
498 // adminAddress is the only address AdminAllow/AdminDisallow will accept. Compiled in, with
499 // no transfer function, for the same reason v4 has none: an admin-transfer path is a second
500 // authority path, and a lost admin key merely freezes the allowlist, which is recoverable by
501 // redeploying a v6 -- not a disaster.
502 //
503 // THIS IS THE ONYX TESTNET DEPLOY KEY `gnotchi-deploy`, which is also the namespace this
504 // realm deploys under (see gnomod.toml) and the only key on this host with enough GNOT to
505 // pay for the admin calls -- AdminAllow and Settle both cost fees, and v4's old key
506 // (g1r4aw43..., the retired splash-deploy) is down to dust, so compiling that address in
507 // here would have produced an admin that could not afford to act. Correct for onyx, wrong
508 // for anywhere real. Change it before deploying anywhere that matters; see gnomod.toml.
509 adminAddress = "g1nq99m3h9ed7vqve6ajpr3qwy2ls252m8jnundm"
510
511 // numEyes / numNoses / the hat and coat catalogues: THE SAME FOUR-DIMENSIONAL FACE SPACE
512 // garden/v4 uses (SPRITES.md's Phase 1: 10 eyes x 8 noses x 4 hats x 4 coats = 1,280 looks).
513 // Nothing here is a new catalogue; eyesWeights/noseWeights below are a weighted DRAW over
514 // the same ids v4 already validates, per ECONOMY.md section 3 and MINT.md section 13's
515 // counted-rarity decision ("no tiers... rarity becomes population frequency counted on
516 // chain"). hat and coat are drawn UNIFORMLY (25% each): ECONOMY.md section 3 only specifies
517 // weights for the "permanent pair" (eyes, nose), and BATCH-ONE.md section 4a says so
518 // explicitly -- "assigning the section 3 percentages to hat and coat as well... is real work
519 // this document does not do... flagged here as the next thing to specify". A uniform draw is
520 // the honest placeholder until that work happens, not a guess dressed up as one.
521 numEyes = int64(10)
522 numNoses = int64(8)
523
524 // hourSeconds/daySeconds: unchanged from v4.
525 hourSeconds = int64(3600)
526 daySeconds = int64(86400)
527
528 // ---- care-mechanic constants, fixed at v4's own deploy DEFAULTS (v4's ruleBands table).
529 // See the package doc: v5 deliberately carries no tunable-rules admin surface.
530 numPlots = 3 // acorns.plots
531 plantCost = int64(1) // acorns.plantCost
532 feedCost = int64(1) // acorns.feedCost
533 acornsStart = int64(10)
534 meterMax = int64(10)
535 patCheer = int64(3)
536 hungryAtOrBelow = int64(2)
537
538 growHours = int64(24)
539 patCooldownHours = int64(12)
540 tinkerCost = int64(5)
541 levelBase = int64(50)
542 levelStep = int64(25)
543 sulkyPenaltyPct = int64(10)
544 harvestPerPlot = int64(3)
545 feedBelly = int64(4)
546 bellyDropEveryHours = int64(3)
547 cheerDropEveryHours = int64(6)
548 dailyCareCap = int64(60)
549
550 // acornReserve: v4's fix for the acorn softlock (AUDIT-DEPLOYED.md G9), carried over
551 // unchanged. See v4's own constant for the full argument; the short version is that a
552 // SaveDay batch that spends acorns may never end with fewer than this and nothing planted.
553 acornReserve = plantCost + feedCost
554
555 happyBonusPct = int64(10)
556 happyWindowHours = int64(2)
557 sulkyAfterHours = int64(72)
558 maxLevel = int64(20)
559 typeAtLevel = int64(5)
560
561 xpPat = int64(5)
562 xpFeed = int64(5)
563 xpHarvestPerPlot = int64(10)
564 xpTinkerTask = int64(15)
565
566 maxActionsPerCall = 32
567 maxBackfillHours = int64(48)
568 caredWindowDays = uint(64)
569
570 maxGnomesPageSize = 100
571 maxListingsPageSize = 100
572
573 // maxKinEdgesPageSize: v7, the same shape and the same number as the two above, for
574 // KinEdgesPage. The whole table is 84 rows, so one page of 100 returns all of it -- the
575 // cap exists to keep the enumerator's shape identical to its two siblings rather than
576 // because 84 needs bounding.
577 maxKinEdgesPageSize = 100
578
579 // ---- the sale. Shape constants, unchanged from v4.
580 ugnotDenom = "ugnot"
581 maxPriceUgnot = int64(1_000_000_000_000_000)
582 maxFeeRateBps = int64(10_000)
583 maxListingDays = int64(30)
584
585 moodHungry = "hungry"
586 moodSulky = "sulky"
587 moodHappy = "happy"
588 moodContent = "content"
589
590 actionPat = "pat"
591 actionFeed = "feed"
592 actionPlant = "plant"
593 actionHarvest = "harvest"
594 actionTinker = "tinker"
595
596 typeGardener = "gardener"
597 typeExplorer = "explorer"
598 typeTinkerer = "tinkerer"
599
600 // ============================================================================
601 // THE MINT: allowlist, one roll per wallet, the batch, the lock, the drand pin. Every
602 // number below is a single named constant, each trivially changeable, per the brief's own
603 // instruction for the lock specifically and applied here to the pin offset too.
604 // ============================================================================
605
606 // batchCap: BATCH-ONE.md's own roster, AT THE SIZE DECISIONS.md SETTLED ON 2026-10-17
607 // ("Batch One becomes 60", superseding "Batch One stays 24" from earlier the same day) --
608 // 60 gnomes, total, forever, this batch. 52 ordinary plus eight named one-of-ones
609 // (numNamedEditions below -- three until v7 change 10, which is also the change that
610 // explains why the split is worth writing out: three of 24 was 12.5% of a batch and three of
611 // 60 was 5%, so change 1 moved this number and silently moved a rarity nobody was discussing.
612 // Eight of 60 is 13.3%). Closed the instant mintedCount reaches this; nothing in this file
613 // can ever raise it (no OpenBatch -- see the package doc's "out of scope" section).
614 //
615 // THIS IS v7 CHANGE 1 AND IT IS ONE CONSTANT. Everything that reads it -- Claim's cap gate,
616 // BatchStatus's third return, the kinship table's 1..60 key space and OwnerTokens' unpaginated
617 // bound -- reads it from here and nothing in this file hardcodes 24 or 60 beside it. THE
618 // NAMED-EDITION PICK NO LONGER READS IT (v7 change 9, 2026-10-18): its modulus is
619 // namedPickRange, so changing the batch size no longer silently changes the odds on any
620 // edition. See namedPickRange for why that coupling was accidental and had to go. THE ROSTER
621 // SIZE IS STILL NOT INDEPENDENT OF THIS NUMBER AS A DESIGN QUESTION, though -- change 10 is
622 // the record of batchCap moving and numNamedEditions needing to follow it by hand, which is a
623 // judgement about what fraction of a batch should be special, not a coupling in the code.
624 batchCap = int64(60)
625
626 // numNamedEditions: THE SIZE OF THE NAMED ROSTER, AND v7 CHANGE 10 (2026-10-18). Hob, Thistle,
627 // Wick, Tally, Glim, Vigil, Spruce and Yonder (namedEditionOrder below), cap ONE EACH. "Cap 1"
628 // is not a different mechanism from any other cap, MINT.md section 3's own point -- it is the
629 // same characterOwner.Has check any other population cap would use, just at the number one.
630 //
631 // THREE UNTIL 2026-10-18, when the owner said "If we are doing 60 there should be more
632 // specials than 3." The arithmetic behind him: three of 24 was 12.5% of the batch, and v7
633 // change 1 took the batch to 60 without touching the roster, which cut it to 5% as a side
634 // effect of a decision about batch size. Eight of 60 is 13.3% -- the proportion restored, not
635 // a new one invented. The five new entries are EXISTING, ALREADY VALIDATED ART renamed, exactly
636 // as Hob (the gilded gnome) and Thistle (the Viking) were; BATCH-ONE.md section 7 names each
637 // one, says which character it is and justifies the name. No art was drawn for change 10.
638 //
639 // EDITING THIS NUMBER IS FOUR EDITS, NOT ONE, AND THE FOURTH IS THE ONE A REVIEWER MISSES.
640 // Three are the tables that must stay the same length and in the same order:
641 // namedEditionOrder, namedEditionDisplayNames and reservedNameKeys (which is
642 // [numNamedEditions + 1] -- the extra slot is the genesis gnome). The FOURTH IS PROSE THAT
643 // SAYS THE COUNT OUT LOUD IN A PANIC MESSAGE: List refuses an early sale with "... is one of
644 // the eight named one-of-ones and cannot be listed until ...", and the test suite's lockedMsg
645 // helper mirrors that string byte-for-byte, so the count appears in two more places than the
646 // tables. gnomod.toml item (4) names the roster as well and is a build input, not source.
647 // TestReservedNamesCoverEveryNamedEdition and TestNamedEditionDisplayNamesPairWithTheirCanonicalKeys
648 // pin the tables against each other and against BATCH-ONE.md's own spelling.
649 //
650 // AND THIS NUMBER MAY NOT GROW AGAIN WITHOUT RAISING namedPickRange: the pick is
651 // `seed % namedPickRange` and an edition at an index >= namedPickRange can never be drawn at
652 // all. 10 >= 8 holds with two residues of headroom; at eleven editions an edition would be
653 // unmintable and the realm immutable. TestNamedEditionOddsAreOneInTen asserts the inequality
654 // directly so that mistake fails a test rather than shipping.
655 numNamedEditions = 8
656
657 // namedPickRange: THE MODULUS OF THE NAMED-EDITION PICK, AND v7 CHANGE 9 (2026-10-18).
658 // settleCore draws namedPick = seedFor(round, claimant, "slot") % namedPickRange and takes the
659 // edition at that index when the result is under numNamedEditions -- so every claimant has a
660 // 1-in-10 shot at each specific one of the eight editions. The owner's instruction, in his
661 // words: "make the last few have them. The rates have to come out during a mint." He wants all
662 // of them to actually exist.
663 //
664 // THIS CONSTANT DID NOT MOVE IN v7 CHANGE 10 AND MUST NOT: change 10 raised the ROSTER from
665 // three to eight at these same owner-approved odds. A specific edition is a 1-in-10 shot now
666 // exactly as it was at three, because the odds on a specific edition are what this constant
667 // controls and the roster size is what numNamedEditions controls. What the two compose to did
668 // move, and the table below is recomputed for eight rather than edited in place.
669 //
670 // THE ARITHMETIC, REPRODUCED RATHER THAN QUOTED, because this constant exists for nothing
671 // except what it does to these numbers. Each claimant draws independently, so for one specific
672 // edition P(never drawn by n claimants) = (1 - 1/namedPickRange)^n, and P(at least one of the
673 // k editions absent) = 1 - (1 - that)^k, at k = numNamedEditions = 8:
674 //
675 // miss one specific at least one of eight missing
676 // 1-in-60 (v5/v6), 60 claimants (59/60)^60 = 36.48% 1 - (1-0.36479)^8 = 97.35%
677 // 1-in-10, 60 claimants (9/10)^60 = 0.18% 1 - (1-0.0017971)^8 = 1.43%
678 // 1-in-10, 35 claimants (9/10)^35 = 2.50% 1 - (1-0.0250316)^8 = 18.36%
679 // 1-in-10, 30 claimants (9/10)^30 = 4.24% 1 - (1-0.0423911)^8 = 29.29%
680 //
681 // AT THREE EDITIONS THE SAME TWO MIDDLE ROWS WERE 0.54% AND 7.32%; the per-edition column is
682 // identical and only the composition changed, which is the whole shape of change 10. Expected
683 // number of the eight that actually mint in a full batch is 8 * (1 - (9/10)^60) = 7.986.
684 //
685 // The last two rows are there because a batch that does not sell out is the realistic bad case,
686 // and the fourth row is the one to quote at anybody planning the allowlist: A BATCH THAT ONLY
687 // HALF FILLS (30 of 60 claimed) LOSES AN EDITION ALMOST THREE TIMES IN TEN. That is the price
688 // of eight rather than three and it is not small. It is still better than a FULL batch was at
689 // 1-in-60 under either roster size. TestNamedEditionOddsAreOneInTen pins the rate against the
690 // realm's own seedFor, and TestNamedEditionsAtSixtyAreNotGuaranteed pins the residual 1.43%.
691 //
692 // WHY THIS IS NOT batchCap, which is what the modulus was until 2026-10-18 and the only reason
693 // the odds were ever 1-in-60. THAT COUPLING WAS ACCIDENTAL: batchCap was reached for because it
694 // was the handy compile-time constant to hand, and it made the rarest-asset rate a side effect
695 // of a batch size decided for entirely unrelated reasons -- 24, then 60 (DECISIONS.md
696 // 2026-10-17), each change silently moving odds nobody was discussing. The two numbers answer
697 // different questions: batchCap is HOW MANY GNOMES EXIST, namedPickRange is HOW LIKELY A NAMED
698 // EDITION IS, and numNamedEditions is HOW MANY THERE ARE TO BE LUCKY ABOUT. A future batch of
699 // 200 must not quietly make Hob a 1-in-200 shot, so the modulus is its own constant and moving
700 // any one of the three leaves the others alone -- v7 change 10 is the proof that this
701 // separation was worth having: the roster tripled in size and this line did not move. What was
702 // load-bearing about batchCap -- and the only thing this must inherit from it -- is that the
703 // modulus is a COMPILE-TIME CONSTANT and not a count of what remains; see the package doc's "a
704 // real flaw found and fixed" section for why that distinction is the whole safety property. Any
705 // value here must be >= numNamedEditions or an edition becomes unreachable (10 >= 8, which was
706 // 10 >= 3 before change 10 and now has only two residues of slack -- a NINTH edition would be
707 // fine, an ELEVENTH would be permanently unmintable), and TestNamedEditionOddsAreOneInTen
708 // checks that too.
709 //
710 // WHY RAISING THE RATE IS SAFE WHERE THE REMOVED GUARANTEE WAS NOT, which is the reason this
711 // change is a constant rather than a mechanism. A claimant's namedPick is a PURE FUNCTION of
712 // the pinned drand round and his own address -- fixed before he can act and unchangeable by
713 // anything he does. WAITING DOES NOT HELP, RUSHING DOES NOT HELP, WATCHING THE COUNTERS DOES
714 // NOT HELP: THERE IS NOTHING TO POSITION FOR. The forcing rule removed by AUDIT-01 RED-1 fired
715 // on a SHRINKING PUBLIC OBSERVABLE (slotsLeft = batchCap - settledCount, computable from
716 // BatchStatus and CharacterOwner), and that is exactly what made it positionable -- a wallet
717 // that simply never claimed until 57 were settled took an edition with certainty. This keys on
718 // nothing observable at all: 60 -> 10 changes a probability and introduces no new input, no new
719 // state, no new branch and no new reader.
720 //
721 // AND THE COST, STATED HONESTLY BECAUSE IT IS REAL, AND CHANGE 10 MAKES IT MUCH BIGGER. At
722 // 1-in-10 with three editions, roughly 18 of 60 claimants (60 * 3/10) held a pick pointing at a
723 // named edition and only three could have one. AT EIGHT EDITIONS IT IS 48 OF 60 (60 * 8/10) --
724 // four claimants in five draw a named-pointing pick, eight of them can keep it, and ABOUT FORTY
725 // LOSE THE RACE and fall through to an ordinary draw. Among claimants whose picks collide,
726 // WHOEVER SETTLES FIRST TAKES IT: v5's and v6's already-documented transaction-ordering race,
727 // in the same place and under the same comment, byte-for-byte unchanged -- but it is now the
728 // NORMAL EXPERIENCE OF A CLAIMANT rather than an unlucky edge case. It rewards PROMPTNESS
729 // rather than lateness, which is the healthy direction and the exact opposite of what the
730 // removed rule did. And because the draw is public arithmetic over public inputs, a claimant
731 // CAN WORK OUT AFTER THE FACT that he was in line for Hob and lost the race -- and after change
732 // 10, forty claimants per batch can. Accepted twice, with eyes open: the alternative on offer
733 // at change 9 was a 74% chance that one of three pieces of commissioned art was never minted at
734 // all, and the alternative at change 10 was leaving five finished, validated characters
735 // (ARCHETYPES.md section 3's own Legendary line) unminted while the batch grew around them.
736 //
737 // THIS IS STILL NOT A GUARANTEE, and the source must not start reading as though it were.
738 // ~1.43% of full batches, ~18.4% of 35-gnome batches and ~29.3% of half-filled 30-gnome
739 // batches miss at least one of the eight.
740 namedPickRange = 10
741
742 // lockCaredDays: the eight named one-of-ones cannot be listed until they have this many cared
743 // days; every ordinary gnome trades from day one. One named constant, and nothing else in this
744 // file assumes any particular value.
745 //
746 // SETTLED 2026-10-19 AT 14, DOWN FROM 30. MINT.md section 12/13's thirty was explicitly a
747 // RECOMMENDATION -- "Option A... this is a recommendation, not a decision" -- and sat unconfirmed
748 // through v5, v6 and most of v7. The owner delegated it ("ok whatever you think") after being
749 // asked three times.
750 //
751 // WHY 14 AND NOT 30. The lock exists so a one-of-one cannot be flipped the hour it is minted;
752 // it does not exist to make the holder prove devotion. Thirty DISTINCT cared days before an
753 // asset becomes usable is a month of attendance, and the realistic outcome of that is not a
754 // more committed holder but an abandoned gnome and a dead listing -- the care ledger already
755 // measures attention, and the resale lock is a speed bump, not a second measure of it.
756 //
757 // SAID PRECISELY, because the first draft of this comment said "unbroken attendance" and that
758 // is a mechanism this realm does not have (AUDIT-03 GREEN-4): the lock counts TOTAL distinct
759 // cared days, not consecutive ones, so no streak has to be unbroken and a missed day costs a
760 // day rather than a restart. The floor is still 14 calendar days, because recordCaredDay banks
761 // at most one distinct UTC day at a time -- and lastCaredDayForOwner means an owner holding
762 // several gnomes must spend each of those days on THIS one.
763 // Fourteen is still long enough that flipping is not a strategy and short enough that somebody
764 // who wants out can get out inside a fortnight.
765 //
766 // NOT ZERO, and not "let the market decide": the eight are the scarcest things in the realm and
767 // a mint-and-flip on day one is exactly the behaviour BATCH-ONE.md's hand-read waitlist exists
768 // to keep out.
769 //
770 // TestTheSellLockIsFourteenCaredDays pins the number itself, because the two behavioural tests
771 // around List are both written relative to this constant and therefore pass at ANY value --
772 // verified by mutation on 2026-10-19: setting it to 99 broke nothing.
773 lockCaredDays = int64(14)
774
775 // ============================================================================
776 // v6 FIX 1: THE NAME. maxNameLen is v4's own constant at v4's own value, kept in step with
777 // app/js/config.js's NAME_MAX_LEN (24) by hand, because Gno has no way to read that file at
778 // build time -- the same hand-maintained pairing v4 documented and v6 inherits. The app's
779 // NAME_RULE.MAX_LEN must equal this number; TestMaxNameLenMatchesTheAppsRule pins the value
780 // so a change here fails a test rather than silently diverging from the one rule the app
781 // shows a player before they pay gas.
782 // ============================================================================
783 maxNameLen = 24
784
785 // pinOffsetSeconds / pinOffsetRounds: how far into drand's future Claim pins a round, sized
786 // AS SECONDS, from drand evmnet's own period (beacon.PeriodSeconds, re-exported from this
787 // project's own bls254 package, not restated here), per MINT.md section 10.5's explicit
788 // instruction -- never as a round count checked only against a block count, which is the
789 // exact dimensional trap this project's own history (the SPLASH raffle shipping with a 100x
790 // wrong round/block ratio, MEMORY.md) says cost real time once already. 60 seconds is twenty
791 // drand rounds at the 3-second period CurrentRound() at Claim time is comfortably able to
792 // pin as "not yet signed by anyone, including drand itself" -- see Claim's own comment for
793 // why the margin matters even though CurrentRound() is pure arithmetic over block time, not
794 // a relay-dependent estimate.
795 pinOffsetSeconds = int64(60)
796)
797
798// pinOffsetRounds is a const expression over an imported const (beacon.PeriodSeconds), which Gno
799// permits across package boundaries exactly as it would within one; TestPinOffsetIsTwentyRounds
800// pins the arithmetic rather than trusting the comment above.
801const pinOffsetRounds = pinOffsetSeconds / beacon.PeriodSeconds
802
803// kindEnd-style ratchet machinery: deliberately absent. See the package doc.
804
805// hatOptions / coatOptions: v4's validHats/validCoats catalogue, as an ORDERED array rather than
806// a set, because Settle needs to draw a POSITION, not merely validate one. Order is frozen the
807// moment this realm deploys, exactly as v4's own numEyes/numNoses ordering is frozen -- a part
808// may be redrawn; it may never be reordered, removed or renumbered (v4's own comment on this).
809var hatOptions = [4]string{"red", "purple", "green", "navy"}
810var coatOptions = [4]string{"blue", "teal", "orange", "brown"}
811
812// eyesWeights: ECONOMY.md section 3, verbatim -- "eyes across 4 colours at 15%, 3 at 9%, 2 at
813// 5%, 1 at 3%" -- 4+3+2+1 = 10 entries = numEyes, and 4*15+3*9+2*5+1*3 = 100.
814// TestWeightTablesSumToOneHundred pins the sum rather than trusting the arithmetic in this
815// comment; drawWeighted's own fallback additionally makes a drift here fail safe, not silently.
816var eyesWeights = [10]int64{15, 15, 15, 15, 9, 9, 9, 5, 5, 3}
817
818// noseWeights: ECONOMY.md section 3 says only "nose similar across 8 shapes" and gives no exact
819// percentages -- BATCH-ONE.md section 4a names this directly as real work nobody has done yet.
820// This is a SHAPE-FAITHFUL INTERPRETATION, not a transcription: four tiers of two shapes each,
821// 20/20/15/15/10/10/5/5, the same big-gap-then-smaller-gaps shape eyesWeights has, scaled to 8
822// entries and summing to 100. Flagged here, not quietly presented as sourced, so the owner or a
823// future author can replace it with real numbers the moment ECONOMY.md section 3 gets them.
824var noseWeights = [8]int64{20, 20, 15, 15, 10, 10, 5, 5}
825
826// ============================================================================
827// v7 CHANGES 2, 3, 4, 5 AND 6: THE TRAIT TABLES. TRAITS.md sections 1, 2.2, 3.2, 3.3, 4.2, 5.1.
828// Ids on chain, NAMES OFF CHAIN -- the precedent v6 already set for parts (EyesCount(4) returns
829// a number; nothing on chain says what eye type 4 looks like), applied to all of this. The
830// thirty-two public names, the seventeen secret names, the eight signs and the two label-word
831// columns all stay in TRAITS.md where FUNNY.md can still revise them weekly. A realm cannot be
832// edited, and revisable wording is the one thing that must never go on one.
833// ============================================================================
834
835const (
836 // numAxes: the FIVE rolled axes, in the order the dominance permutation indexes them --
837 // 0 temperament, 1 obsession, 2 bond, 3 fear, 4 boast. Born-under is NOT one of them
838 // (TRAITS.md section 1.6: "not in the trait vector, not in the dominance order, not in the
839 // label rule"), which is why bornUnder has no slot in `dominance` and can therefore never be
840 // loud, never be latent, and never appear in the label -- by construction, not by discipline.
841 numAxes = 5
842
843 // axisTotalWeight: all six weight tables below sum to exactly this. TRAITS.md section 1's
844 // whole point and the direct answer to BATCH-ONE.md:120 -- a 64-wide cumulative table has NO
845 // REMAINDER TO GET WRONG, and 64 divides 2^64 so the draw carries exactly zero modulo bias.
846 // TestEveryAxisTableSumsToSixtyFour pins all six, the way v6's own
847 // TestWeightTablesSumToOneHundred pins its two.
848 axisTotalWeight = int64(64)
849
850 // numSecrets: TRAITS.md section 3.2's pool, after the owner's 2026-10-17 cut to seventeen.
851 // SEVENTEEN IS NOT A TUNING PARAMETER; IT IS A PRECONDITION of the first-seventeen probe in
852 // secretFor -- 17 is PRIME, so every stride in 1..16 is coprime to it and the probe walk
853 // visits all seventeen ids exactly once. An eighteen-secret pool would let a stride share a
854 // factor with the modulus, so the walk could cycle without reaching a free id, turning an
855 // unreachable panic into a reachable one that aborts a claimant's one and only roll.
856 // TestSecretPoolSizeIsPrime says exactly that.
857 numSecrets = 17
858
859 // firstDistinctCount: TRAITS.md section 3.2.2, agreed by the owner 2026-10-17. "The first
860 // seventeen mints draw seventeen distinct secrets... From token 18 onward the draw is the
861 // full seventeen with duplicates free." It is load-bearing and not a nicety: on a pure draw,
862 // P(at least one secret missing from a batch of 60) = 36.9%, so more than one batch in three
863 // could not spell the acrostic of section 3.2.1 -- the thing nobody is allowed to break.
864 firstDistinctCount = int64(17)
865
866 // careThresholdFloor / careThresholdSpan: TRAITS.md section 5.1's roll, threshold = 14 + u
867 // with u a uniform 0-31 draw, range 14-45 inclusive and flat. v7 STORES u, NOT THE
868 // THRESHOLD: they are the same five bits either way, and storing the draw keeps the +14 floor
869 // in one named place instead of baked into sixty stored numbers, so a future batch's realm can
870 // use a different floor without Batch One's stored values meaning something different.
871 // 32 divides 256 and 2^64, so there is no modulo bias.
872 careThresholdFloor = int64(14)
873 careThresholdSpan = int64(32)
874
875 // The four Top Trumps stats, TRAITS.md section 4.2's budget: 22 points across four stats,
876 // each 1-10, with at least one >= 8 and at least one <= 3. 670 four-tuples sum to 22; 546 of
877 // them satisfy both constraints. See legalStatLines and init().
878 numStats = 4
879 statPointTotal = int64(22)
880 statMin = int64(1)
881 statMax = int64(10)
882 statHighAtLeast = int64(8)
883 statLowAtMost = int64(3)
884
885 // numBornUnder: TRAITS.md section 1.6's eight signs. Rolled, not derived -- section 1.6's own
886 // recommendation and the only option open to this realm, because a derived sign is a function
887 // of the adoption week and sixty gnomes settling inside one window would all derive the SAME
888 // sign, which is not a clan system. The question stays live for Batch Two, where minting may
889 // be spread across a year; deferring costs nothing, because a future batch's realm can derive
890 // while Batch One's stays rolled and nobody's sign changes.
891 numBornUnder = 8
892)
893
894// The six 64-wide weight tables, transcribed from TRAITS.md sections 1.1-1.6, each summing to
895// exactly axisTotalWeight. Entry order is FROZEN the moment this realm deploys, exactly as
896// numEyes/numNoses ordering already is -- an entry may be reweighted for a future batch; it may
897// never be reordered, removed or renumbered, because the stored id is what pins each gnome to
898// what he actually rolled.
899var temperamentWeights = [6]int64{13, 11, 12, 10, 10, 8} // section 1.1: Blithe, Grudgeful, Fretful, Insufferable, Theatrical, Stony
900var obsessionWeights = [8]int64{12, 10, 9, 9, 7, 6, 6, 5} // section 1.2: Cheese, Fence, Book, Hat, Spade, Lantern, Carrot Row, Moss
901var bondWeights = [6]int64{13, 12, 11, 10, 10, 8} // section 1.3: Pibbs, Stone, Fennick, Middle Plot, Gran, Tin
902var fearWeights = [6]int64{14, 12, 11, 10, 10, 7} // section 1.4: Beetles, Being Forgotten, The Dark, Being Laughed At, Slugs, Shine
903var boastWeights = [6]int64{12, 11, 11, 11, 10, 9} // section 1.5: Badger, Oak, Wheel, Frost, Row, Mend
904var bornUnderWeights = [8]int64{12, 10, 9, 9, 8, 6, 5, 5} // section 1.6: Carrot, Bramble, Acorn, Nettle, Clover, Toadflax, Ember, Thistle
905
906// secretTargetKind / secretTargetID: TRAITS.md section 3.2's "Undercuts" column, AS IDS ONLY --
907// the name and the line itself live off chain, where the register is still being revised
908// (FUNNY.md). kind 0 means the secret undercuts a BOAST, 1 means a FEAR; the id is the axis id
909// within that axis. Paired by index: secret i undercuts (secretTargetKind[i], secretTargetID[i]).
910//
911// TestSecretTargetsMatchTheAllocation pins the three facts section 3.2 states about this table
912// and that LINEAGE.md section 3.3's arithmetic depends on: twelve secrets target a boast and
913// five target a fear; EVERY ONE of the six boasts is the target of exactly two; and the fears
914// are Beetles 2, Being Forgotten 1, Being Laughed At 1, Slugs 1, The Dark 0 and Shine 0. A
915// transcription slip in either array would move LINEAGE.md section 3.3's per-edge 0.0489 and
916// TRAITS.md section 3.3's population 61.7% without changing anything observable, which is
917// exactly the kind of error that needs a test rather than an eye --
918// TestSecretWeightingHitsSixtyOnePercent is the second, behavioural lock on the same door.
919//
920// A DECLARED CONSEQUENCE OF PUTTING THIS TABLE ON CHAIN, recorded here so the first person to
921// notice finds the reasoning rather than reporting it: the two arrays publish, in this realm's
922// own public source, which boast or fear each secret id undercuts, so an off-chain observer
923// combining Secret/Boast/Fear can determine that a given gnome's secret targets his own boast.
924// That is not a name, not a definition and not a rarity count, so it is inside TRAITS.md section
925// 0 rule 2 -- and v6 already settles the precedent in the same direction, because eyesWeights is
926// both "internal" per ECONOMY.md section 3 and published verbatim in v5's deployed source.
927// "Internal" means NOT PRINTED ON A CARD, not absent from the source.
928var secretTargetKind = [numSecrets]int64{1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1}
929var secretTargetID = [numSecrets]int64{0, 0, 1, 2, 0, 5, 3, 1, 4, 2, 5, 3, 3, 1, 0, 4, 4}
930
931const (
932 // secretTargetKindBoast / secretTargetKindFear: the two values secretTargetKind holds, named
933 // rather than written as 0 and 1 at the comparison site in secretWeightsFor.
934 secretTargetKindBoast = int64(0)
935 secretTargetKindFear = int64(1)
936
937 // secretMatchMultiplier: TRAITS.md section 3.3, "base weight 1 for all seventeen, multiply by
938 // 8 if the secret's target matches the gnome's rolled boast or rolled fear". One multiplier,
939 // one comparison, no second table. The dial was recomputed at seventeen secrets and x8 stayed
940 // the recommendation: it puts 61.7% of gnomes on a secret that undercuts something they say
941 // out loud, where x4 gives 44.9% and x16 gives 76.0%. The reason not to go higher is that a
942 // system which ALWAYS punctures the boast is a system whose output you can predict from the
943 // card.
944 secretMatchMultiplier = int64(8)
945
946 // secretBaseWeight: the other half of the same rule.
947 secretBaseWeight = int64(1)
948)
949
950// legalStatLines: TRAITS.md section 4.2's 546 legal stat lines, ENUMERATED AT init() FROM THE
951// RULE rather than transcribed as a literal table, and packed four stats to an int64. See
952// buildLegalStatLines and init().
953//
954// WHY ENUMERATED AND NOT A 546-ROW LITERAL -- auditability, not bytes: a transcription error in a
955// 546-row table is undetectable by eye, and the rule it transcribes is four comparisons.
956// TestLegalStatLinesIsExactly546 pins the count, TestEveryLegalStatLineSumsToTwentyTwoAndIsSpiky
957// pins the property of every row, and TestLegalStatLineOrderIsStable pins that the enumeration
958// order is the one this realm deployed with -- because the draw indexes into it, so the order is
959// frozen the moment v7 deploys, exactly as numEyes/numNoses ordering already is.
960//
961// WHY AT init() AND NOT INSIDE settleCore: 10,000 iterations once, at deploy, instead of on every
962// claimant's one transaction. The named risk, recorded rather than hidden: v5's own deploy used
963// 131.9M gas of a 160M --gas-wanted, which is not a comfortable margin, so MEASURE THIS BEFORE
964// DEPLOYING (DESIGN.md section 3.6). If it does not fit, the cheap fallback is to build the table
965// lazily on the first Settle, paid once by whoever settles first; that is not the default because
966// it makes one arbitrary claimant pay for everybody and puts a conditional in settleCore.
967var legalStatLines []int64
968
969// namedEditionOrder: BATCH-ONE.md section 7's eight names, in the fixed order Settle's
970// namedPick indexes into. The order carries no meaning -- it is not a ranking -- it only has to
971// be fixed, which it is, forever, the moment this realm deploys.
972//
973// THE FIRST THREE ARE v5's AND v6's, IN v5's ORDER, AND THE FIVE ADDED BY v7 CHANGE 10 ARE
974// APPENDED RATHER THAN INTERLEAVED. That is deliberate and it is the only thing about this
975// ordering that is a decision: appending leaves hob/thistle/wick at indexes 0/1/2, so every
976// worked example in this file's comments, in DESIGN.md and in the test suite that says "pick 0 is
977// Hob" is still true, and the diff against v6 stays readable. Nothing reads this array by index
978// except settleCore's own `namedEditionOrder[namedPick]`, which does not care.
979var namedEditionOrder = [numNamedEditions]string{
980 "hob", "thistle", "wick",
981 "tally", "glim", "vigil", "spruce", "yonder",
982}
983
984// namedEditionDisplayNames: BATCH-ONE.md section 7's eight names AS THEY ARE WRITTEN -- "Hob,
985// Thistle, Wick, Tally, Glim, Vigil, Spruce, Yonder", the form this file's own documentation uses
986// throughout -- paired by index with namedEditionOrder above.
987//
988// WHY A SECOND ARRAY RATHER THAN JUST CAPITALISING namedEditionOrder, which is the change a
989// reviewer will reach for first: namedEditionOrder's values are not labels, they are KEYS.
990// settleCore writes characterOwner.Set(character, ...) and tests characterOwner.Has(candidate)
991// with those exact strings, and the public CharacterOwner(name) reader does a raw tree Get with
992// no case folding at all -- so CharacterOwner("hob") resolving is a chain fact that an
993// off-chain caller already depends on, and init() registers the genesis gnome under the matching
994// lowercase key "gaffer". Capitalising namedEditionOrder would move those keys to "Hob" while
995// leaving "gaffer" lowercase, break CharacterOwner("hob") for every existing caller, and it is
996// unfixable after deploy. The keys stay canonical.
997//
998// This array exists ONLY to supply the gnome's `name`, which is a DISPLAY STRING and has always
999// been a different thing from its canonical key -- see canonicalNameKey's own comment on why the
1000// stored form and the uniqueness key are deliberately two strings. nameOwner is still keyed by
1001// canonicalNameKey(name), which folds "Hob" to "hob", so reservedNameKeys and the uniqueness
1002// index are untouched by this: no new mechanism, just the display side of a separation that was
1003// already here. TestNamedEditionDisplayNamesPairWithTheirCanonicalKeys pins the pairing in both
1004// directions, so an edit to either array that breaks it fails a test rather than shipping a
1005// one-of-one whose display name canonicalises to somebody else's key.
1006var namedEditionDisplayNames = [numNamedEditions]string{
1007 "Hob", "Thistle", "Wick",
1008 "Tally", "Glim", "Vigil", "Spruce", "Yonder",
1009}
1010
1011// namedEditionDisplayName maps a named edition's canonical character key to its display name.
1012// Used by settleCore's mint and mirrored by the test fabricator, so there is exactly one copy of
1013// the mapping. Returns the input unchanged for anything that is not one of the eight editions --
1014// which is unreachable from settleCore (it only ever passes a namedEditionOrder entry) and is a
1015// pass-through rather than a panic because this is a label lookup, not a security boundary.
1016func namedEditionDisplayName(character string) string {
1017 for i, canonical := range namedEditionOrder {
1018 if canonical == character {
1019 return namedEditionDisplayNames[i]
1020 }
1021 }
1022 return character
1023}
1024
1025// genesisGnomeName: DECISIONS.md 2026-10-15 settles the MECHANISM (minted to this realm's own
1026// address, non-transferable, never listable, separate from the batch) and until 2026-10-19 never
1027// settled a NAME anywhere in this project's documents -- "Genesis" was a placeholder that had
1028// been read as a decision for four days.
1029//
1030// SETTLED 2026-10-19 as SPRIG, and **RE-SETTLED 2026-10-20 as GAFFER**, which is what v8 is for.
1031//
1032// v7's argument for Sprig still reads well and was wrong on one axis it never checked: "it does
1033// not collide with any of the eight reserved names, any born-under sign, or any noun in LORE.md's
1034// fauna or flora." Every one of those tests is INTERNAL. None of them asks what the word already
1035// means to somebody outside this repository -- and `Sprig` is a funded product-research SaaS
1036// (formerly UserLeap), so to the startup and product audience this project is pitched at, the
1037// software reading arrives first. The owner caught it: *"I don't really care too much about the
1038// minted guys because obviously it's random. But I do care about the main guy."* He is right that
1039// the asymmetry matters. The sixty are drawn by a beacon and belong to whoever mints them; a name
1040// collision there is a private matter. THIS gnome fronts everything -- the group, the page, the
1041// reply to a stranger -- so his name is the only one that has to survive contact with the world.
1042//
1043// WHY GAFFER, and the test it passes that Sprig failed: eight candidates were searched on
1044// 2026-10-20 and all eight were already somebody's app (NAMES.md 3b). "Find an unused word" is
1045// not achievable, so the test is not "is it taken" but **"which meaning arrives first for our
1046// reader"**. For a British or Irish reader "the gaffer" is the old boy in charge, instantly, and
1047// the only software of that name is studio-niche VFX lighting. It also DESCRIBES him: the one
1048// gnome outside the batch, owned by nobody, who is the village rather than a villager. Runner-up
1049// was Pogle, whose sole namesake is Pogles' Wood (BBC, 1965) -- a clash so in-register it is
1050// closer to an ancestor. Sprig's own runner-up, Pip, was dropped for reading as a diminutive of
1051// somebody, and that reasoning still holds: this gnome is nobody's.
1052//
1053// NOT CHANGED, deliberately: `Spruce`, which collides with a decentralised-identity company that
1054// won an Ethereum Foundation grant and is therefore the only clash INSIDE this industry. It is one
1055// of the sixty, the owner said he does not care about those, and a realm is not a place to act on
1056// an unasked-for opinion. Recorded here because the next reader will wonder.
1057//
1058// CHANGED FROM v5 IN ONE RESPECT, deliberately: v5's comment ended "it is read by nothing else
1059// in this file", which was true there and is NOT true here. v6 fix 1 writes this string into the
1060// genesis gnome's own `name` field at init() and registers its canonical key in nameOwner, so
1061// changing it also changes which name is taken. It must still canonicalise to one of
1062// reservedNameKeys below, or an ordinary gnome could be named "Gaffer" and pose as it --
1063// TestGenesisNameIsReserved pins exactly that, so an edit here that breaks the pairing fails a
1064// test rather than shipping.
1065const genesisGnomeName = "Gaffer"
1066
1067// reservedNameKeys: the canonical name keys NO ORDINARY GNOME MAY EVER TAKE -- the eight named
1068// one-of-ones (BATCH-ONE.md section 7's roster, re-read rather than assumed: Hob, Thistle, Wick,
1069// and v7 change 10's Tally, Glim, Vigil, Spruce, Yonder) plus the genesis gnome. Checked by
1070// SetName; see assertNameAssignable.
1071//
1072// THE ARRAY LENGTH IS [numNamedEditions + 1] AND THAT "+ 1" IS THE GENESIS GNOME, so adding a
1073// roster entry without adding its key here is a COMPILE ERROR rather than a silent hole -- which
1074// is the one piece of luck in change 10's four-places-in-step problem and the reason this file
1075// states the length arithmetically instead of writing 9.
1076//
1077// WHY THIS EXISTS SEPARATELY FROM THE nameOwner UNIQUENESS INDEX, which is the subtle part and
1078// the reason a reviewer should not "simplify" one of the two away: the uniqueness index only
1079// protects a name that is ALREADY TAKEN BY A LIVING TOKEN. Hob's cap-one edition may not have
1080// been minted yet -- its namedPick is a pure function of a drand round nobody has rolled -- and
1081// for the whole window before it mints, nameOwner has no "hob" key at all. Without this list,
1082// the FIRST ordinary claimant could name their gnome "Hob", take the key, and the real Hob would
1083// then mint into a collection where its own name already belongs to an impostor. Reservation
1084// covers the pre-mint window; uniqueness covers the post-mint one. Both are needed, and together
1085// they are the "two locks on a door" discipline this file already uses for the genesis gnome.
1086//
1087// Every entry is already in canonical form (lowercase, single-spaced, trimmed), and
1088// TestReservedNameKeysAreCanonical pins that -- a non-canonical entry here would be a key
1089// canonicalNameKey could never produce, i.e. a reservation that silently protects nothing.
1090var reservedNameKeys = [numNamedEditions + 1]string{
1091 "hob", "thistle", "wick",
1092 "tally", "glim", "vigil", "spruce", "yonder",
1093 "gaffer",
1094}
1095
1096// ============================================================================
1097// v7 CHANGE 7: THE KINSHIP WEB, STRUCTURE ONLY. LINEAGE.md section 2.4 and section 6's
1098// OVERRULED box.
1099// ============================================================================
1100//
1101// The owner's own words, and the document's own correction: "what rarity and bloodline is good,
1102// stop trying to nerf everything, as long as we don't charge people money, what difference does
1103// it make, we haven't sold anything." -- and: "The corrected position: store the web on chain...
1104// The one argument that survives, and it is about timing, not safety: a realm cannot be edited,
1105// and the 84 relation sentences in section 2 are hours old while the comic register is still
1106// being revised weekly. Store the STRUCTURE -- who is related to whom, which is settled -- and
1107// keep the WORDING off chain where it can still be improved."
1108//
1109// SO: the 84 (from, to, type) rows, as ids. NOT the 84 label sentences, NOT the thirteen lots,
1110// NOT their members, NOT the grandparents' one-liners, NOT the card's line-name field.
1111//
1112// THE LOTS STAY OFF CHAIN DELIBERATELY, and this is the one place the OVERRULED box has to be
1113// read narrowly rather than generously. It names "the 84 relation sentences" as the wording and
1114// "who is related to whom" as the structure; it does not extend to lot membership. And section
1115// 6.5 -- which the box says explicitly SURVIVES and "is the part that was doing the real work
1116// all along" -- forbids LineOf(tokenID) and LineCount(line) BY NAME. A realm that stored the
1117// thirteen lots would be a realm that could answer both, so v7 does not store them. The line a
1118// card prints ("Out of Pell's lot") is derived by the client from LINEAGE.md section 1.1's
1119// published table joined on BatchIndex, which is the same ids-on-chain/names-off-chain split as
1120// everything else here.
1121//
1122// AND THE "SECOND CODE PATH" -- LINEAGE.md section 6.2's third named cost -- DOES NOT EXIST.
1123// This table is deploy-time-constant state. No function writes it. No function in settleCore,
1124// SaveDay, Buy, List, Wake or SetName reads it. The only code that touches it is the two
1125// enumerators and the verification tests. There is nothing to drift, nothing to gate, and
1126// nothing that could accidentally branch on it, because no code path that does anything
1127// consequential can see it.
1128//
1129// WHY THIS IS ALSO LINEAGE.md section 6.4's COMMITMENT, satisfied better than the repo hash it
1130// recommends: section 6.4 wanted sha256 of the section 2.4 block published in the repo, dated,
1131// before the batch opens, and noted the recommendation "got slightly less comfortable at 60"
1132// because "a stranger who mints #47 has no relationship with the repo and no reason to trust an
1133// mtime". THE DEPLOYED REALM IS THE COMMITMENT: dated by a block height, signed by a known key,
1134// immutable, and readable by that stranger without anybody telling him where to look. The
1135// sequence section 6.4 insists on ("take the hash before the mint opens") is satisfied
1136// automatically, because this realm must deploy before AdminAllow can let anybody claim.
1137
1138// The nine kinship type ids, FROZEN ALPHABETICALLY rather than in LINEAGE.md's own frequency
1139// order (cousin 39, neighbour 11, sibling 10, rival 9, not-speaking 5, in-law 4, disputed 3,
1140// latent-kin 2, taken-in 1), because the frequency order is a property of THIS batch and would
1141// change for Batch Two, whereas alphabetical is stable and self-checking. LINEAGE.md section 2.4
1142// declares the vocabulary closed -- "the nine type values are the complete vocabulary; section 5
1143// adds no tenth, and the rebuild at 60 added no tenth either" -- so the ids are safe to freeze.
1144// TestKinTypeNamesAreAlphabeticalAndNine pins both facts.
1145const (
1146 kinCousin = int64(0)
1147 kinDisputed = int64(1)
1148 kinInLaw = int64(2)
1149 kinLatentKin = int64(3)
1150 kinNeighbour = int64(4)
1151 kinNotSpeaking = int64(5)
1152 kinRival = int64(6)
1153 kinSibling = int64(7)
1154 kinTakenIn = int64(8)
1155
1156 numKinTypes = 9
1157 numKinEdges = 84
1158)
1159
1160// kinTypeNames: the nine words, paired by index with the ids above. THIS IS A JUDGEMENT CALL
1161// AGAINST THE STRICTEST READING OF "WORDING OFF CHAIN", declared rather than slipped in, and the
1162// argument for it is three-part: the nine words are a CLOSED vocabulary LINEAGE.md declares
1163// complete and that the rebuild at 60 did not extend; they are one word each, carrying none of
1164// the comic register FUNNY.md revises weekly (the thing being revised is the 84 sentences, and
1165// not one of those is here); and without them the table is 84 triples of integers that no client
1166// can render into anything a human reads -- "#14 is a 7 of #6" is not a family.
1167//
1168// IF THE OWNER DISAGREES, THE CHANGE IS ONE FUNCTION. Drop KinTypeName and TotalKinTypes and the
1169// nine words live in LINEAGE.md keyed by id exactly as the thirty-two axis names do. The table
1170// and its enumerators are unaffected and nothing else moves.
1171var kinTypeNames = [numKinTypes]string{
1172 "cousin", "disputed", "in-law", "latent-kin", "neighbour",
1173 "not-speaking", "rival", "sibling", "taken-in",
1174}
1175
1176// kinEdge: one row of LINEAGE.md section 2.4, structure only. `from` is always the LOWER batch
1177// index; every edge is undirected except the three `disputed` rows, where `from` is the gnome
1178// doing the claiming -- and the claimer is the lower index in all three, which is that
1179// document's own constraint on how a dispute may be written rather than a coincidence.
1180type kinEdge struct {
1181 from int64 // 1..60, a batchIndex (NOT a token id -- see batchIndex's own comment)
1182 to int64 // 1..60, a batchIndex
1183 typ int64 // 0..8, one of the nine ids above
1184}
1185
1186// kinEdges: LINEAGE.md section 2.4's fenced block, transcribed row by row IN ITS OWN ORDER,
1187// labels omitted. Frozen at deploy; NOTHING IN THIS FILE WRITES TO IT, EVER.
1188//
1189// WRITTEN AS 84 ONE-LINE STRUCT LITERALS WITH NAMED TYPE CONSTANTS, NOT AS PACKED INTEGERS. A
1190// row would squeeze into 16 bits (from 6, to 6, typ 4) and all 84 into eleven int64s, a tenth of
1191// the bytes. It is not worth it: the whole value of this table is that a reviewer can diff it
1192// against LINEAGE.md section 2.4 line by line, and {1, 9, kinSibling} does that where 0x0409
1193// does not. Transcribing 84 rows is the single highest-risk clerical act in building v7, and the
1194// mitigation is that the source looks like the document PLUS TestKinEdgesMatchTheDocument and
1195// TestAllThirtyTwoOriginalEdgesSurvive -- not that the source is small.
1196var kinEdges = [numKinEdges]kinEdge{
1197 {1, 9, kinSibling},
1198 {1, 10, kinNeighbour},
1199 {1, 25, kinCousin},
1200 {2, 19, kinInLaw},
1201 {2, 21, kinRival},
1202 {2, 51, kinNeighbour},
1203 {2, 57, kinRival},
1204 {3, 12, kinDisputed},
1205 {3, 15, kinCousin},
1206 {3, 30, kinCousin},
1207 {4, 7, kinRival},
1208 {4, 11, kinSibling},
1209 {4, 19, kinCousin},
1210 {4, 28, kinCousin},
1211 {5, 12, kinCousin},
1212 {5, 16, kinNeighbour},
1213 {5, 18, kinTakenIn},
1214 {5, 33, kinCousin},
1215 {6, 13, kinRival},
1216 {6, 14, kinSibling},
1217 {6, 31, kinCousin},
1218 {6, 60, kinRival},
1219 {7, 15, kinCousin},
1220 {7, 23, kinLatentKin},
1221 {7, 43, kinNeighbour},
1222 {8, 17, kinCousin},
1223 {8, 22, kinNeighbour},
1224 {8, 24, kinCousin},
1225 {8, 45, kinInLaw},
1226 {9, 14, kinNotSpeaking},
1227 {9, 16, kinCousin},
1228 {9, 50, kinNeighbour},
1229 {10, 21, kinSibling},
1230 {10, 37, kinCousin},
1231 {10, 38, kinNeighbour},
1232 {11, 12, kinDisputed},
1233 {11, 13, kinCousin},
1234 {12, 18, kinCousin},
1235 {13, 20, kinInLaw},
1236 {13, 44, kinDisputed},
1237 {14, 22, kinCousin},
1238 {14, 53, kinNeighbour},
1239 {15, 17, kinNotSpeaking},
1240 {15, 23, kinCousin},
1241 {16, 20, kinCousin},
1242 {16, 25, kinCousin},
1243 {17, 35, kinCousin},
1244 {18, 23, kinNeighbour},
1245 {18, 34, kinCousin},
1246 {19, 27, kinCousin},
1247 {20, 26, kinCousin},
1248 {20, 39, kinNeighbour},
1249 {21, 24, kinRival},
1250 {21, 37, kinCousin},
1251 {22, 32, kinCousin},
1252 {23, 29, kinCousin},
1253 {24, 36, kinCousin},
1254 {24, 48, kinNotSpeaking},
1255 {25, 26, kinSibling},
1256 {27, 28, kinCousin},
1257 {29, 30, kinRival},
1258 {31, 32, kinCousin},
1259 {33, 34, kinNeighbour},
1260 {35, 36, kinRival},
1261 {37, 38, kinCousin},
1262 {39, 40, kinSibling},
1263 {40, 41, kinCousin},
1264 {41, 42, kinCousin},
1265 {42, 43, kinNotSpeaking},
1266 {42, 49, kinNotSpeaking},
1267 {44, 45, kinSibling},
1268 {45, 46, kinCousin},
1269 {46, 47, kinCousin},
1270 {47, 48, kinInLaw},
1271 {49, 50, kinSibling},
1272 {50, 51, kinCousin},
1273 {51, 52, kinRival},
1274 {52, 56, kinLatentKin},
1275 {53, 54, kinSibling},
1276 {54, 55, kinCousin},
1277 {55, 56, kinCousin},
1278 {57, 58, kinSibling},
1279 {58, 59, kinCousin},
1280 {59, 60, kinCousin},
1281}
1282
1283// ============================================================================
1284// Storage
1285// ============================================================================
1286
1287// plot: unchanged from v4. plantedAt == 0 is the empty state; readyAt is stored, not computed,
1288// so a future change to growHours (there is none possible in this file, but the shape is kept
1289// for the day a v6 adds tuning back) could never reach backwards into an already-planted plot.
1290type plot struct {
1291 plantedAt int64
1292 readyAt int64
1293}
1294
1295// gnome is the whole persisted pet. EVERY FIELD HERE HAS AN EXPORTED READER -- v4's own rule,
1296// carried over verbatim: a realm cannot grow a second chance at this after deploy.
1297//
1298// NOT HERE, DELIBERATELY: a hat/coat the OWNER chose (nothing is chosen at mint any more;
1299// hat/coat are ROLLED exactly as eyes/nose are, and are exactly as permanent as eyes/nose always
1300// were in v4 -- there has never been a function anywhere in this family of realms that changes a
1301// gnome's hat or coat after mint). v5's own comment also listed "a name" here; v6 fix 1 is
1302// precisely the correction of that, and `name` is below.
1303//
1304// character is "" for every ordinary gnome and one of namedEditionOrder's eight strings for a
1305// named one-of-one. eyes/nose/hat/coat are the sentinel -1/-1/""/"" for a named edition: its
1306// look is NOT a point in the ordinary 1,280-look space, it is the art pack's own character art
1307// (Hob's crown and gold coat, Thistle's helmet and axe, Wick's lantern, and v7 change 10's five:
1308// Tally's trowel, Glim's halo, Vigil's slate-and-ink coat, Spruce's top hat and moustache,
1309// Yonder's bandana and parrot), which this contract
1310// does not need to know the pixels of -- only that Character(tokenID) names it, so the app can
1311// render the right thing. A named edition's parts are therefore never counted in
1312// eyesCounts/noseCounts/hatCounts/coatCounts: it is already as rare as a thing can be, at cap
1313// one, counted instead through characterOwner.
1314type gnome struct {
1315 hat string
1316 coat string
1317 eyes int64
1318 nose int64
1319 character string
1320
1321 // name: v6 FIX 1. The gnome's own name, as its owner typed it, byte-for-byte -- this realm
1322 // NEVER rewrites it (see canonicalNameKey's own comment on why the stored form and the
1323 // uniqueness key are deliberately two different strings).
1324 //
1325 // "" means UNNAMED, which is the state EVERY ordinary gnome is minted in and stays in until
1326 // its owner calls SetName. That is not an oversight to be fixed by defaulting it to
1327 // something: a gnome with no name yet is honestly nameless, and inventing "Gnome #7" on
1328 // chain would make a fabricated string indistinguishable from a chosen one forever.
1329 //
1330 // For the THREE NAMED ONE-OF-ONES and for the GENESIS GNOME this is set ONCE, at mint, to
1331 // their own character name, and SetName refuses them permanently (their identity is the
1332 // whole point of them -- see SetName's own guard). So `name != ""` does not imply "an owner
1333 // chose this"; `character == ""` is the test for that.
1334 name string
1335
1336 // settledRound: v6 FIX 2. The drand round whose published signature produced this gnome's
1337 // parts -- the single number that makes "provably unrigged" (ECONOMY.md section 1) checkable
1338 // by a holder in one state read instead of a transaction-log search. Written once, in
1339 // settleCore, from the pendingClaim's own pinned round; never written again by anything.
1340 //
1341 // 0 means "not produced by a roll at all", which is true of exactly one token for the whole
1342 // life of this realm: the genesis gnome, minted directly in init() and never rolled for. A
1343 // caller can tell the two apart without ambiguity because round 0 is not a real drand round
1344 // and never will be.
1345 settledRound int64
1346
1347 // ------------------------------------------------------------------------------------
1348 // v7: THE PERSONALITY. Fourteen fields, every one of them written ONCE, in settleCore's
1349 // commit block, from the pure drawRoll's output, and never written again by anything.
1350 // Every one has an exported reader EXCEPT careThreshold -- v4's own "every field has a
1351 // reader" rule, carried by v5 and v6, honoured here with one declared exception (see that
1352 // field's own comment and DESIGN.md section 2.6).
1353 //
1354 // FOR THE GENESIS GNOME EVERY ONE OF THESE IS 0, including batchIndex, which is the same
1355 // "never produced by a roll" sentinel settledRound = 0 already is and is true of exactly
1356 // one token for the life of this realm. batchIndex = 0 is therefore unambiguous: the batch
1357 // is 1-indexed and there is no gnome #0.
1358 //
1359 // STORED AS EIGHTEEN EXPLICIT int64 FIELDS, NOT PACKED INTO ONE uint64, which is what
1360 // TRAITS.md section 7 literally says ("51 bits, in one uint64"). Three reasons, in order
1361 // of weight: (1) fourteen shifts and fourteen masks is fourteen chances at a silent
1362 // off-by-one, and a wrong mask returns a PLAUSIBLE number -- temperament 2 instead of 6 --
1363 // with nothing failing, which is precisely the failure mode BATCH-ONE.md:120 warns about,
1364 // introduced into the one function the warning is about, to save 136 bytes a gnome; (2) a
1365 // bit layout is the most load-bearing thing you can ship, load-bearing in exactly the way
1366 // section 7 itself rejects for stored seeds and stored table indexes -- once temperament
1367 // lives at bits 0-2, it lives there forever and every client, indexer and future realm has
1368 // to agree; (3) v6's whole discipline is explicit named fields with explicit readers.
1369 // Section 7's "in one uint64" was making the argument that THE WHOLE PERSONALITY IS SMALL,
1370 // which is true and is still true: it is 65 bits of information (section 7's 51 plus 8 for
1371 // five explicit dominance slots instead of a Lehmer code, plus 6 for batchIndex) and it
1372 // still fits in one machine word. The ENCODING is eighteen fields.
1373 //
1374 // The five rolled axis ids. TRAITS.md sections 1.1-1.5, each drawn from its own 64-wide
1375 // weight table. Axis order here is axis order everywhere: 0 temperament, 1 obsession,
1376 // 2 bond, 3 fear, 4 boast -- which is what `dominance` below indexes.
1377 temperament int64 // 0-5, TRAITS.md section 1.1
1378 obsession int64 // 0-7, section 1.2
1379 bond int64 // 0-5, section 1.3
1380 fear int64 // 0-5, section 1.4
1381 boast int64 // 0-5, section 1.5
1382
1383 // dominance: the whole permutation of the five axis indexes, loudest first. TRAITS.md
1384 // section 2.2 stores the WHOLE ORDER and not the state labels, and that is the decision
1385 // this realm is making permanently: WHERE THE LOUD/LATENT BOUNDARY FALLS IS AN OFF-CHAIN
1386 // READING of this number. Two states versus three is therefore a constant in the writing
1387 // layer, fully reversible after deploy in either direction, for free -- BUT ONLY IF ALL
1388 // FIVE SLOTS ARE STORED. A realm that stored just "loudest, second-loudest" (5 x 4 = 20
1389 // values, 5 bits, which is what a reviewer optimising for bytes reaches for) would be
1390 // storing the two-state design itself, and the third state would be UNRECOVERABLE
1391 // FOREVER: there would be no stored fact about the other three axes' order for a future
1392 // stimulus engine to re-split. "Two states now is reversible; a third state now is a spec
1393 // nobody can honour" is only true of a realm that keeps all five slots.
1394 //
1395 // The cost of five explicit slots, stated honestly: they can represent invalid states
1396 // ([0,0,0,0,0]) where a 0-119 Lehmer index could not -- so assertRoll checks the
1397 // permutation before anything is committed. The reason to pay that cost rather than spend
1398 // 7 bits on a factorial-base index is that the index needs A CODEC ON BOTH SIDES OF THE
1399 // WIRE, two codecs that must agree forever over a convention nobody has written down, in
1400 // the one function MINT.md section 9 names as needing its own audit pass above all others.
1401 // Five explicit slots have no codec at all: settleCore stores exactly what it computed,
1402 // Dominance returns exactly what is stored, and there is nothing in between for an
1403 // off-by-one to live in.
1404 dominance [numAxes]int64 // a permutation of 0..4; slot 0 is the loudest
1405
1406 // secret: 0-16, TRAITS.md section 3.2. The id only -- the NAME and the LINE live off chain.
1407 // Drawn by secretFor, which is weighted toward the secrets that undercut this gnome's own
1408 // rolled boast or fear, and constrained to be distinct for the first seventeen settles.
1409 secret int64
1410
1411 // The four Top Trumps stats, TRAITS.md section 4.1. Stored as four explicit values and
1412 // NEVER as the index into legalStatLines -- section 7's own reason, twice over: a stored
1413 // index makes the 546-row order permanent, so the table could never be re-derived or
1414 // re-ordered for Batch Two without re-deriving every Batch One gnome.
1415 //
1416 // NOTHING IN ANY REALM COMPARES TWO GNOMES' STATS. There is no Top Trumps game on chain:
1417 // section 4.5's tie rule is social, and v7 ships no reader that takes two token ids.
1418 pluck int64 // 1-10
1419 nous int64 // 1-10
1420 grumble int64 // 1-10
1421 appetite int64 // 1-10
1422
1423 // careThreshold: the 0-31 DRAW, not the threshold. threshold = careThresholdFloor + this.
1424 //
1425 // THE ONE FIELD IN THIS REALM WITH NO EXPORTED READER, AND THE ONLY WRITE-ONLY FIELD IN
1426 // IT. Not read by Settle after it is written, not by SaveDay, not by any reader, not by
1427 // Render. That is unusual enough that a reviewer will try to delete it as dead code --
1428 // hence this paragraph, and hence TestCareThresholdIsStoredAndInRange, which reads it
1429 // directly from the same package because that is how a write-only field is verified.
1430 //
1431 // WHY NO GETTER. Not because it hides the number: a realm's persisted object graph is
1432 // written into every node's store and nothing in Gno encrypts it, so this field is OBSCURE
1433 // RATHER THAN SECRET and this file does not rest on the optimistic reading. The reasons
1434 // are (1) GAMES.md:38's converse -- "a missing enumerator cannot be added later" has a
1435 // mirror image, A SHIPPED GETTER CAN NEVER BE REMOVED, and a getter is what the first
1436 // third-party UI finds in an hour and builds a progress bar on within a week, where a
1437 // serialized object graph is what nobody will bother with: the asymmetry in EFFORT is the
1438 // whole mechanism and it is real even though it is not a guarantee; and (2) the actual
1439 // protection is elsewhere and v7 has to honour it IN STORAGE rather than in omission --
1440 // TRAITS.md section 5.1 point 2: the promptness bonus is a Mood reading taken at the
1441 // moment of care and NEVER STORED, so nobody can tell whether a given care-day scored 1 or
1442 // 2, so nobody can pin a gnome's score closer than a factor of two. Which makes a HARD
1443 // CONSTRAINT on this realm, stated here because it is easy to violate by being helpful:
1444 //
1445 // v7 STORES NO CARE SCORE, AND STORES NOTHING THAT DISTINGUISHES A 1-POINT CARE-DAY
1446 // FROM A 2-POINT ONE. recordCaredDay is carried from v6 unchanged -- it records WHICH
1447 // UTC DAYS were cared for (caredMask, caredDayCount, lastCaredDay) and nothing about
1448 // the mood at the time. No promptMask, no promptDayCount, no per-day mood log, no
1449 // "arrived before the mood turned" counter. Any of them would collapse the
1450 // factor-of-two interval to zero and hand over the countdown that the no-getter rule
1451 // is only the SECOND line of defence against.
1452 //
1453 // WHY STORE IT AT ALL, given nothing reads it: TRAITS.md section 7 -- derive it from a
1454 // hash and the salt becomes load-bearing forever, and a gnome's threshold must never
1455 // change under him. Stored, it is pinned to the roll that produced it, verifiable by the
1456 // holder against SettledRound and drand's own public API, and independent of any off-chain
1457 // scorer's configuration. One byte per gnome buys all of that.
1458 careThreshold int64 // 0-31, the draw; the threshold is careThresholdFloor + this
1459
1460 // bornUnder: 0-7, TRAITS.md section 1.6. Rolled, stored and read back by BornUnder exactly
1461 // like any other field. WHAT MAKES IT CARD-ONLY IS THAT IT IS NOT IN `dominance` -- the
1462 // permutation is over the five axes, indexed 0-4, and born-under has no index, so it can
1463 // never be loud, never be latent and never appear in the label, by construction. Whether
1464 // the runtime is handed it is an OFF-CHAIN discipline and nothing this realm can enforce;
1465 // this is the half the realm can guarantee, and it does.
1466 bornUnder int64
1467
1468 // batchIndex: 1-60, this gnome's position in Batch One, assigned as settledCount+1 in
1469 // settleCore. 0 for the genesis gnome and for nothing else.
1470 //
1471 // IT EXISTS BECAUSE LINEAGE.md NUMBERS ITS GNOMES #1 TO #60 AND THOSE ARE NOT THIS REALM'S
1472 // TOKEN IDS. init() spends the first seqid on the genesis gnome, so the first CLAIMED
1473 // gnome is token "0000002" -- confirmed on the live v5 chain, not inferred. Without a
1474 // stored join, every client that wants to render a holder's cousins has to know (a) that a
1475 // genesis gnome exists, (b) that it consumed id 1, and (c) that seqid renders as seven
1476 // zero-padded digits: three pieces of internal realm trivia, on a realm that cannot be
1477 // edited, standing between a holder and his own family. Six bits removes all three.
1478 //
1479 // SETTLE ORDER, NOT CLAIM ORDER, and for the same reason the first-seventeen secret rule
1480 // uses settle order: token ids are assigned in settleCore, so batchIndex and the token id
1481 // advance together and never disagree. A client can cross-check (batchIndex should equal
1482 // the token's numeric id minus one) but is never required to.
1483 //
1484 // LINEAGE.md section 1.3 already licenses the consequence for Hob, Thistle and Wick: it
1485 // nominally places them at #1, #12 and #24 and says in the same breath that "no edge in
1486 // section 2 depends on it -- if the roster assigns the three one-of-ones to different ids,
1487 // swap the ids and the web is unchanged". The web attaches to batch indexes; the named
1488 // editions attach to whichever gnomes drew them. Nothing reconciles the two and
1489 // nothing needs to.
1490 //
1491 // THAT ARGUMENT CARRIES v7 CHANGE 10's FIVE NEW EDITIONS UNCHANGED, which is worth saying
1492 // because LINEAGE.md section 1.3 was written when the roster was three and still says three.
1493 // Its nominal #1/#12/#24 placement was already decorative ("no edge depends on it"), and the
1494 // 84 edges are keyed by batch index 1..60 with no reference to a character at all, so a
1495 // roster of eight needs no edge to move, no index to be reserved, and no edit to LINEAGE.md
1496 // for this file to be correct. The stale count there is a documentation lag, not a
1497 // disagreement with the chain.
1498 batchIndex int64
1499
1500 level int64
1501 gnomeType string
1502 adoptedAt int64 // unix seconds; the moment Settle minted this gnome, or init() for genesis
1503
1504 belly int64
1505 bellyAt int64
1506 cheer int64
1507 cheerAt int64
1508
1509 lastPatAt int64
1510 lastHappyEventAt int64
1511
1512 totalXp int64
1513
1514 xpGarden int64
1515 xpTinker int64
1516 harvestCount int64
1517
1518 dailyCareXp int64
1519 dailyCareDay int64
1520
1521 plots [numPlots]plot
1522
1523 lastTinkerAt int64
1524
1525 // Cared days. v4's own shape, unchanged: the chain stores FACTS (which UTC days were
1526 // cared for), never the STREAK RULE, which stays in app/js/rules.js's streakFrom. See
1527 // v4's gnome struct comment for the full argument; it applies here without edit.
1528 caredDayCount int64
1529 lastCaredDay int64
1530 caredMask uint64
1531
1532 lastActionAt int64
1533
1534 // lastWokenAt: unix seconds, the moment THIS token last became somebody's active gnome --
1535 // set at mint (settleCore), at purchase (Buy), and at every explicit Wake, always to "now" at
1536 // that moment, never anything earlier. SaveDay's backfill floor is max(lastActionAt,
1537 // lastWokenAt, now-48h): a token cannot be credited for any period before it was actually the
1538 // active one, closing the gap ATTACK-REVIEW.md goal 7 found -- v4 never needed this field
1539 // because v4 enforced one gnome per address, so there was no second token to backfill against
1540 // while this one slept. See SaveDay's own comment for the full argument.
1541 lastWokenAt int64
1542
1543 acquiredAt int64
1544 saleCount int64
1545}
1546
1547// listing: unchanged in shape from v4, now simply one of possibly SEVERAL a single owner may
1548// have standing at once (mint one, hold many) -- listings is still keyed by tokenID, which was
1549// always the right key for this and needed no change.
1550type listing struct {
1551 seller address
1552 priceUgnot int64
1553 buyer address
1554 listedAt int64
1555 expiresAt int64
1556}
1557
1558// feeConfig: unchanged from v4.
1559type feeConfig struct {
1560 recipient address
1561 rateBps int64
1562 floorUgnot int64
1563}
1564
1565// pendingClaim is the record Claim writes and Settle consumes: the permanent, irrevocable fact
1566// that this address spent its one roll, and the future drand round it is waiting on.
1567type pendingClaim struct {
1568 round int64
1569 claimedAt int64
1570}
1571
1572var (
1573 token *grc721.Token
1574 ledger *grc721.PrivateLedger
1575
1576 nextID seqid.ID
1577
1578 gnomes = avl.NewTree() // tokenID -> *gnome
1579
1580 // ownerTokens: owner address -> *[]string, every tokenID that address currently holds.
1581 // MINT.md section 2a's "hold many": v4 never needed this because v4 never let an address
1582 // hold more than one. See addOwnerToken/removeOwnerToken.
1583 ownerTokens = avl.NewTree()
1584
1585 // activeToken: owner address -> tokenID string, the ONE gnome that is awake for that
1586 // address right now. Absent means "nothing awake" -- reachable only for an address that has
1587 // never settled a claim or bought a gnome, or that has sold its only awake gnome and not yet
1588 // woken another (Buy deliberately does not auto-promote a replacement; see Buy's comment).
1589 activeToken = avl.NewTree()
1590
1591 // lastWakeDay: owner address -> int64 UTC day number, the once-per-day gate on Wake.
1592 lastWakeDay = avl.NewTree()
1593
1594 // lastCaredDayForOwner: owner address -> int64 UTC day number, the most recent calendar day
1595 // on which ANY ONE of this owner's tokens has already banked a cared day. The package doc is
1596 // explicit and unconditional: "exactly one is ever AWAKE... and only the awake one can ever
1597 // gain... a cared day" (see this file's own doc comment). lastWokenAt (see the gnome struct)
1598 // closes the BACKFILL half of ATTACK-REVIEW.md goal 7 -- a token cannot backfill a day it
1599 // spent asleep. This tree closes the LIVE half: without it, an owner could Wake(B) partway
1600 // through a UTC day already spent genuinely caring for A, then genuinely care for B too,
1601 // using only real-time, non-backfilled timestamps, and bank the SAME calendar day on BOTH --
1602 // no backfill panic ever fires, because neither action is backfilled, each is simply real
1603 // care given to whichever gnome was actually awake at the time. That still doubles the one
1604 // property this bookkeeping exists to measure (lockCaredDays' resale lock, and the
1605 // public cared-days streak), for one wallet, once per day, for as many tokens as the owner is
1606 // willing to switch through -- the exact harm goal 7 names, reached by a second, undiscovered
1607 // mechanism. recordCaredDay only ever runs, and this tree only ever advances, when the day
1608 // being recorded is NEWLY banked for this OWNER, not merely newly banked for this TOKEN.
1609 lastCaredDayForOwner = avl.NewTree()
1610
1611 balances = avl.NewTree() // owner address -> int64 acorns, per PLAYER, exactly as v4
1612
1613 listings = avl.NewTree() // tokenID -> *listing
1614
1615 // allowlist: owner address -> true. Admin-managed (AdminAllow/AdminDisallow), gates Claim
1616 // only -- MINT.md section 11: "gate issuance, never the secondary market". Buying and
1617 // selling need no entry here at all.
1618 allowlist = avl.NewTree()
1619
1620 // hasRolled: owner address -> true, PERMANENT, never removed by anything in this file. "One
1621 // roll per wallet, forever" (MINT.md section 1a/2.3), independent of whether the resulting
1622 // gnome is still held, independent of whether the address is later removed from the
1623 // allowlist. Disallowing an address after it has rolled does not and cannot un-roll it.
1624 hasRolled = avl.NewTree()
1625
1626 // pendingClaims: owner address -> *pendingClaim. Written by Claim, removed by Settle. At
1627 // most one per address, because hasRolled already refuses a second Claim from the same one.
1628 pendingClaims = avl.NewTree()
1629
1630 // eyesCounts / noseCounts / hatCounts / coatCounts: counted rarity (MINT.md section 13).
1631 // Keyed by the part's own id/name, value is a lifetime population count, incremented only by
1632 // an ORDINARY Settle (never by a named one-of-one, which is counted separately below, and
1633 // never decremented: there is no burn, and a sale moves a part without minting one).
1634 eyesCounts = avl.NewTree()
1635 noseCounts = avl.NewTree()
1636 hatCounts = avl.NewTree()
1637 coatCounts = avl.NewTree()
1638
1639 // nameOwner: v6 FIX 1, the uniqueness index. canonicalNameKey(name) -> tokenID. A key is
1640 // present if and only if exactly one living token currently carries a name that canonicalises
1641 // to it, which is what makes "names are unique across the collection" a chain fact rather
1642 // than an app convention.
1643 //
1644 // IT IS KEYED ON THE CANONICAL FORM, NOT THE TYPED FORM, and that is the whole point: with a
1645 // raw-string key, "Hob", "hob", "HOB" and "Hob " are four different keys, so uniqueness would
1646 // be worth nothing and the one-of-one reservation below could be walked straight around by
1647 // changing one letter's case. See canonicalNameKey.
1648 //
1649 // RENAMING FREES THE OLD KEY, and that is REQUIRED, not a convenience. If SetName only ever
1650 // added keys, a single token could lock up an unbounded number of names by renaming over and
1651 // over -- one gnome, one transaction each, every good name in the collection permanently
1652 // unavailable to everybody else. Freeing on rename bounds what any wallet can hold hostage to
1653 // exactly one name per token it owns, and since no address can ever hold more than batchCap
1654 // tokens, the worst case for the entire realm is 60 names held at once by the 60 tokens that
1655 // exist. See SetName's commit block, which removes before it sets.
1656 nameOwner = avl.NewTree()
1657
1658 // characterOwner: one of namedEditionOrder's eight names -> tokenID, present only once that
1659 // edition has actually been minted. Has(name) is both "has this cap-one edition been taken"
1660 // (Settle's own fallback check) and "does it exist yet" (CharacterOwner's reader) -- the same
1661 // single tree answers both questions, which is why there is no separate counter for it.
1662 characterOwner = avl.NewTree()
1663
1664 // batchIndexToToken: int64 batchIndex (as decimal) -> tokenID string. v7's join between
1665 // LINEAGE.md's #1-#60 numbering and this realm's token ids, in the direction a client cannot
1666 // compute for itself. Written once per settle, in settleCore, beside the gnome's own
1667 // batchIndex field; never removed, never rewritten -- a sale moves a token, it does not move
1668 // a batch position. See TokenByBatchIndex.
1669 batchIndexToToken = avl.NewTree()
1670
1671 // secretsDrawnMask: bit i is set once secret i has been drawn by one of the first
1672 // firstDistinctCount mints. Written only while settledCount < firstDistinctCount, read only
1673 // while batchIndex <= firstDistinctCount, NEVER CLEARED by anything -- not by a sale, not by
1674 // a rename, not by anything.
1675 //
1676 // Never-cleared is load-bearing for the monotonicity property in secretFor's own comment.
1677 //
1678 // THIS MASK IS DERIVABLE BY ANYONE, AND THAT IS ACCEPTED. AUDIT-01 YELLOW-2 corrected an
1679 // earlier version of this comment which claimed the mask was "exposed by nothing". It is
1680 // not. TraitsCsv returns `secret` AND `batchIndex` in the same call and TokensPage
1681 // enumerates every token, so one pass over the collection reconstructs this word EXACTLY:
1682 // { Secret(t) : BatchIndex(t) <= firstDistinctCount }. There is no getter for the mask and
1683 // there is no SecretCount, SecretCensus, HoldersOfSecret or IsFirstSeventeen -- but shipping
1684 // no getter does not make the value private, it only keeps it off the path of least
1685 // resistance.
1686 //
1687 // Removing Secret or BatchIndex from the readers is NOT on the table and would be the wrong
1688 // trade: the acrostic is found by READING THE SECRET NAMES, and BatchIndex is what closes
1689 // the join to LINEAGE.md's #1-#60 numbering. DESIGN.md section 5.3 already makes exactly
1690 // this argument for the kinship table -- "a third party can of course build KinOf on top of
1691 // 84 public rows; the concern was never that the data is derivable" -- and the same answer
1692 // applies here.
1693 //
1694 // SO, STATED PLAINLY BECAUSE IT HAS AN OPERATIONAL CONSEQUENCE THE KINSHIP TABLE DOES NOT:
1695 // this mask is a live input to an UNSETTLED draw (secretFor's `taken`), so it is a KNOWN
1696 // INPUT TO ANYONE CHOOSING WHEN TO SETTLE. Nobody should reason about the draw on the
1697 // assumption that a pending claimant cannot see it; he can, from two public readers, at any
1698 // moment. TRAITS.md section 3.2.2 note 3 -- an owner who can see which secrets are taken can
1699 // deduce other gnomes' secrets by elimination -- is therefore a product rule about the UI
1700 // and the marketing, not a technical guarantee, and never was one: v7's SOURCE is public too
1701 // (vm/qfile returns it verbatim), so anyone who looks finds firstDistinctCount = 17 and this
1702 // field.
1703 secretsDrawnMask uint32
1704
1705 mintedCount int64 // reservations made by Claim, gates batchCap; never decremented
1706
1707 // settledCount: completions made by Settle. INHERITED COMMENT CORRECTED, AUDIT-01 YELLOW-2 --
1708 // it read "informational only, see BatchStatus", which was true in v6 and is FALSE IN V7.
1709 // Here it drives batchIndex (settledCount+1, the kinship join's key) and the first-seventeen
1710 // secret window (batchIndex <= firstDistinctCount). With mintedCount it is one of the two
1711 // most load-bearing numbers in the realm; nothing here is informational.
1712 //
1713 // It briefly drove a third thing -- slotsLeft (batchCap - settledCount), the trigger of the
1714 // named-edition guarantee written 2026-10-17. That guarantee was removed on 2026-10-18 (see
1715 // settleCore and AUDIT-01 RED-1) precisely because this counter is public through
1716 // BatchStatus, which made the trigger watchable off chain.
1717 settledCount int64
1718
1719 genesisTokenID string // set exactly once, in init()
1720
1721 rulesFrozen = true // always true: there are no rules to freeze or unfreeze in v5. See AdminState.
1722
1723 fee feeConfig
1724
1725 transferGate string
1726)
1727
1728func init(cur realm) {
1729 if !address(adminAddress).IsValid() {
1730 panic("garden: adminAddress is not a valid address; fix the constant before deploying")
1731 }
1732
1733 cfg := feeConfig{
1734 recipient: address(saleFeeRecipient),
1735 rateBps: saleFeeRateBps,
1736 floorUgnot: saleFeeFloorUgnot,
1737 }
1738 assertFeeConfig(cfg, cur.Address())
1739 fee = cfg
1740
1741 token, ledger = grc721.NewToken(collectionName, collectionSymbol, 0, cur)
1742 ledger.RegisterExtension(&saleGateExtension{})
1743
1744 // v7 change 4: TRAITS.md section 4.2's legal stat set, enumerated once from the rule. See
1745 // legalStatLines and buildLegalStatLines. Done here rather than in settleCore so no
1746 // claimant pays for it; done from the rule rather than from a 546-row literal so a
1747 // transcription error is not possible.
1748 legalStatLines = buildLegalStatLines()
1749 if len(legalStatLines) != numLegalStatLines {
1750 // Unreachable: the enumeration is deterministic and TestLegalStatLinesIsExactly546 pins
1751 // the count. Named so init() is correct on its own terms -- the same discipline v4's Buy
1752 // uses for its own unreachable checks, and the stakes here are a DEPLOY that silently
1753 // ships a different draw space from the one the tests measured.
1754 panic("garden: DEPLOY GATE: the legal stat line enumeration did not produce " +
1755 strconv.Itoa(numLegalStatLines) + " rows; TRAITS.md section 4.2's rule and this code disagree")
1756 }
1757
1758 // AUDIT-02 YELLOW-D: THE ONE INVARIANT THE NAMED-EDITION ROSTER CANNOT SURVIVE LOSING, now a
1759 // gate rather than only prose, an array length and two tests. settleCore draws
1760 // `namedPick = seedFor(...) % namedPickRange` and takes namedEditionOrder[namedPick] only when
1761 // namedPick < numNamedEditions -- so if the roster ever grows past the modulus, EVERY EDITION AT
1762 // AN INDEX >= namedPickRange IS PERMANENTLY UNMINTABLE. See numNamedEditions and namedPickRange,
1763 // which both say this in words; this is the same statement in a form a DEPLOY cannot ignore.
1764 //
1765 // WHY AN OFF-BY-ONE HERE IS WORSE THAN A WRONG ROW COUNT, which is the reason it earns a gate of
1766 // its own next to the enumeration check above: a miscounted stat enumeration ships a draw space
1767 // that differs from the measured one -- bad, and the stated reason that gate exists -- but it is
1768 // a DIFFERENT distribution, which means it is visibly wrong in the traits the first settles
1769 // produce. AN ELEVENTH EDITION IS NOT WRONG, IT IS SILENT. Claim succeeds, Settle succeeds,
1770 // assertRoll passes, BatchStatus reports a namedCap that counts it, CharacterOwner answers
1771 // not-found exactly as it does for an edition nobody has drawn yet, and the roster reads complete
1772 // in every reader. The only symptom is a one-of-one that never appears -- indistinguishable from
1773 // the 1-in-550 ordinary bad luck this realm deliberately accepts -- and it is noticed when the
1774 // batch closes, by which time this realm is immutable and a piece of commissioned art has nowhere
1775 // to go. A wrong row count is caught by looking at a gnome; this is caught by nothing.
1776 if numNamedEditions > namedPickRange {
1777 panic("garden: DEPLOY GATE: numNamedEditions (" + strconv.Itoa(numNamedEditions) +
1778 ") exceeds namedPickRange (" + strconv.Itoa(namedPickRange) +
1779 "); the named-edition pick is seed % namedPickRange, so every edition at an index >= namedPickRange " +
1780 "can never be drawn and would be permanently unmintable with no error anywhere -- raise " +
1781 "namedPickRange (and re-derive the odds at its declaration) or shrink the roster before deploying")
1782 }
1783
1784 // THE GENESIS GNOME. Minted here, directly, to this realm's own address -- never through
1785 // Claim, never counted against batchCap, never allowlist-gated, because it is not a roll and
1786 // not part of the batch (DECISIONS.md 2026-10-15). No wallet holds this realm's private key,
1787 // so no transaction can ever be signed as its owner: List, Buy and Wake all additionally
1788 // refuse genesisTokenID by name as a second, redundant lock, and saleGateExtension's
1789 // OnTransfer refuses it unconditionally as a third -- the same "two locks on a door that
1790 // already has one" discipline v4's own sale gate uses, because the claim "belongs to
1791 // nobody" is only as strong as the weakest reachable path around it.
1792 id := nextID.Next()
1793 genesisTokenID = id.String()
1794 me := cur.Address()
1795 if err := ledger.Mint(me, grc721.TokenID(genesisTokenID)); err != nil {
1796 panic(err)
1797 }
1798 now := time.Now().Unix()
1799 gnomes.Set(genesisTokenID, &gnome{
1800 character: "gaffer",
1801 // v6 FIX 1: the genesis gnome carries its own name from the instant this realm exists.
1802 // It is the one token whose name was never chosen by an owner and never can be -- no
1803 // wallet holds this realm's key, and SetName refuses any token with a character anyway.
1804 name: genesisGnomeName,
1805 eyes: -1,
1806 nose: -1,
1807 level: 1,
1808 adoptedAt: now,
1809 belly: meterMax,
1810 bellyAt: now,
1811 cheer: meterMax,
1812 cheerAt: now,
1813 lastActionAt: now,
1814 acquiredAt: now,
1815 // settledRound stays 0: the genesis gnome was never rolled for. See the field's comment.
1816 })
1817 addOwnerToken(me, genesisTokenID)
1818
1819 // v6 FIX 1: register the genesis name in the uniqueness index, so NameOwner resolves it and
1820 // no ordinary gnome can take it. reservedNameKeys already refuses it independently of this
1821 // write (same two-locks discipline as the genesis transfer refusals above) -- this one makes
1822 // the index COMPLETE, so "every name a living token carries is in nameOwner" holds without an
1823 // exception a future reader would have to know about.
1824 nameOwner.Set(canonicalNameKey(genesisGnomeName), genesisTokenID)
1825
1826 // Registered under its own name, "gaffer", in the SAME tree the eight named one-of-ones
1827 // use -- not because it competes with their cap (BatchStatus's namedMinted count iterates
1828 // namedEditionOrder only, which "gaffer" is never a member of, so this cannot affect the
1829 // batch), but so CharacterOwner("gaffer") resolves instead of silently reporting not-found
1830 // for the one named character that is neither ordinary nor one of the eight. Second-audit-
1831 // pass finding (G7): fixed here, not left as a reader inconsistency.
1832 characterOwner.Set("gaffer", genesisTokenID)
1833
1834 // The EVENT type stays "Genesis" after the gnome was named (Sprig in v7, Gaffer in v8), and
1835 // decision rather than a miss: this names the EVENT -- the genesis of the collection, which
1836 // happens exactly once at deploy -- and not the gnome. The gnome's name travels in the payload,
1837 // so a reader gets both and neither word is doing the other's job.
1838 chain.Emit("Genesis", "tokenId", genesisTokenID, "name", genesisGnomeName)
1839}
1840
1841// assertFeeConfig: unchanged from v4's own gate.
1842func assertFeeConfig(cfg feeConfig, realmAddr address) {
1843 if !cfg.recipient.IsValid() {
1844 panic("garden: DEPLOY GATE: saleFeeRecipient is not a valid address; fix the constant at the top of garden.gno before deploying")
1845 }
1846 if cfg.recipient == realmAddr {
1847 panic("garden: DEPLOY GATE: saleFeeRecipient is this realm's own address, and coins sent there are burned because there is no withdrawal path")
1848 }
1849 if cfg.recipient == address(adminAddress) {
1850 panic("garden: DEPLOY GATE: saleFeeRecipient is the admin/deploy key; the fee recipient must be an address chosen for that purpose, not the key that happens to deploy")
1851 }
1852 if cfg.rateBps < 1 || cfg.rateBps > maxFeeRateBps {
1853 panic("garden: DEPLOY GATE: saleFeeRateBps must be between 1 and " + strconv.FormatInt(maxFeeRateBps, 10) + " basis points; fix the constant at the top of garden.gno before deploying")
1854 }
1855 if cfg.floorUgnot < 1 || cfg.floorUgnot > maxPriceUgnot {
1856 panic("garden: DEPLOY GATE: saleFeeFloorUgnot must be between 1 and " + strconv.FormatInt(maxPriceUgnot, 10) + " ugnot; fix the constant at the top of garden.gno before deploying")
1857 }
1858}
1859
1860// saleGateExtension: v4's shape, plus the genesis refusal. Panics on every transfer except the
1861// one Buy is performing right now, and ALWAYS panics for genesisTokenID regardless of
1862// transferGate -- there is no path, including a future bug in Buy, by which the genesis gnome
1863// should ever be transferable, so this checks it first and unconditionally.
1864type saleGateExtension struct{}
1865
1866func (saleGateExtension) ExtensionKind() string { return "sale-gate" }
1867
1868func (saleGateExtension) OnMint(to address, tid grc721.TokenID) {}
1869
1870func (saleGateExtension) OnTransfer(from, to address, tid grc721.TokenID) {
1871 if tid.String() == genesisTokenID {
1872 panic("garden: the genesis gnome belongs to nobody and can never change hands")
1873 }
1874 if transferGate == "" || transferGate != tid.String() {
1875 panic("garden: a gnome can only change hands through Buy; every other transfer is permanently disabled")
1876 }
1877}
1878
1879func (saleGateExtension) OnBurn(tid grc721.TokenID) {
1880 panic("garden: gnomes cannot be burned")
1881}
1882
1883// ============================================================================
1884// Admin: the allowlist. The only admin-reachable state in this whole realm -- there is no
1885// SetRule, no FreezeRules, no fee steward, no treasury reach, nothing else at all.
1886// ============================================================================
1887
1888// AdminAllow adds addr to the allowlist, so it may call Claim. Adding an address a second time
1889// is a harmless no-op. Adding an address that has already rolled changes nothing -- hasRolled is
1890// checked independently and is permanent regardless of allowlist state.
1891func AdminAllow(cur realm, addr address) {
1892 if !unsafe.OriginSend().IsZero() {
1893 panic("garden: admin calls accept no coins")
1894 }
1895 caller := cur.Previous().Address()
1896 if caller != address(adminAddress) {
1897 panic("garden: only the admin may manage the allowlist")
1898 }
1899 if !addr.IsValid() {
1900 panic("garden: not a valid address")
1901 }
1902 allowlist.Set(addr.String(), true)
1903 chain.Emit("AdminAllow", "admin", caller.String(), "address", addr.String())
1904}
1905
1906// AdminDisallow removes addr from the allowlist. It does NOT, and cannot, undo a roll already
1907// spent: hasRolled is a separate, permanent record, and this function never touches it. Removing
1908// an address that was never allowed, or that has already claimed, is a harmless no-op.
1909func AdminDisallow(cur realm, addr address) {
1910 if !unsafe.OriginSend().IsZero() {
1911 panic("garden: admin calls accept no coins")
1912 }
1913 caller := cur.Previous().Address()
1914 if caller != address(adminAddress) {
1915 panic("garden: only the admin may manage the allowlist")
1916 }
1917 allowlist.Remove(addr.String())
1918 chain.Emit("AdminDisallow", "admin", caller.String(), "address", addr.String())
1919}
1920
1921// ============================================================================
1922// Claim: the one roll. Phase one of two. MINT.md section 10.5 step 1.
1923// ============================================================================
1924
1925// Claim spends the caller's one-and-only roll on this realm and pins a drand round that does
1926// not exist yet. It mints NOTHING -- Settle does that, once the pinned round is actually
1927// published -- because the whole point of this split is that the result must not be computable,
1928// by anyone, including this transaction's own caller, at the moment the roll becomes irrevocable
1929// (MINT.md section 10.2: gno.land has no on-chain entropy at all, so anything decidable inside
1930// one transaction is decidable by the caller before broadcasting it).
1931//
1932// WALLET-ONLY, the same reasoning as v4's Adopt: a gnome minted to a realm's address could never
1933// be recovered, so a batching, sponsor or multicall realm must never be able to spend a roll on
1934// anyone's behalf. The `gnokey maketx run` shape is refused by the identical gate, for the
1935// identical reason (IsUserCall is false for both).
1936func Claim(cur realm) {
1937 if !cur.Previous().IsUserCall() {
1938 panic("garden: claim must be called directly by a wallet, not through another realm; the one roll this spends could never be returned to an address that could not hold the resulting gnome")
1939 }
1940 if !unsafe.OriginSend().IsZero() {
1941 panic("garden: claim accepts no coins; the mint is free, exactly as garden/v4's adopt already refuses coins, and nothing here can return them")
1942 }
1943
1944 caller := cur.Previous().Address()
1945 if !caller.IsValid() {
1946 panic("garden: claim requires a valid caller address")
1947 }
1948
1949 if !allowlist.Has(caller.String()) {
1950 panic("garden: this address is not on the allowlist; ask the owner to add it before claiming")
1951 }
1952
1953 if hasRolled.Has(caller.String()) {
1954 panic("garden: this address has already rolled on this realm; one roll per wallet, forever, whether or not the resulting gnome is still held")
1955 }
1956
1957 if mintedCount >= batchCap {
1958 panic("garden: batch one is closed; all " + strconv.FormatInt(batchCap, 10) + " editions have already been claimed")
1959 }
1960
1961 // CurrentRound() is pure arithmetic over THIS call's own block time (RoundAt(time.Now())),
1962 // not an extrapolation from however recently somebody last relayed -- see MINT.md section
1963 // 10.4's own live measurement of EstimatedLatestRound() erroring under a stale relay while
1964 // CurrentRound() kept working regardless. Pinning pinOffsetRounds past it is therefore safe
1965 // against relay staleness; the margin exists only for ordinary block-time/wall-clock skew.
1966 round := beacon.CurrentRound() + pinOffsetRounds
1967
1968 // ---- commit. Nothing below this line can fail. ----
1969 hasRolled.Set(caller.String(), true)
1970 mintedCount++
1971 pendingClaims.Set(caller.String(), &pendingClaim{round: round, claimedAt: time.Now().Unix()})
1972
1973 chain.Emit("Claim", "claimant", caller.String(), "round", strconv.FormatInt(round, 10))
1974}
1975
1976// ============================================================================
1977// Settle: permissionless. Phase two of two. MINT.md section 10.5 step 2. Two entry points, one
1978// shared core -- see the package doc's "SETTLE CAN NOW PAY FOR ITS OWN BEACON RELAY" section for
1979// the full account of why, what it costs, and the two live-chain facts (beacon.Publish panics
1980// rather than no-ops on an already-published round; beacon.Publish is reachable cross-realm as
1981// beacon.Publish(cross(cur), round, sigHex), already proven by this package's own test harness)
1982// that shape of both functions below depends on.
1983// ============================================================================
1984
1985// pendingClaimFor looks up claimant's pending roll, shared by both Settle and
1986// SettleWithSignature so the "no pending claim" message and lookup logic exist exactly once.
1987func pendingClaimFor(claimant address) *pendingClaim {
1988 pcv := pendingClaims.Get(claimant.String())
1989 pc, ok := pcv.(*pendingClaim)
1990 if !ok || pc == nil {
1991 panic("garden: no pending claim for this address; it must Claim first, or this claim has already been settled")
1992 }
1993 return pc
1994}
1995
1996// Settle computes and permanently mints the result of claimant's pending roll, once the round
1997// Claim pinned has actually been published to the beacon by SOMEBODY -- another claimant's own
1998// SettleWithSignature, a relay, anyone. It does not publish anything itself; call
1999// SettleWithSignature instead if the round is not published yet and the caller has its
2000// signature to offer. ANYONE may call this -- the claimant themself, the project's own relayer, a
2001// stranger -- because by the time it can succeed at all, nothing is left to decide: the drand
2002// signature for the pinned round either exists publicly or it does not, and the parts this mints
2003// are a pure function of (that signature, the claimant's own address), fixed the instant the
2004// round is published, not the instant Settle happens to be called. Simulating this call before
2005// calling it for real previews a fact, not a choice -- exactly the property MINT.md section 10.5
2006// names as the whole fix: "reading the result afterward is a free query, same as any other realm
2007// read."
2008//
2009// Refuses coins for the same reason every other non-paying function here does: there is no
2010// banker to return them, and this is a free, permissionless computation, never a payment.
2011func Settle(cur realm, claimant address) {
2012 if !unsafe.OriginSend().IsZero() {
2013 panic("garden: settle accepts no coins; it is a free, permissionless computation, not a payment")
2014 }
2015 if !claimant.IsValid() {
2016 panic("garden: settle requires a valid claimant address")
2017 }
2018
2019 pc := pendingClaimFor(claimant)
2020
2021 if !beacon.Published(pc.round) {
2022 panic("garden: drand round " + strconv.FormatInt(pc.round, 10) + " has not been relayed to the beacon yet; call Settle again once it has, or call SettleWithSignature with the round's drand signature (fetched from drand's own public API) to relay it yourself in this same transaction -- relaying is permissionless, and so is settling")
2023 }
2024
2025 settleCore(claimant, pc)
2026}
2027
2028// SettleWithSignature is Settle, plus: if claimant's pinned round has not been published yet,
2029// this function publishes it ITSELF first, in this same transaction, using sigHex -- the round's
2030// drand signature, which the caller's own app fetches read-only from drand's public API
2031// (https://api.drand.sh/<chain>/public/<round> or any mirror) and passes straight through
2032// unmodified. This is the function a player's app should call once it has fetched a signature;
2033// Settle (above) remains for a caller with no signature to offer, or that already knows (or does
2034// not care) that somebody else has published the round.
2035//
2036// SAFE EITHER WAY THE ROUND TURNS OUT TO BE, which is the one property this function exists to
2037// guarantee and the live chain fact its shape is built around (see the package doc): beacon/v2's
2038// own Publish PANICS, not no-ops, when the round is already published, so this function checks
2039// beacon.Published first and only calls Publish if that check says no. If the round was published
2040// by someone else (another claimant's own SettleWithSignature, a relay, anyone) between the
2041// moment the caller fetched sigHex and the moment this transaction lands, sigHex is simply never
2042// used -- this function still succeeds, exactly as Settle would have. A bad sigHex, by contrast,
2043// is a real failure: beacon.Publish rejects it with its own "beacon: invalid signature" panic,
2044// which aborts this whole call (and the claim stays pending, to be retried with a correct
2045// signature or left for Settle once someone else relays it).
2046//
2047// Refuses coins for the same reason Settle does and every non-paying function here does: there
2048// is no banker to return them, and this is a free, permissionless computation -- the only payment
2049// anywhere in this call is the ordinary GNOT gas the caller's own transaction spends, to the
2050// network, never to this project (MINT.md section 14).
2051func SettleWithSignature(cur realm, claimant address, sigHex string) {
2052 if !unsafe.OriginSend().IsZero() {
2053 panic("garden: settle accepts no coins; it is a free, permissionless computation, not a payment")
2054 }
2055 if !claimant.IsValid() {
2056 panic("garden: settle requires a valid claimant address")
2057 }
2058
2059 pc := pendingClaimFor(claimant)
2060
2061 if !beacon.Published(pc.round) {
2062 // Permissionless and carries no caller-identity check (verified against the live
2063 // deployed beacon/v2 source -- see the package doc): this realm relaying on a player's
2064 // behalf is exactly as valid as the player relaying directly, or anyone else doing so.
2065 beacon.Publish(cross(cur), pc.round, sigHex)
2066 }
2067
2068 settleCore(claimant, pc)
2069}
2070
2071// settleCore is Settle's and SettleWithSignature's shared tail: the draw, the mint, and every
2072// write that follows, once the caller above has already confirmed the pinned round is published.
2073// Takes no `cur realm` because it makes no cross-realm call of its own -- beacon.Randomness and
2074// beacon.Published below are plain reads, not crossing functions, exactly as they always were
2075// before this change.
2076func settleCore(claimant address, pc *pendingClaim) {
2077 randomnessHex := beacon.Randomness(pc.round)
2078
2079 if mintedCount < settledCount+1 {
2080 // Unreachable: Claim never lets mintedCount fall behind settledCount+1 for a claim that
2081 // is about to settle, because mintedCount is incremented before any pendingClaim this
2082 // function could ever read exists. Named so this function is correct on its own terms,
2083 // the same discipline v4's Buy uses for its own unreachable checks.
2084 panic("garden: batch accounting is inconsistent; refusing to mint")
2085 }
2086
2087 // v7: the second unreachable accounting check, same discipline, for the one piece of state
2088 // the first-seventeen secret rule depends on. popcount(secretsDrawnMask) must equal
2089 // min(settledCount, firstDistinctCount) at the top of every settle: the mask gains exactly
2090 // one bit per settle until the window closes and is never cleared by anything. If it ever
2091 // does not, something has written the mask outside the one place below that may, and the
2092 // acrostic's distinctness guarantee is no longer true of this realm.
2093 expectedMaskBits := settledCount
2094 if expectedMaskBits > firstDistinctCount {
2095 expectedMaskBits = firstDistinctCount
2096 }
2097 if int64(popcount32(secretsDrawnMask)) != expectedMaskBits {
2098 panic("garden: the first-seventeen secret mask disagrees with the settle count; refusing to mint")
2099 }
2100
2101 // batchIndex: SETTLE ORDER, NOT CLAIM ORDER, and getting this backwards would break both the
2102 // kinship join and the distinctness guarantee. mintedCount counts Claims and gates batchCap;
2103 // settledCount counts Settles. Because Settle is permissionless and claims may settle in any
2104 // order at any later time, mintedCount can be 40 while settledCount is 6 -- so keying the
2105 // first-seventeen rule off claim order would key it off a number already spent before the
2106 // relevant gnome exists.
2107 batchIndex := settledCount + 1
2108
2109 // THE NAMED-EDITION PICK. seed % namedPickRange, against the COMPILE-TIME CONSTANT
2110 // namedPickRange, never against a shrinking count of what remains -- see the package doc's
2111 // "flaw found and fixed" section for why that distinction is the whole safety property here.
2112 // namedPick is fixed the instant randomnessHex exists (i.e. the instant the pinned round is
2113 // signed by drand, which nobody controls), and WHEN Settle happens to be called after that
2114 // changes nothing about it.
2115 //
2116 // v7 CHANGE 9, 2026-10-18: this modulus was uint64(batchCap), making each edition a 1-in-60
2117 // shot and leaving a 74.4% chance at least one of the three never existed. It is now its own
2118 // constant at 10, decoupled from the batch size and ten times the rate. The arithmetic, why it
2119 // is not batchCap, the safety argument and the cost are all at namedPickRange's declaration.
2120 //
2121 // v7 CHANGE 10, the same day, DID NOT TOUCH THIS LINE: it raised numNamedEditions from 3 to 8,
2122 // which widens the branch below without changing the draw. This expression is identical to
2123 // v6's apart from change 9's one constant.
2124 namedPick := seedFor(randomnessHex, claimant, "slot") % uint64(namedPickRange)
2125
2126 // The ordinary draw is always computed, unconditionally, whether or not namedPick lands in
2127 // the named range -- so a claimant whose pick collides with an edition someone else already
2128 // settled first has a ready, equally valid fallback rather than needing a second derivation.
2129 eyesSeed := seedFor(randomnessHex, claimant, "eyes")
2130 noseSeed := seedFor(randomnessHex, claimant, "nose")
2131 hatCoatSeed := seedFor(randomnessHex, claimant, "hatcoat")
2132 drawnEyes := int64(drawWeighted(eyesWeights[:], 100, eyesSeed))
2133 drawnNose := int64(drawWeighted(noseWeights[:], 100, noseSeed))
2134 drawnHat := hatOptions[hatCoatSeed%uint64(len(hatOptions))]
2135 drawnCoat := coatOptions[(hatCoatSeed/uint64(len(hatOptions)))%uint64(len(coatOptions))]
2136
2137 // v7 PHASE 1 AND PHASE 2: the fourteen new values, drawn by one pure function and checked
2138 // before anything is committed. r is carried to the commit block unmodified -- nothing below
2139 // may change a trait, and nothing below has access to a draw.
2140 r := drawRoll(randomnessHex, claimant, batchIndex, secretsDrawnMask)
2141 assertRoll(r, secretsDrawnMask)
2142
2143 character := ""
2144 eyes, nose := drawnEyes, drawnNose
2145 hat, coat := drawnHat, drawnCoat
2146 if namedPick < uint64(numNamedEditions) {
2147 candidate := namedEditionOrder[namedPick]
2148 if !characterOwner.Has(candidate) {
2149 // Whichever Settle call lands on chain first among possibly several colliding
2150 // claimants gets this edition; a later one finds characterOwner already holds it and
2151 // falls through to its own ordinary draw instead. This is a transaction-ordering
2152 // race, not a strategy any claimant can win by choosing WHEN to settle their OWN
2153 // claim -- their own pick is already fixed and cannot change -- see the package doc's
2154 // "a real flaw found and fixed" section for why claimedAt-based tie-breaking was
2155 // considered and rejected, and for the precise property this guarantees instead.
2156 character = candidate
2157 eyes, nose = -1, -1
2158 hat, coat = "", ""
2159 }
2160 }
2161
2162 // ------------------------------------------------------------------------------------
2163 // THERE IS NO NAMED-EDITION GUARANTEE, AND THAT IS A DECISION, NOT AN OVERSIGHT.
2164 // Guarantee written 2026-10-17, audited, REMOVED 2026-10-18 by the owner. The named-edition
2165 // path above is v6's, byte-for-byte. AUDIT-01.md RED-1 is the finding this answers.
2166 // ------------------------------------------------------------------------------------
2167 //
2168 // THE ARITHMETIC, KEPT BECAUSE IT IS THE THING BEING ACCEPTED, AND RECOMPUTED FOR v7 CHANGE
2169 // 10's EIGHT EDITIONS RATHER THAN EDITED. The pick above is `seed % namedPickRange`, named if
2170 // the result is under numNamedEditions = 8, and each of the sixty claimants draws
2171 // independently. So for a given edition, P(never drawn by 60 claimants) =
2172 // (9/10)^60 = 0.0017971, and
2173 // P(at least one of the eight never minted) = 1 - (1-0.0017971)^8 = 1.43%.
2174 // A HALF-FILLED BATCH OF 30 IS 1 - (1-(9/10)^30)^8 = 29.3%, which is the number to quote at
2175 // anyone sizing the allowlist. "HOB WAS NEVER BORN" IS STILL A POSSIBLE PERMANENT FACT ABOUT
2176 // THIS REALM, and so now is "TALLY WAS NEVER BORN" -- there is no guarantee here, only a rate.
2177 // Per edition it is a one-in-five-hundred-and-fifty fact; across eight editions the chance that
2178 // SOME ONE OF THEM is missing is 1.43%, UP from 0.54% at three editions, because adding
2179 // editions adds chances to miss one. That increase was accepted knowingly with change 10.
2180 //
2181 // THOSE WERE 1-in-60 AND 74.4% UNTIL 2026-10-18, when the modulus became namedPickRange
2182 // instead of batchCap (v7 change 9). DESIGN.md section 9.1 is where the 74.4% was first
2183 // computed and it describes the odds as they were; v5, which is live, and v6, which is not,
2184 // both still carry 1-in-60. The decision below -- REMOVAL OF THE GUARANTEE -- was taken
2185 // against the 74.4% and is unaffected by the new rate: raising the odds is not a guarantee and
2186 // does not reintroduce one. The owner's position, recorded while that figure stood, was that a
2187 // one-of-one which might never be minted is arguably the better one-of-one; what he asked for
2188 // afterwards was "the rates have to come out during a mint", which is a probability, not a
2189 // promise. Full arithmetic, including why the modulus is its own constant, at namedPickRange.
2190 //
2191 // KNOWN AND ACCEPTED, THEN: not missed. A guarantee WAS written here, in the owner's
2192 // approved form -- "when the number of unsettled slots remaining equals the number of named
2193 // editions still unclaimed, the remaining settles take them" -- as a forced branch at this
2194 // point plus a helper (namedEditionToForce) comparing slotsLeft = batchCap - settledCount
2195 // against the editions characterOwner did not yet hold. It was audited and it was removed.
2196 // NOTHING SHIPPED IN ITS PLACE. The 74.4% is the status quo v5 and v6 already had.
2197 //
2198 // WHY IT WAS REMOVED, which is the part a future reader needs because it inverts the
2199 // argument the removed code made for itself. That code's defence was that it did NOT draw
2200 // against a shrinking modulus -- it assigned DETERMINISTICALLY -- on the reasoning that a
2201 // draw against a shrinking, observable modulus is the gameable shape v6 exists to document.
2202 // THE REASONING RUNS THE OTHER WAY. DETERMINISTIC ASSIGNMENT KEYED ON A SHRINKING PUBLIC
2203 // OBSERVABLE IS STRICTLY WORSE THAN A DRAW AGAINST ONE: with a draw you are still betting,
2204 // and with assignment THE OUTCOME IS CERTAIN THE MOMENT THE PREDICATE HOLDS. The predicate
2205 // was computable off chain by anybody at any moment from two public readers -- BatchStatus
2206 // returns settledCount, CharacterOwner says which editions are gone -- so a claimant did not
2207 // have to guess; he could watch the branch come to him.
2208 //
2209 // AND THE DEFENCE ADDRESSED THE WRONG WITHHOLDING. It argued that Settle is permissionless
2210 // and never expires, so nobody can reserve a late slot: anyone can settle a holdout's claim
2211 // for the price of gas. That is true of a withheld SETTLE and IRRELEVANT TO A WITHHELD
2212 // CLAIM. Settle requires a pendingClaim to exist (see pendingClaimFor); A CLAIM THAT WAS
2213 // NEVER MADE CANNOT BE SETTLED BY ANYBODY. And Claim gates only on hasRolled and
2214 // mintedCount >= batchCap -- it never reads settledCount. So a wallet that simply did not
2215 // claim until 57 slots were settled took a named edition WITH CERTAINTY instead of the
2216 // honest draw (~3/60 = 5% at the 1-in-60 odds that stood when this was found; ~27% of picks
2217 // pointed at an edition after change 9, and ~8/60 = 13% of claimants can actually end up
2218 // holding one after change 10 -- every one of those numbers changes the GAIN and none of them
2219 // changes the CERTAINTY, so the finding is unaffected, and it gets WORSE as the roster grows
2220 // because a wallet holding back for a forced slot would now have had eight to be handed),
2221 // and eight such wallets took the lot. The one-mint-per-wallet cap did
2222 // not contain it, because each of those wallets needs exactly one mint.
2223 //
2224 // IT ALSO MISFIRED WITH NO ATTACKER AT ALL. BATCH-ONE.md section 2's hand-read rolling
2225 // allowlist leaves claim slots free late BY CONSTRUCTION, so the rarest assets in the
2226 // realm would have gone to whoever the admin happened to allowlist last -- an admin-timed
2227 // allocation of the scarcest things here, unremovable after deploy. The rule existed to
2228 // avoid TRAITS.md section 1.7's "the devs gave their friends the good ones"; rewarding the
2229 // latest instead of the quickest hands the owner that lever directly.
2230 //
2231 // WHAT ANY FUTURE ATTEMPT MUST SOLVE BEFORE IT IS WORTH WRITING, so that re-adding this is
2232 // not cheap. It is not enough to pick a cleverer trigger: ANY condition computed from a
2233 // public monotone counter is watchable, and both settledCount and mintedCount are public
2234 // and must stay so. The hard part is THE WITHHELD CLAIM, and NOTHING IN THIS REALM CAN
2235 // COUNTER IT, because AN UNMADE CLAIM IS UNSETTLEABLE -- there is no pendingClaim for a
2236 // third party to settle, no state to sweep, and nobody who can end the holdout's wait for
2237 // him. The move that suggests itself, a claim expiry or settle deadline, IS FORBIDDEN here
2238 // for an unrelated reason: the first-seventeen secret rule's self-healing property depends
2239 // on abandoned claims staying settleable forever (recorded again at that rule, and in
2240 // AUDIT-01 YELLOW-4, which notes the two guarantees were coupled and that the file had not
2241 // noticed). Three alternatives to removal were considered -- a different trigger, moving the
2242 // allocation off chain to a holding address and allocating by a published process, and
2243 // leaving the rule as written -- and removal was chosen. Solve the withheld claim first or
2244 // leave this alone; it is a design problem, not a patch.
2245 settledCount++
2246
2247 id := nextID.Next()
2248 tid := grc721.TokenID(id.String())
2249 if err := ledger.Mint(claimant, tid); err != nil {
2250 panic(err)
2251 }
2252
2253 // v6 FIX 1: a named one-of-one is born with its own name; an ordinary gnome is born NAMELESS
2254 // and stays that way until its owner calls SetName. Nothing is chosen at mint time, including
2255 // a name -- `character` here is a pure function of the drand round, not of anything the
2256 // claimant asked for, so assigning the name that goes with it is not a choice either.
2257 //
2258 // The name written here is the DISPLAY form -- "Hob", not the canonical key "hob" that
2259 // `character` is and that characterOwner is keyed by. See namedEditionDisplayNames for why
2260 // those are two strings and why collapsing them into one would break CharacterOwner.
2261 name := ""
2262 if character != "" {
2263 name = namedEditionDisplayName(character)
2264 }
2265
2266 now := time.Now().Unix()
2267 gnomes.Set(tid.String(), &gnome{
2268 hat: hat,
2269 coat: coat,
2270 eyes: eyes,
2271 nose: nose,
2272 character: character,
2273 name: name,
2274 // v6 FIX 2: the drand round that produced this gnome, stored rather than only emitted.
2275 // pc.round is the round Claim pinned and the one beacon.Randomness was just read at, so
2276 // this is the exact number a holder needs to re-derive their own parts from drand's own
2277 // public API and check this realm did not rig them.
2278 settledRound: pc.round,
2279
2280 // v7 PHASE 3: fourteen field writes taken VERBATIM from the roll. Nothing here computes,
2281 // adjusts, clamps or re-derives anything -- if a value needed fixing up at this point,
2282 // the fix would belong in drawRoll where it could be tested without minting, and
2283 // assertRoll above has already refused anything out of range.
2284 temperament: r.temperament,
2285 obsession: r.obsession,
2286 bond: r.bond,
2287 fear: r.fear,
2288 boast: r.boast,
2289 dominance: r.dominance,
2290 secret: r.secret,
2291 pluck: r.pluck,
2292 nous: r.nous,
2293 grumble: r.grumble,
2294 appetite: r.appetite,
2295 careThreshold: r.careThresholdDraw,
2296 bornUnder: r.bornUnder,
2297 batchIndex: r.batchIndex,
2298
2299 level: 1,
2300 adoptedAt: now,
2301 belly: meterMax,
2302 bellyAt: now,
2303 cheer: meterMax,
2304 cheerAt: now,
2305 lastActionAt: now,
2306 lastWokenAt: now,
2307 acquiredAt: now,
2308 })
2309
2310 // v7: the kinship join, written beside the gnome's own batchIndex so the two can never
2311 // disagree, and the first-seventeen mask. The mask is written ONLY here, ONLY inside the
2312 // window, and never cleared -- the two facts the monotonicity property in secretFor's
2313 // comment and the popcount check at the top of this function both rest on.
2314 batchIndexToToken.Set(strconv.FormatInt(r.batchIndex, 10), tid.String())
2315 if r.batchIndex <= firstDistinctCount {
2316 secretsDrawnMask |= 1 << uint(r.secret)
2317 }
2318
2319 addOwnerToken(claimant, tid.String())
2320 if character != "" {
2321 characterOwner.Set(character, tid.String())
2322 // v6 FIX 1: a named edition's name is taken the instant it mints. reservedNameKeys
2323 // already refused it to everyone else before this moment (that is the pre-mint half of
2324 // the protection -- see its own comment); this write is the post-mint half, so the
2325 // nameOwner index is complete for every living token without exception.
2326 nameOwner.Set(canonicalNameKey(name), tid.String())
2327 } else {
2328 eyesCounts.Set(strconv.FormatInt(eyes, 10), eyesCount(eyes)+1)
2329 noseCounts.Set(strconv.FormatInt(nose, 10), noseCount(nose)+1)
2330 hatCounts.Set(hat, hatCount(hat)+1)
2331 coatCounts.Set(coat, coatCount(coat)+1)
2332 }
2333
2334 grantStartingPurseIfNew(claimant)
2335
2336 // Auto-wake only if this claimant currently has nothing awake at all (never settled or
2337 // bought a gnome before, or sold their only one). Never otherwise: a claimant who already
2338 // has an awake gnome gets this one asleep in their stash, exactly as a purchase would (Buy's
2339 // own comment explains why switching must always be a deliberate Wake, never a side effect).
2340 if !activeToken.Has(claimant.String()) {
2341 activeToken.Set(claimant.String(), tid.String())
2342 }
2343
2344 pendingClaims.Remove(claimant.String())
2345
2346 chain.Emit(
2347 "Settle",
2348 "claimant", claimant.String(),
2349 "tokenId", tid.String(),
2350 "round", strconv.FormatInt(pc.round, 10),
2351 "character", character,
2352 "eyes", strconv.FormatInt(eyes, 10),
2353 "nose", strconv.FormatInt(nose, 10),
2354 "hat", hat,
2355 "coat", coat,
2356 )
2357}
2358
2359// popcount32 counts the set bits in secretsDrawnMask, for settleCore's accounting check. Written
2360// out rather than imported (math/bits is not used anywhere else in this file) and bounded at 32
2361// iterations, which is free next to a SHA-256.
2362func popcount32(v uint32) int {
2363 n := 0
2364 for i := 0; i < 32; i++ {
2365 if v&(1<<uint(i)) != 0 {
2366 n++
2367 }
2368 }
2369 return n
2370}
2371
2372// ============================================================================
2373// THE SEED DERIVATION. v7 CHANGE 8 -- THE ONE CHANGE ON THIS VERSION'S LIST THAT IS NOT A
2374// FEATURE, AND THE FIRST THING TO READ IN REVIEW. DESIGN.md section 1.2 item 8, section 4.3.
2375// ============================================================================
2376//
2377// v5 and v6 derive every seed with FNV-1a. v7 does not, and the reason is a provable defect
2378// rather than a preference -- the argument in full, because this is a change to a line an
2379// AUDITED realm carries forward and section 0's rule exists to refuse exactly that:
2380//
2381// FNV-1a, 64-bit, is `h = offset`, then for each input byte `h ^= b; h *= 1099511628211`.
2382// THE MULTIPLIER IS ODD, so multiplying never touches bit 0, and XOR acts on bit 0
2383// independently. Therefore bit0(fnv64a(s)) = 1 XOR bit0(s[0]) XOR ... XOR bit0(s[n-1]) --
2384// bit 0 of the hash is a PARITY of bit 0 of every input byte, not a hash of it. The general
2385// form: fnv64a(s) mod 2^k depends ONLY on the low k bits of each input byte, because both
2386// ^ and * are closed on the low k bits. FNV-1a has ZERO diffusion into its low bits -- not
2387// weak diffusion, none. TestFnvBitZeroIsAParity in garden_test.gno proves it against a local
2388// copy of v6's own implementation, so the reason for this change lives in the suite and not
2389// only in this comment.
2390//
2391// WHY v5 AND v6 SURVIVED IT. They take seed % 100, % 100, % 60 and % 4 and nothing else. The
2392// three large moduli mix high bits in, so the defect reaches them only through their mod-4
2393// component; the fully-affected pair (hat and coat) is a UNIFORM PLACEHOLDER draw in those
2394// realms anyway (BATCH-ONE.md section 4a). It is also not exploitable in any of the three
2395// realms: the outcome also depends on the parity of a drand randomness that does not exist
2396// yet when Claim commits, so a claimant cannot know which address parity to shop for. It is
2397// a randomness-QUALITY defect, not a hole, and v5 being immutable with one ordinary gnome on
2398// it means there is nothing to do about it there.
2399//
2400// WHY v7 CANNOT SHIP IT. v7 takes seed % 2, % 3, % 5, % 16 and % 32, which v5 and v6 never
2401// did, and it takes them for the DOMINANCE PERMUTATION -- the field TRAITS.md section 1.7's
2402// whole duplicate-vector argument rests on (8,776 vectors to 1,053,132 WITH the order; the
2403// expected fully-identical pairs among sixty from 0.202 to 0.0017). Under FNV-1a the last
2404// Fisher-Yates swap would not be a coin flip at all: it would be an XOR of three parities,
2405// so two claimants who pinned the SAME round would have their final swap decided entirely by
2406// the parity of their own addresses -- the batch splitting into two halves by address parity,
2407// every member of a half getting the identical swap. At a 3-second drand period with a
2408// 20-round pin, claimants claiming within a minute of each other share a round, which on a
2409// curated launch day is most of them. That is structure in the batch's dominance orders: one
2410// of the two most load-bearing numbers in TRAITS.md, quietly wrong, forever, on an immutable
2411// realm. And "provably unrigged" (ECONOMY.md section 1) is this project's headline claim: a
2412// holder who re-derives his own gnome and finds that one bit of it is a parity of his own
2413// address has found something this project should have found first.
2414//
2415// WHAT THIS CHANGE DOES NOT BUY, said plainly because the temptation is to over-claim.
2416// SHA-256 IS NOT THE SOURCE OF UNPREDICTABILITY HERE EITHER -- drand's signature for a
2417// not-yet-existent round is, exactly as MINT.md section 10.5 designs, and v6's own comment on
2418// fnv64a was right about that and is carried forward in spirit. After the pinned round
2419// publishes, every seed is computable by anybody (that is the design: the commit/reveal split
2420// exists so the fact is fixed before anyone can see it); before it publishes nobody can
2421// compute any of them, and the reason is drand's threshold signature alone; and if drand's
2422// threshold is ever broken, v7 is broken, hash or no hash. v7 does not become more
2423// unpredictable. IT BECOMES CORRECTLY DECORRELATED, which is the only job seedFor was ever
2424// doing and the job v6's implementation does not do.
2425//
2426// crypto/sha256 is not a new assumption: arena/v1 uses sha256.Sum256 for its commit hashes and
2427// its own source records cross-checking the output against Node's crypto.createHash('sha256')
2428// on a fixed input; bls254 and beacon/v2 use it too (beacon.Randomness(round) IS
2429// sha256(signature)). fnv64a is DELETED rather than kept beside this -- two hashes in one realm
2430// is an invitation to call the wrong one.
2431
2432// seedDomain: a version tag, so a seed derived by this realm can never collide with one derived
2433// by any other realm or any later version from the same round and claimant. It is part of the
2434// hashed input and therefore frozen the moment v7 deploys.
2435// seedDomain DELIBERATELY STILL SAYS v7, and this is a decision rather than the oversight it
2436// looks like. tools/check-realm-version.js exempts it by exact text and says so.
2437//
2438// It is a cryptographic domain constant: every gnome's traits are sha256 of it concatenated with
2439// the round, the claimant and a purpose. Bumping it to "v9" therefore RE-ROLLS EVERY POSSIBLE
2440// GNOME -- a given (round, claimant) draws something different. v9 exists to rename ONE gnome;
2441// re-rolling the whole collection is a far larger change than the one that was asked for, and
2442// doing it as a side effect of tidying a string would be the wrong kind of quiet.
2443//
2444// Measured, not assumed: bumping it to v9 made TestNamedEditionCanOnlyEverBeMintedOnce fail,
2445// because an earlier test's draw began hitting "wick" where it previously had not. Realm state
2446// persists across test functions, so the suite's fixtures depend on the draw. That failure is
2447// the warning working -- see gnomod.toml item (14) -- not a test to be adjusted.
2448//
2449// Nothing leaks by sharing a domain with v5-v8. The randomness is public drand and the derivation
2450// is published, so anybody can already compute their own gnome off chain; that is the point of
2451// TestSeedForMatchesAnOffChainDerivation. Sharing the domain only means the same inputs give the
2452// same gnome on two realms, one of which is abandoned.
2453const seedDomain = "gnogotchi/garden/v7/seed/v1"
2454
2455// purposeStrings: EVERY purpose literal v7 passes to seedFor, declared once so that the
2456// properties the independence proof rests on are checkable by a test rather than believed.
2457// DESIGN.md section 4.2. Three things must hold and each has a test:
2458// TestPurposeStringsAreDistinct (a duplicate would silently weld two draws together -- bond and
2459// boast sharing a seed would make a gnome's bond a deterministic function of his boast across
2460// the whole batch, invisible in any single gnome), TestNoPurposeContainsTheSeparator (so the
2461// hashed input is uniquely decodable and two distinct (claimant, purpose) pairs can never build
2462// the same string), and TestEveryPurposeLiteralIsInTheDeclaredList (so a nineteenth draw added
2463// later cannot escape the other tests).
2464//
2465// FROZEN AT DEPLOY, ASYMMETRICALLY: adding a purpose later is impossible (the realm is
2466// immutable) so this list has to be complete now, and CHANGING one is worse than impossible --
2467// it would re-roll every gnome already minted.
2468var purposeStrings = [18]string{
2469 "slot", // the named-edition pick (v6's, unchanged)
2470 "eyes", // eyes, weighted over 100 (v6's, unchanged)
2471 "nose", // nose, weighted over 100 (v6's, unchanged)
2472 "hatcoat", // hat and coat, two 2-bit fields (v6's, unchanged)
2473 "temperament", // axis 0, weighted over 64
2474 "obsession", // axis 1, weighted over 64
2475 "bond", // axis 2, weighted over 64
2476 "fear", // axis 3, weighted over 64
2477 "boast", // axis 4, weighted over 64
2478 "bornunder", // the card-only sign, weighted over 64
2479 "dom0", // Fisher-Yates swap for slot 4, mod 5
2480 "dom1", // Fisher-Yates swap for slot 3, mod 4
2481 "dom2", // Fisher-Yates swap for slot 2, mod 3
2482 "dom3", // Fisher-Yates swap for slot 1, mod 2
2483 "stats", // index into the 546 legal stat lines
2484 "secret", // the weighted first choice over 17
2485 "secretstride", // the first-seventeen probe stride, mod 16
2486 "threshold", // the care-threshold draw, mod 32
2487}
2488
2489// seedFor derives one independent-looking 64-bit value from the published drand randomness, the
2490// claimant's own address, and a purpose string -- so a single drand round can safely drive
2491// several unrelated draws (which named edition, which eyes, which nose, which hat/coat, and in
2492// v7 fourteen more) for several different simultaneous claimants without any of them correlating
2493// with each other. SIGNATURE, SEPARATOR, ARGUMENT ORDER AND PURPOSE MECHANISM ARE v6's,
2494// UNCHANGED; only the hash moved (see the block above).
2495//
2496// The first eight bytes of the digest, big-endian, assembled by hand rather than through
2497// encoding/binary so the import list grows by exactly one entry and the byte order is visible in
2498// the source instead of in a package somebody has to go and read.
2499func seedFor(randomnessHex string, claimant address, purpose string) uint64 {
2500 sum := sha256.Sum256([]byte(
2501 seedDomain + "|" + randomnessHex + "|" + claimant.String() + "|" + purpose))
2502 return uint64(sum[0])<<56 | uint64(sum[1])<<48 | uint64(sum[2])<<40 |
2503 uint64(sum[3])<<32 | uint64(sum[4])<<24 | uint64(sum[5])<<16 |
2504 uint64(sum[6])<<8 | uint64(sum[7])
2505}
2506
2507// drawIndex is the one unweighted bounded draw in v7: int64(seed % uint64(n)), and that is all.
2508// It exists so that every unweighted bounded draw goes through ONE line of arithmetic that one
2509// person audits once, instead of nine inline % expressions scattered through drawRoll.
2510//
2511// ON MODULO BIAS, because this is the line an auditor should stop on. For a uniform 64-bit seed
2512// and modulus m, the maximum relative deviation of any outcome's probability from 1/m is
2513// strictly less than m / 2^64. v7's moduli: 2, 4, 16, 32 and 64 divide 2^64 and are therefore
2514// EXACTLY unbiased; 3 and 5 are under 2.8e-19; 31/38/45 (the secret's weighted total) under
2515// 2.5e-18; 60 (v6's named pick) under 3.3e-18; 100 (v6's part tables) under 5.4e-18; and 546
2516// (the stat line), the worst case in the realm, under 3.0e-17 -- about seventeen orders of
2517// magnitude below the probability that drand's own threshold signature scheme is broken. It is
2518// not zero and this file does not claim it is.
2519//
2520// THIS IS A DECLARED DISAGREEMENT WITH TRAITS.md section 2.2 and section 4.2, both of which
2521// instruct rejection sampling ("reject a byte >= 256 - 256 mod n", "draw an index 0-545 by
2522// rejection sampling from ten bits"). Those instructions are right about the draws they were
2523// written for and the difference is the WIDTH of the draw, not the use of %: from a single BYTE,
2524// % 5 really does skew one outcome in five by 0.4%, and from TEN BITS against 546 the skew is
2525// 1.87:1 -- rejection is mandatory in both cases. From a 64-bit draw neither hazard exists at
2526// any scale anyone can measure. And rejection is not free in the one function that most needs to
2527// stay simple: it needs a fresh seed per attempt (MORE seed-derivation surface, not less), an
2528// attempt cap (an unbounded loop in Settle is worse than a 1e-17 bias), and a documented
2529// deterministic fallback at the cap -- three new things, each with its own failure mode, to
2530// remove a deviation of one part in 3e16. If a reviewer wants rejection anyway, the change is
2531// confined to this function and nothing else in the design moves.
2532func drawIndex(seed uint64, n int64) int64 {
2533 if n <= 0 {
2534 panic("garden: drawIndex: n must be positive")
2535 }
2536 return int64(seed % uint64(n))
2537}
2538
2539// drawWeighted picks an index into weights (which TestWeightTablesSumToOneHundred pins as
2540// summing to totalWeight) using seed as the source of randomness -- a plain cumulative-sum
2541// bucket walk, MINT.md section 10.5's own recommendation for what Settle should do ("a simple,
2542// bounded index... easy to audit"), applied to a weight table instead of a published array,
2543// which is the same shape of arithmetic.
2544func drawWeighted(weights []int64, totalWeight int64, seed uint64) int {
2545 if totalWeight <= 0 {
2546 panic("garden: drawWeighted: totalWeight must be positive")
2547 }
2548 x := int64(seed % uint64(totalWeight))
2549 var cum int64
2550 for i, w := range weights {
2551 cum += w
2552 if x < cum {
2553 return i
2554 }
2555 }
2556 // Unreachable if weights sums to exactly totalWeight, which the test above pins. Returning
2557 // the last index rather than panicking keeps Settle from ever aborting a claimant's one and
2558 // only roll over an arithmetic slip elsewhere in this file.
2559 return len(weights) - 1
2560}
2561
2562// ============================================================================
2563// v7: THE DRAW. ONE PURE FUNCTION, THEN v6's COMMIT BLOCK.
2564// ============================================================================
2565//
2566// BATCH-ONE.md:120 names Settle as "the one function MINT.md section 9 names as needing its own
2567// audit pass above all others", and the row it appears in warns that drawing several weighted
2568// tables off one random value is "more arithmetic, more surface for an off-by-one or modulo-bias
2569// bug". THAT WARNING WAS WRITTEN AGAINST FOUR DRAWS. v7 has eighteen, plus a permutation, plus a
2570// constrained draw. So the shape below is not chosen for elegance; it is chosen so that eighteen
2571// draws are no harder to audit than four:
2572//
2573// PHASE 1 -- THE DRAW. drawRoll: one pure function. No tree read, no tree write, no mint,
2574// no emit, no time.Now(), no cur realm. Everything it needs is an
2575// argument.
2576// PHASE 2 -- THE CHECKS. assertRoll: every "unreachable" assertion about the result, in one
2577// block, BEFORE anything is committed.
2578// PHASE 3 -- THE COMMIT. v6's own commit block, plus fourteen field writes taken verbatim
2579// from the roll. Nothing below that line can fail.
2580//
2581// drawRoll BEING PURE IS THE SINGLE MOST VALUABLE PROPERTY IN THIS FILE and it buys four things:
2582//
2583// 1. IT IS TESTABLE WITHOUT MINTING ANYTHING. A table-driven test runs it over a large corpus
2584// of synthetic randomnessHex values and asserts distributions, with no allowlist, no claim,
2585// no beacon fixture and no token. v6 cannot do this: its draw is welded to ledger.Mint, so
2586// every distributional test costs a full claim/publish/settle cycle.
2587// 2. IT IS RE-DERIVABLE OFF CHAIN BY A HOLDER, which is what makes v6 fix 2 (SettledRound)
2588// worth anything. Fetch https://api.drand.sh/<chain>/public/<round>, sha256 the signature to
2589// get randomnessHex, run the same draws against your own address. If this function is pure,
2590// "re-derive it yourself" is a complete instruction. If it read realm state, it would not be.
2591// 3. AN AUDITOR READS ONE FUNCTION. "Can this realm rig a gnome" is answered by reading
2592// drawRoll and checking that settleCore writes the result through unmodified. Nothing in
2593// phase 3 can change a trait, because phase 3 has no access to a draw -- only to the roll.
2594// 4. THE TWO PLACES THAT GENUINELY NEED REALM STATE ARE VISIBLE AS ARGUMENTS, not buried as
2595// global reads: batchIndex (settledCount+1) and taken (secretsDrawnMask). THOSE TWO
2596// PARAMETERS ARE THE COMPLETE LIST OF ORDER-DEPENDENCE IN v7's DRAW, and being able to see
2597// the whole list in a function signature is the point.
2598
2599// numLegalStatLines: TRAITS.md section 4.2's own count, re-derived from the rule rather than
2600// quoted -- 670 four-tuples in 1-10 sum to 22, and 546 of those have a stat >= 8 and a stat <= 3.
2601const numLegalStatLines = 546
2602
2603// packStatLine / unpackStatLine: four stats, each 1-10, in one int64, one byte each. A packing
2604// here is safe in the way a packing of the GNOME's fields would not be (see the gnome struct's
2605// own comment): this word is a deploy-time table entry that no client ever sees and no stored
2606// gnome ever holds -- settleCore writes the four stats out as four explicit fields.
2607func packStatLine(a, b, c, d int64) int64 {
2608 return a<<24 | b<<16 | c<<8 | d
2609}
2610
2611func unpackStatLine(packed int64) (a, b, c, d int64) {
2612 return (packed >> 24) & 0xff, (packed >> 16) & 0xff, (packed >> 8) & 0xff, packed & 0xff
2613}
2614
2615// buildLegalStatLines enumerates TRAITS.md section 4.2's legal set ONCE, FROM THE RULE: "22
2616// points across four stats, each in 1-10, with at least one stat >= 8 and at least one <= 3".
2617// Four nested loops, four comparisons, 10,000 iterations, at deploy.
2618//
2619// TRAITS.md section 4.2's instruction is explicit and this follows it: "enumerate the legal set
2620// once (it is small and fixed), then draw uniformly from it. Do NOT roll four stats and repair
2621// them -- a repair loop is a modulo-bias bug and a fairness argument waiting to happen."
2622//
2623// THE ENUMERATION ORDER IS FROZEN THE MOMENT THIS REALM DEPLOYS, because drawRoll indexes into
2624// it. TestLegalStatLineOrderIsStable pins it with a checksum over the whole table.
2625func buildLegalStatLines() []int64 {
2626 out := make([]int64, 0, numLegalStatLines)
2627 for a := statMin; a <= statMax; a++ {
2628 for b := statMin; b <= statMax; b++ {
2629 for c := statMin; c <= statMax; c++ {
2630 for d := statMin; d <= statMax; d++ {
2631 if a+b+c+d != statPointTotal {
2632 continue
2633 }
2634 hi, lo := a, a
2635 // Three elements, not numStats: `a` is already seeded into hi and lo
2636 // above. An array sized numStats here would have a fourth, zero-valued
2637 // element, which would make `lo` zero for every row and silently drop the
2638 // "at least one stat <= 3" constraint -- 546 rows would become 586 and
2639 // nothing would fail except a count. Found by the init() deploy gate while
2640 // building this file, recorded rather than quietly fixed.
2641 for _, v := range [3]int64{b, c, d} {
2642 if v > hi {
2643 hi = v
2644 }
2645 if v < lo {
2646 lo = v
2647 }
2648 }
2649 // The two extra constraints are what kill 6/5/6/5 -- "a gnome with no
2650 // strength and no weakness, who is the one card nobody ever plays".
2651 if hi < statHighAtLeast || lo > statLowAtMost {
2652 continue
2653 }
2654 out = append(out, packStatLine(a, b, c, d))
2655 }
2656 }
2657 }
2658 }
2659 return out
2660}
2661
2662// roll is everything one claimant's draw produces: one field per stored value, and nothing else.
2663// It is a plain value with no pointers and no realm state, returned by drawRoll and consumed by
2664// assertRoll and settleCore's commit block.
2665type roll struct {
2666 temperament int64
2667 obsession int64
2668 bond int64
2669 fear int64
2670 boast int64
2671
2672 dominance [numAxes]int64
2673
2674 secret int64
2675
2676 pluck int64
2677 nous int64
2678 grumble int64
2679 appetite int64
2680
2681 careThresholdDraw int64
2682 bornUnder int64
2683 batchIndex int64
2684}
2685
2686// drawRoll is PHASE 1: the whole audit surface for v7's fairness, as a pure function of its four
2687// arguments. The draws are listed in the order they are computed, which is fixed -- NOTHING
2688// DEPENDS ON THE ORDER, because every draw has its own purpose string and therefore its own
2689// seed, so reordering them would change nothing -- but it is fixed anyway, because a fixed order
2690// is what makes "re-derive it yourself" a checkable instruction.
2691//
2692// batchIndex is settledCount+1 and taken is secretsDrawnMask; both are passed in, and they are
2693// the only order-dependent inputs in the whole draw. Everything else is (randomnessHex,
2694// claimant), which is fixed the instant the pinned round is signed by drand.
2695func drawRoll(randomnessHex string, claimant address, batchIndex int64, taken uint32) roll {
2696 var r roll
2697 r.batchIndex = batchIndex
2698
2699 // The five axes and born-under: six weighted draws over six 64-wide tables, each with its
2700 // own seed. 64 divides 2^64, so each of these carries exactly zero modulo bias.
2701 r.temperament = int64(drawWeighted(temperamentWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "temperament")))
2702 r.obsession = int64(drawWeighted(obsessionWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "obsession")))
2703 r.bond = int64(drawWeighted(bondWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "bond")))
2704 r.fear = int64(drawWeighted(fearWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "fear")))
2705 r.boast = int64(drawWeighted(boastWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "boast")))
2706 r.bornUnder = int64(drawWeighted(bornUnderWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "bornunder")))
2707
2708 // The dominance permutation: four draws, four distinct seeds, four DESCENDING moduli.
2709 r.dominance = dominanceFor(randomnessHex, claimant)
2710
2711 // The stat line: one uniform index into the 546 legal lines, unpacked into four explicit
2712 // values. The index is never stored (see the gnome struct).
2713 statIdx := drawIndex(seedFor(randomnessHex, claimant, "stats"), int64(len(legalStatLines)))
2714 r.pluck, r.nous, r.grumble, r.appetite = unpackStatLine(legalStatLines[statIdx])
2715
2716 // The secret: weighted toward the ones that undercut this gnome's OWN rolled boast or fear,
2717 // and distinct from every secret already drawn if this is one of the first seventeen settles.
2718 // THIS IS THE ONLY DRAW THAT READS ANY REALM STATE, and it reads it as the `taken` argument.
2719 r.secret = secretFor(randomnessHex, claimant, r.boast, r.fear, batchIndex, taken)
2720
2721 // The care threshold draw: 0-31, flat, 32 divides 2^64. Stored, read by nothing.
2722 r.careThresholdDraw = drawIndex(seedFor(randomnessHex, claimant, "threshold"), careThresholdSpan)
2723
2724 return r
2725}
2726
2727// dominanceFor draws the dominance order: Fisher-Yates over {0,1,2,3,4}, descending -- swap slot
2728// 4 with a slot drawn from 0-4, slot 3 with one from 0-3, slot 2 from 0-2, slot 1 from 0-1. Four
2729// draws, four distinct seeds, four distinct moduli.
2730//
2731// TRAITS.md section 2.2's two prohibitions are both honoured and both are worth restating as the
2732// things NOT to do:
2733//
2734// - NOT seed % 120. 120 is not a power of two, and more importantly a single index would need
2735// the Lehmer codec the gnome struct's own comment rejects.
2736// - NOT seed % 5 FIVE TIMES. That maps 5^5 = 3,125 outcomes onto 120 permutations
2737// non-uniformly and is the classic naive-shuffle bug. Four descending moduli is the correct
2738// Fisher-Yates, and TestDominanceMatchesAnIndependentFisherYates compares this against a
2739// reference implementation written separately, which is the only way to catch somebody
2740// reintroducing the naive version later.
2741//
2742// ALL 120 ORDERS ARE LEGAL, so there is no post-draw constraint, no rejection and no loop. That
2743// is the realm-side dividend of TRAITS.md section 2.1's two-state recommendation, and it is a
2744// stronger argument for two states than any of the four section 2.1 gives: under THREE states
2745// with FUNNY.md section 6.5's fix applied, only 96 of the 120 orders would be legal (every order
2746// except the twenty-four that put Temperament in the single dormant slot), and an illegal-order
2747// exclusion is a REJECTION LOOP INSIDE THE DRAW -- Fisher-Yates, then a check, then a redraw --
2748// with an unbounded worst case and a fallback nobody wants to specify, in the one function
2749// BATCH-ONE.md:120 is a warning about. Two states make all 120 legal, so the draw has no
2750// constraint, no check and no loop.
2751func dominanceFor(randomnessHex string, claimant address) [numAxes]int64 {
2752 order := [numAxes]int64{0, 1, 2, 3, 4}
2753 // i counts down over the slots still to be fixed; the modulus descends 5, 4, 3, 2.
2754 for i := numAxes - 1; i >= 1; i-- {
2755 j := drawIndex(seedFor(randomnessHex, claimant, "dom"+strconv.Itoa(numAxes-1-i)), int64(i+1))
2756 order[i], order[j] = order[j], order[i]
2757 }
2758 return order
2759}
2760
2761// secretWeightsFor builds one gnome's secret weight table from TRAITS.md section 3.3's rule:
2762// base weight 1 for all seventeen, MULTIPLIED BY 8 if the secret's target matches this gnome's
2763// rolled boast or rolled fear. One multiplier, one comparison, no second table.
2764//
2765// The total it returns is 31 + 7k with k in {0, 1, 2} -- 31, 38 or 45 and NO OTHER VALUE -- which
2766// is the check that the table is right, because every gnome's boast carries exactly two secrets
2767// and k is the number aimed at his rolled fear (2 for Beetles, 1 for Being Forgotten / Being
2768// Laughed At / Slugs, 0 for The Dark and Shine). TestSecretWeightTotalIsAlwaysThirtyOneOrThirtyEightOrFortyFive
2769// pins exactly that over every (boast, fear) pair, exhaustively.
2770//
2771// THIS IS THE ONE PLACE IN v7 WHERE TRAIT DATA IS COMPARED TO ANYTHING, and it is worth being
2772// precise about what the comparison is and is not: it reads a gnome's own just-drawn boast and
2773// fear, and it produces a weight table that produces a secret id. It gates nothing, it changes no
2774// fee, it grants no eligibility, and it never sees another gnome. The invariant stated once:
2775// NO FUNCTION IN v7 BRANCHES ON A TRAIT, A STAT, A LINEAGE EDGE OR A SECRET -- not in a guard,
2776// not in a loop condition, not in a fee calculation, not in an eligibility check -- and this
2777// weighting is the single exception the invariant is written around.
2778func secretWeightsFor(boast, fear int64) ([numSecrets]int64, int64) {
2779 var w [numSecrets]int64
2780 total := int64(0)
2781 for i := 0; i < numSecrets; i++ {
2782 weight := secretBaseWeight
2783 if secretTargetKind[i] == secretTargetKindBoast && secretTargetID[i] == boast {
2784 weight = secretBaseWeight * secretMatchMultiplier
2785 }
2786 if secretTargetKind[i] == secretTargetKindFear && secretTargetID[i] == fear {
2787 weight = secretBaseWeight * secretMatchMultiplier
2788 }
2789 w[i] = weight
2790 total += weight
2791 }
2792 return w, total
2793}
2794
2795// secretFor is the one genuinely new mechanism in v7 and the one to read twice.
2796//
2797// THE RULE BEING IMPLEMENTED (TRAITS.md section 3.2.2, agreed by the owner 2026-10-17): "The
2798// first seventeen mints draw seventeen distinct secrets. For tokens 1-17 the weighted draw of
2799// section 3.3 runs over the not-yet-drawn subset of the pool only. From token 18 onward the draw
2800// is the full seventeen with duplicates free."
2801//
2802// IT IS NOT IMPLEMENTED AS THE LITERAL SUBSET DRAW, and the reason is the flaw v6 exists to
2803// document. A renormalised weighted draw over the free secrets has a modulus of "the sum of the
2804// weights of the free secrets" -- A SHRINKING, OBSERVABLE, TIME-VARYING NUMBER, which is v6's
2805// package doc word for word: "THAT IS PEEKABLE AND GAMEABLE... a claimant whose seed is already
2806// fixed can simply precompute, for every possible remainingTotal, whether that value would win
2807// them [the thing they want] -- and then wait, costing nothing." Worse, it is not monotone:
2808// removing a secret reshuffles which index every seed maps to, so waiting CAN improve the
2809// outcome. It would also need DIFFERENT ARITHMETIC IN THE TWO PATHS -- a filtered cumulative
2810// walk for the first seventeen and an unfiltered one for the rest -- which is BATCH-ONE.md:120's
2811// warning exactly.
2812//
2813// Instead: one weighted walk, called IDENTICALLY by the first gnome and the sixtieth, plus a
2814// probe with a fall-through made of two modulos by compile-time constants. THE PROBE IS NOT A NEW
2815// INVENTION: it is v6's own audited named-edition pattern generalised -- draw against a constant
2816// modulus; if what you drew is taken, fall through -- with the fall-through made a full cycle
2817// instead of one step deep.
2818//
2819// SIX PROPERTIES, EACH WITH ITS PROOF, because this is where the audit will live.
2820//
2821// P1 -- IT ALWAYS TERMINATES, AND INSIDE THE WINDOW IT ALWAYS RETURNS A FREE SECRET. The loop
2822// is bounded at numSecrets iterations by construction. Inside the window at most
2823// batchIndex-1 <= 16 bits of `taken` are set, and by P2 the walk visits all seventeen ids, so
2824// at least one is free. The panic below is therefore unreachable; it is named rather than
2825// omitted, v6's own discipline.
2826//
2827// P2 -- THE WALK VISITS ALL SEVENTEEN. numSecrets = 17 is PRIME, so gcd(stride, 17) = 1 for
2828// every stride in [1, 16], so {first + i*stride mod 17 : i = 0..16} is all of Z/17.
2829// TestSecretProbeVisitsAllSeventeen proves it EXHAUSTIVELY over all 17 x 16 = 272 (first,
2830// stride) pairs, and TestSecretPoolSizeIsPrime pins the precondition.
2831//
2832// P3 -- THE STRIDE DRAW HAS EXACTLY ZERO MODULO BIAS. 16 divides 2^64.
2833//
2834// P4 -- THE ONLY REALM STATE CONSULTED IS `taken`, AND ONLY INSIDE THE WINDOW. drawRoll takes
2835// it as an argument. There is no other global read anywhere in the draw.
2836//
2837// P5 -- MONOTONICITY: WAITING CAN NEVER IMPROVE A CLAIMANT'S OUTCOME. `taken` only ever gains
2838// bits. A claimant's probe ORDER is fixed by his own seed the instant the pinned round
2839// publishes and cannot change. So if T1 is a subset of T2, the first free id in a given walk
2840// under T1 sits at an index less than or equal to its index under T2. THERE IS NO
2841// SETTLE-TIMING STRATEGY HERE, which is the exact property v6's package doc identifies as the
2842// one that matters ("nobody can improve THEIR OWN odds by choosing when to settle") -- and in
2843// the stronger direction, because the incentive actively points at settling promptly rather
2844// than merely being indifferent. TestSecretIsMonotoneUnderWaiting is the proof.
2845//
2846// P6 -- THE RESIDUAL, STATED PRECISELY AND NOT ASPIRATIONALLY. A third party can settle OTHER
2847// claims before a claimant settles his, pushing him further down his own fixed probe list.
2848// That costs the griefer gas and buys him nothing, and the ceiling on the harm is TRAITS.md
2849// section 3.2.2 note 1's own already-accepted cost: "his gnome may hold a secret that
2850// undercuts nothing he says." All seventeen secrets are one line each and note 2 is explicit
2851// that "nobody's secret is better than anybody else's" -- so this is a transaction-ordering
2852// race over an outcome with no prize, the same category v6 already analysed and accepted for
2853// the named-edition collision, at strictly lower stakes.
2854//
2855// WHAT THE DEVIATION FROM THE LITERAL RULE COSTS, measured rather than waved at: the probe and
2856// the subset draw agree EXACTLY at batchIndex 1 (nothing taken, so both are the plain weighted
2857// draw) and are FORCED IDENTICALLY at batchIndex 17 (a pool of one). In between they differ only
2858// in WHICH free secret a claimant lands on, never in how many free secrets there are. TRAITS.md
2859// section 3.2.2 note 1 has already priced that as "a fractional softening of a soft rule" against
2860// "a guaranteed solvable acrostic".
2861func secretFor(randomnessHex string, claimant address, boast, fear, batchIndex int64, taken uint32) int64 {
2862 // STEP 1. The weighted first choice. IDENTICAL CODE, IDENTICAL TABLE, IDENTICAL MODULUS for
2863 // the first gnome and the sixtieth.
2864 w, total := secretWeightsFor(boast, fear)
2865 first := int64(drawWeighted(w[:], total, seedFor(randomnessHex, claimant, "secret")))
2866
2867 // STEP 2. Past the forced window, that is the answer. Duplicates free.
2868 if batchIndex > firstDistinctCount {
2869 return first
2870 }
2871
2872 // STEP 3. Inside it, walk THIS CLAIMANT'S OWN probe sequence to the first free secret.
2873 stride := 1 + drawIndex(seedFor(randomnessHex, claimant, "secretstride"), int64(numSecrets-1))
2874 id := first
2875 for i := 0; i < numSecrets; i++ {
2876 if taken&(1<<uint(id)) == 0 {
2877 return id
2878 }
2879 id = (id + stride) % numSecrets
2880 }
2881 // Unreachable by P1 above. Named rather than omitted so this function is correct on its own
2882 // terms: if it ever fires, something has written secretsDrawnMask outside the one place that
2883 // may, and aborting is better than minting a gnome with a duplicate secret inside the window
2884 // the acrostic depends on.
2885 panic("garden: the first seventeen secret pool is exhausted; refusing to mint")
2886}
2887
2888// assertRoll is PHASE 2: every range assertion about a roll, in one block, before anything is
2889// committed. EVERY ONE OF THESE IS UNREACHABLE IF drawRoll IS CORRECT -- which is exactly v6's
2890// own discipline for its batch-accounting check ("named so this function is correct on its own
2891// terms"), and it means a drawRoll bug produces a LOUD ABORT rather than a silently malformed
2892// gnome that can never be fixed, on a realm that cannot be edited.
2893//
2894// taken is the SAME secretsDrawnMask that was handed to drawRoll, and it is here for the one
2895// assertion that range-checking cannot express: FREEDOM, not merely range. See the secret check
2896// below for why this one abort has to happen on the offending mint rather than one settle later.
2897func assertRoll(r roll, taken uint32) {
2898 if r.temperament < 0 || r.temperament >= int64(len(temperamentWeights)) {
2899 panic("garden: roll check: temperament out of range; refusing to mint")
2900 }
2901 if r.obsession < 0 || r.obsession >= int64(len(obsessionWeights)) {
2902 panic("garden: roll check: obsession out of range; refusing to mint")
2903 }
2904 if r.bond < 0 || r.bond >= int64(len(bondWeights)) {
2905 panic("garden: roll check: bond out of range; refusing to mint")
2906 }
2907 if r.fear < 0 || r.fear >= int64(len(fearWeights)) {
2908 panic("garden: roll check: fear out of range; refusing to mint")
2909 }
2910 if r.boast < 0 || r.boast >= int64(len(boastWeights)) {
2911 panic("garden: roll check: boast out of range; refusing to mint")
2912 }
2913 if r.bornUnder < 0 || r.bornUnder >= int64(len(bornUnderWeights)) {
2914 panic("garden: roll check: bornUnder out of range; refusing to mint")
2915 }
2916
2917 // The permutation check the five-slot encoding needs and a Lehmer index would not: every
2918 // axis index 0..4 present exactly once.
2919 var seen [numAxes]bool
2920 for _, slot := range r.dominance {
2921 if slot < 0 || slot >= numAxes {
2922 panic("garden: roll check: dominance holds a value that is not an axis index; refusing to mint")
2923 }
2924 if seen[slot] {
2925 panic("garden: roll check: dominance is not a permutation; refusing to mint")
2926 }
2927 seen[slot] = true
2928 }
2929
2930 if r.secret < 0 || r.secret >= numSecrets {
2931 panic("garden: roll check: secret out of range; refusing to mint")
2932 }
2933
2934 // AUDIT-01 YELLOW-3: INSIDE THE WINDOW, THE SECRET MUST ALSO BE FREE. Unreachable if
2935 // secretFor is correct -- its probe returns the first id with a clear bit in `taken`, or
2936 // aborts -- but the range check above cannot see a duplicate, and the consequence of not
2937 // seeing one is not local. settleCore's popcount check fires at the TOP of a settle, so a
2938 // taken id drawn inside the window would mint SUCCESSFULLY (range ok, `mask |= 1 << secret`
2939 // a no-op, settledCount++ lands) and then panic EVERY SUBSEQUENT SETTLE forever: there is no
2940 // admin mint, no migration and no repair path, so one malformed gnome would be a permanent
2941 // denial of the rest of the mint. Checking it here aborts ONLY the offending mint, which is
2942 // this function's own stated discipline -- a loud abort rather than a silently malformed
2943 // gnome. The downstream popcount assertion stays as well; it is cheap, and it is a genuine
2944 // second line against a write to the mask from somewhere this check cannot see.
2945 if r.batchIndex <= firstDistinctCount && taken&(1<<uint(r.secret)) != 0 {
2946 panic("garden: roll check: the drawn secret is already taken inside the first seventeen; refusing to mint")
2947 }
2948
2949 // The stats, against the rule rather than against the table they came from -- so a corrupt
2950 // table entry is caught here too, not merely a corrupt index.
2951 stats := [numStats]int64{r.pluck, r.nous, r.grumble, r.appetite}
2952 sum, hi, lo := int64(0), stats[0], stats[0]
2953 for _, v := range stats {
2954 if v < statMin || v > statMax {
2955 panic("garden: roll check: a stat is outside 1-10; refusing to mint")
2956 }
2957 sum += v
2958 if v > hi {
2959 hi = v
2960 }
2961 if v < lo {
2962 lo = v
2963 }
2964 }
2965 if sum != statPointTotal {
2966 panic("garden: roll check: the four stats do not sum to 22; refusing to mint")
2967 }
2968 if hi < statHighAtLeast || lo > statLowAtMost {
2969 panic("garden: roll check: the stat line is not spiky (needs one >= 8 and one <= 3); refusing to mint")
2970 }
2971
2972 if r.careThresholdDraw < 0 || r.careThresholdDraw >= careThresholdSpan {
2973 panic("garden: roll check: the care threshold draw is outside 0-31; refusing to mint")
2974 }
2975 if r.batchIndex < 1 || r.batchIndex > batchCap {
2976 panic("garden: roll check: batchIndex is outside 1-" + strconv.FormatInt(batchCap, 10) + "; refusing to mint")
2977 }
2978}
2979
2980// ============================================================================
2981// Wake: switching which gnome is awake. MINT.md section 2a.
2982// ============================================================================
2983
2984// Wake makes tokenID the caller's awake gnome. Free, and limited to once per UTC day -- the same
2985// day-boundary discipline SaveDay's own dailyCareDay already uses, chosen over a GNOT fee for the
2986// reason MINT.md section 2a gives: "a cost that cannot be bought, only lived".
2987func Wake(cur realm, tokenID string) {
2988 if !unsafe.OriginSend().IsZero() {
2989 panic("garden: wake accepts no coins; switching which gnome is awake is free, once per UTC day")
2990 }
2991
2992 if tokenID == genesisTokenID {
2993 panic("garden: the genesis gnome belongs to nobody and can never be woken")
2994 }
2995
2996 caller := cur.Previous().Address()
2997 if !caller.IsValid() {
2998 panic("garden: wake requires a valid caller address")
2999 }
3000
3001 owner, ok := ownerOfToken(tokenID)
3002 if !ok || owner != caller {
3003 panic("garden: you do not own that gnome")
3004 }
3005
3006 if active, hasActive := activeToken.Get(caller.String()).(string); hasActive && active == tokenID {
3007 panic("garden: that gnome is already awake")
3008 }
3009
3010 now := time.Now().Unix()
3011 today := dayNumber(now)
3012 if last, isInt := lastWakeDay.Get(caller.String()).(int64); isInt && last == today {
3013 panic("garden: switching which gnome is awake is limited to once per UTC day; try again tomorrow")
3014 }
3015
3016 // g must exist: ownerOfToken above already confirmed tokenID is a real, owned token.
3017 // Recording lastWokenAt HERE, on the token actually being woken, is the other half of the
3018 // fix for ATTACK-REVIEW.md goal 7 -- see the gnome struct's own comment on the field. The
3019 // token being put to sleep (whichever was active(caller) a moment ago, if any) needs no
3020 // symmetrical write: its own lastActionAt/caredDayCount are already frozen facts the instant
3021 // it stops being the active one (SaveDay can only ever act on activeGnomeForAddr's result),
3022 // and the next time anything reactivates IT, that reactivation (another Wake, a later Buy by
3023 // a new owner, etc.) will set its lastWokenAt correctly at that time, exactly as this call is
3024 // doing now.
3025 g, ok := gnomeByTokenID(tokenID)
3026 if !ok {
3027 panic("garden: that gnome does not exist")
3028 }
3029 g.lastWokenAt = now
3030 gnomes.Set(tokenID, g)
3031
3032 activeToken.Set(caller.String(), tokenID)
3033 lastWakeDay.Set(caller.String(), today)
3034
3035 chain.Emit("Wake", "owner", caller.String(), "tokenId", tokenID)
3036}
3037
3038// ============================================================================
3039// SaveDay: v4's shape, exactly, resolved through the caller's ACTIVE gnome rather than "the"
3040// gnome for that address -- because after mint one/hold many/play one, there no longer is one.
3041// A sleeping gnome is simply never found here: activeGnomeForAddr only ever returns the awake
3042// token, so care applied to "my gnome" always means the one gnome actually allowed to earn
3043// anything (MINT.md section 2a: "a sleeping gnome earns zero -- no cared days, no streak, no
3044// level, no XP").
3045// ============================================================================
3046
3047type saveAction struct {
3048 kind string
3049 at int64
3050 plot int
3051}
3052
3053func SaveDay(cur realm, actionsRaw string) {
3054 if !unsafe.OriginSend().IsZero() {
3055 panic("garden: save day accepts no coins; only Buy takes payment, and nothing else here can return them")
3056 }
3057
3058 caller := cur.Previous().Address()
3059 if !caller.IsValid() {
3060 panic("garden: save day requires a valid caller address")
3061 }
3062
3063 tid, g, ok := activeGnomeForAddr(caller)
3064 if !ok {
3065 panic("garden: no gnome is awake for this address; claim or buy one, or Wake one of your own")
3066 }
3067
3068 now := time.Now().Unix()
3069 actions := parseActions(actionsRaw)
3070
3071 if len(actions) == 0 {
3072 chain.Emit("SaveDay", "owner", caller.String(), "tokenId", tid, "actions", "0", "xpApplied", "0")
3073 return
3074 }
3075
3076 earliestAllowed := now - maxBackfillHours*hourSeconds
3077
3078 // awakeSince: the moment THIS token last became the active one (set by settleCore, Buy, and
3079 // Wake -- see the gnome struct's own comment). ATTACK-REVIEW.md goal 7: without this, a
3080 // token's own earliestAllowed was bounded only by its own lastActionAt and the 48-hour
3081 // horizon, with nothing checking that the backfilled window actually fell inside a period
3082 // this specific token was awake. A token that just became active (whether by a brand-new
3083 // Claim/Settle, a Buy, or an explicit Wake switching away from a sibling in the same stable)
3084 // cannot backfill anything from before that moment -- it was asleep then, and MINT.md /
3085 // DECISIONS.md are both explicit that a sleeping gnome earns zero, full stop, not "zero
3086 // unless a sibling token happens to cover the gap."
3087 awakeSince := g.lastWokenAt
3088 if awakeSince > earliestAllowed {
3089 earliestAllowed = awakeSince
3090 }
3091
3092 firstDay := dayNumber(actions[0].at)
3093 prevAt := g.lastActionAt
3094 if prevAt < earliestAllowed {
3095 prevAt = earliestAllowed
3096 }
3097 for _, a := range actions {
3098 if a.at <= earliestAllowed {
3099 if a.at <= awakeSince {
3100 panic("garden: this action predates the last time this gnome was woken; a sleeping gnome earns nothing for the time it was asleep")
3101 }
3102 panic("garden: this action is more than " + strconv.FormatInt(maxBackfillHours, 10) + " hours old; care can only be backfilled for the last " + strconv.FormatInt(maxBackfillHours, 10) + " hours, not longer absences")
3103 }
3104 if a.at <= prevAt {
3105 panic("garden: action timestamp must be strictly after the last saved action")
3106 }
3107 if a.at > now {
3108 panic("garden: action timestamp cannot be in the future")
3109 }
3110 if dayNumber(a.at) != firstDay {
3111 panic("garden: every action in one SaveDay call must fall on the same UTC day")
3112 }
3113 prevAt = a.at
3114 }
3115
3116 work := *g
3117 acorns := balanceOf(caller)
3118 spentThisCall := false
3119
3120 gateWaived := acorns < acornReserve && !anyPlotPlanted(g)
3121 rawCareXp := int64(0)
3122 tinkeredThisCall := false
3123
3124 for _, a := range actions {
3125 switch a.kind {
3126 case actionPat:
3127 if !canPat(work.lastPatAt, a.at, patCooldownHours) {
3128 panic("garden: pat must be at least " + strconv.FormatInt(patCooldownHours, 10) + " hours after the last one")
3129 }
3130 work.lastPatAt = a.at
3131 work.lastHappyEventAt = a.at
3132 work.cheer = clampMeter(meterNow(work.cheer, work.cheerAt, a.at, cheerDropEveryHours) + patCheer)
3133 work.cheerAt = a.at
3134 rawCareXp += xpPat
3135
3136 case actionFeed:
3137 if acorns < feedCost {
3138 panic("garden: not enough acorns to feed")
3139 }
3140 acorns -= feedCost
3141 spentThisCall = true
3142 work.belly = clampMeter(meterNow(work.belly, work.bellyAt, a.at, bellyDropEveryHours) + feedBelly)
3143 work.bellyAt = a.at
3144 work.lastHappyEventAt = a.at
3145 rawCareXp += xpFeed
3146
3147 case actionPlant:
3148 waived := gateWaived && acorns < acornReserve
3149 if !waived && meterNow(work.belly, work.bellyAt, a.at, bellyDropEveryHours) <= hungryAtOrBelow {
3150 panic("garden: too hungry to garden; feed first")
3151 }
3152 p := &work.plots[a.plot]
3153 if p.plantedAt != 0 {
3154 panic("garden: that plot is already planted")
3155 }
3156 if acorns < plantCost {
3157 panic("garden: not enough acorns to plant")
3158 }
3159 acorns -= plantCost
3160 spentThisCall = true
3161 p.plantedAt = a.at
3162 p.readyAt = a.at + growHours*hourSeconds
3163
3164 case actionHarvest:
3165 p := &work.plots[a.plot]
3166 if p.plantedAt == 0 {
3167 panic("garden: that plot has nothing to harvest")
3168 }
3169 if a.at < p.readyAt {
3170 panic("garden: that plot is not ready to harvest yet")
3171 }
3172 p.plantedAt = 0
3173 p.readyAt = 0
3174 acorns += harvestPerPlot
3175 work.xpGarden += xpHarvestPerPlot
3176 work.harvestCount++
3177 rawCareXp += xpHarvestPerPlot
3178
3179 case actionTinker:
3180 if tinkeredThisCall {
3181 panic("garden: only one tinker task per day")
3182 }
3183 if work.lastTinkerAt != 0 && dayNumber(work.lastTinkerAt) == dayNumber(a.at) {
3184 panic("garden: already did a tinker task today")
3185 }
3186 if acorns < tinkerCost {
3187 panic("garden: not enough acorns to tinker")
3188 }
3189 acorns -= tinkerCost
3190 spentThisCall = true
3191 work.xpTinker += xpTinkerTask
3192 work.lastTinkerAt = a.at
3193 tinkeredThisCall = true
3194 rawCareXp += xpTinkerTask
3195
3196 default:
3197 panic("garden: unknown action kind")
3198 }
3199 }
3200
3201 if spentThisCall && acorns < acornReserve && !anyPlotPlanted(&work) {
3202 panic("garden: that would leave fewer than " + strconv.FormatInt(acornReserve, 10) + " acorns with nothing planted, and a gnome with no acorns and no garden can never earn another; plant first, or keep " + strconv.FormatInt(acornReserve, 10) + " back")
3203 }
3204
3205 latestActionAt := actions[len(actions)-1].at
3206 bellyAtEnd := meterNow(work.belly, work.bellyAt, latestActionAt, bellyDropEveryHours)
3207 mood := moodNow(bellyAtEnd, work.lastPatAt, work.lastHappyEventAt, work.adoptedAt, latestActionAt, hungryAtOrBelow)
3208
3209 adjustedXp := rawCareXp
3210 switch mood {
3211 case moodHappy:
3212 adjustedXp = adjustedXp * (100 + happyBonusPct) / 100
3213 case moodSulky:
3214 adjustedXp = adjustedXp * (100 - sulkyPenaltyPct) / 100
3215 }
3216
3217 if work.dailyCareDay != firstDay {
3218 work.dailyCareDay = firstDay
3219 work.dailyCareXp = 0
3220 }
3221 remainingToday := dailyCareCap - work.dailyCareXp
3222 if remainingToday < 0 {
3223 remainingToday = 0
3224 }
3225 if adjustedXp > remainingToday {
3226 adjustedXp = remainingToday
3227 }
3228 work.dailyCareXp += adjustedXp
3229
3230 work.totalXp += adjustedXp
3231 work.level = levelFromXp(work.totalXp)
3232
3233 // At most ONE of this owner's tokens may ever bank a given UTC day as cared-for -- see
3234 // lastCaredDayForOwner's own comment. Without this, Wake (once per day, but still once)
3235 // lets an owner genuinely care for A earlier today, switch to B, and genuinely care for B
3236 // too, banking the SAME calendar day on both from nothing but real, non-backfilled
3237 // timestamps -- the live-switch half of ATTACK-REVIEW.md goal 7 that lastWokenAt's backfill
3238 // floor does not by itself prevent. XP, acorns, feeding, planting and harvesting are
3239 // deliberately NOT gated here: those are this token's own resource mechanics (already
3240 // bounded per-gnome by dailyCareCap) and giving B real care today is still real care: only
3241 // the CARED-DAY LEDGER -- the thing lockCaredDays and the public streak actually count -- is
3242 // restricted to one credit per owner per day.
3243 if lastOwnerDay, ok := lastCaredDayForOwner.Get(caller.String()).(int64); !ok || lastOwnerDay != firstDay {
3244 if recordCaredDay(&work, firstDay) {
3245 lastCaredDayForOwner.Set(caller.String(), firstDay)
3246 }
3247 }
3248 work.lastActionAt = latestActionAt
3249
3250 setBalance(caller, acorns)
3251 *g = work
3252 gnomes.Set(tid, g)
3253
3254 chain.Emit(
3255 "SaveDay",
3256 "owner", caller.String(),
3257 "tokenId", tid,
3258 "actions", strconv.Itoa(len(actions)),
3259 "xpApplied", strconv.FormatInt(adjustedXp, 10),
3260 "mood", mood,
3261 )
3262}
3263
3264// ============================================================================
3265// ChooseType: v4's shape, resolved through the active gnome.
3266// ============================================================================
3267
3268func ChooseType(cur realm, kind string) {
3269 if !unsafe.OriginSend().IsZero() {
3270 panic("garden: choose type accepts no coins; only Buy takes payment, and nothing else here can return them")
3271 }
3272
3273 caller := cur.Previous().Address()
3274 if !caller.IsValid() {
3275 panic("garden: choose type requires a valid caller address")
3276 }
3277
3278 tid, g, ok := activeGnomeForAddr(caller)
3279 if !ok {
3280 panic("garden: no gnome is awake for this address")
3281 }
3282
3283 if g.gnomeType != "" {
3284 panic("garden: this gnome's calling is already chosen and cannot be changed")
3285 }
3286
3287 level := levelFromXp(g.totalXp)
3288 if level < typeAtLevel {
3289 panic("garden: a calling can only be chosen at level " + strconv.FormatInt(typeAtLevel, 10) + " or above")
3290 }
3291
3292 if kind != typeGardener && kind != typeExplorer && kind != typeTinkerer {
3293 panic("garden: a calling is one of \"" + typeGardener + "\", \"" + typeExplorer + "\" or \"" + typeTinkerer + "\"")
3294 }
3295
3296 work := *g
3297 work.gnomeType = kind
3298 work.level = level
3299 *g = work
3300 gnomes.Set(tid, g)
3301
3302 chain.Emit("ChooseType", "owner", caller.String(), "tokenId", tid, "type", kind)
3303}
3304
3305// ============================================================================
3306// v6 FIX 1: SetName. THE ONLY NEW WRITE SURFACE IN THIS WHOLE FILE RELATIVE TO v5.
3307//
3308// WHY IT IS A SEPARATE FUNCTION AND NOT A PARAMETER ON Claim OR Settle, which is the first
3309// thing a reader will wonder and the one design question here that is already settled: the mint
3310// is a ROLL, and the whole safety property of the two-phase roll (MINT.md section 10.5, and this
3311// file's own package doc at length) is that the claimant does not and cannot choose what comes
3312// out of it. A name parameter on Claim would not technically break that -- a name does not
3313// influence the draw -- but it would blur the one sentence the product has to be able to say
3314// without an asterisk ("you do not choose what you get"), and it would mean a claimant has to
3315// have decided on a name at the exact moment they are least able to: before they have any idea
3316// what they are naming. Naming what you GOT is a different act from getting it, so it is a
3317// different function, called later, by the owner, as many times as they like.
3318//
3319// ONLY THE CURRENT OWNER. Not the original claimant -- the CURRENT owner. A gnome that has been
3320// sold belongs to its buyer, and so does its name; the seller loses the ability to rename it at
3321// the same instant they lose the token, through the same ownerOfToken check every other
3322// owner-gated function in this file uses (Wake, List). There is no admin override: this realm's
3323// admin can gate issuance and nothing else (see AdminAllow's own comment), and a name is not
3324// issuance.
3325// ============================================================================
3326
3327// SetName names tokenID, which the caller must currently own, or RENAMES it. Free, unlimited,
3328// and refused for the eight named one-of-ones and the genesis gnome.
3329//
3330// RENAMING IS ALLOWED, NOT ONE-SHOT, and this is a deliberate decision with a named trade-off
3331// rather than an oversight. On an immutable realm a one-shot name means a typo is forever, and
3332// "my gnome is called Fenncik" is a worse permanent outcome than "somebody changed their mind".
3333// Aavegotchi, the closest prior art this project has looked at, allows renaming. What it costs:
3334// a name is not a stable identifier over time, so nothing anywhere -- in this realm, in the
3335// adventure realm, or in the app -- may ever key anything on a name. tokenID is the identifier;
3336// the name is a label. (The nameOwner index is keyed BY name only to answer "is this taken",
3337// never to identify a gnome; every function here looks tokens up by tokenID.)
3338//
3339// Refuses coins for the same reason every other non-paying function here does: there is no
3340// banker to return them, and naming your own gnome is free (MINT.md section 14 -- batch one is
3341// free end to end, and this is not a new place to charge).
3342func SetName(cur realm, tokenID string, name string) {
3343 if !unsafe.OriginSend().IsZero() {
3344 panic("garden: set name accepts no coins; naming your own gnome is free, and nothing here can return them")
3345 }
3346
3347 // Checked FIRST and by name, before the ownership lookup that would also refuse it: the
3348 // genesis gnome belongs to nobody, so there is no caller for whom the owner check could ever
3349 // pass, but saying so explicitly is this file's established discipline for it (List, Buy and
3350 // Wake all do the same) and it gives a reader of the panic the real reason rather than a
3351 // misleading "you do not own that gnome".
3352 if tokenID == genesisTokenID {
3353 panic("garden: the genesis gnome belongs to nobody and can never be renamed")
3354 }
3355
3356 caller := cur.Previous().Address()
3357 if !caller.IsValid() {
3358 panic("garden: set name requires a valid caller address")
3359 }
3360
3361 owner, ok := ownerOfToken(tokenID)
3362 if !ok || owner != caller {
3363 panic("garden: you do not own that gnome")
3364 }
3365
3366 g, isGnome := gnomeByTokenID(tokenID)
3367 if !isGnome {
3368 panic("garden: that tokenID does not exist")
3369 }
3370
3371 // THE EIGHT NAMED ONE-OF-ONES KEEP THEIR CHARACTER NAME, PERMANENTLY. Hob is Hob; that is
3372 // the entire point of a cap-one edition, and an owner who could rename Hob to "xXx" would be
3373 // destroying the thing they were lucky enough to roll, irreversibly, for everyone who comes
3374 // after them. This also closes the obvious way around the reservation: if a named edition
3375 // could be renamed, its own canonical key would be freed by the rename (SetName's commit
3376 // block frees the old key by design -- see nameOwner's comment) and "hob" would become
3377 // takeable by an ordinary gnome even though reservedNameKeys still lists it, because
3378 // reservation is checked against the NEW name, never the freed one. Refusing the rename
3379 // outright is what keeps those two mechanisms from interacting.
3380 if g.character != "" {
3381 panic("garden: " + g.character + " is a named one-of-one and keeps its own name permanently; only ordinary gnomes can be named")
3382 }
3383
3384 // A LISTED GNOME CANNOT BE RENAMED, because the seller still owns it right up until the
3385 // instant Buy lands and every guard above would therefore pass. THE ATTACK: Alice names
3386 // token A "Pebble" and lists it. Bob reads the name, broadcasts Buy(A) with the full price.
3387 // Before Bob's transaction lands, Alice calls SetName(A, "Junk") -- she is still the owner,
3388 // so nothing refuses her -- and then SetName(B, "Pebble") on a second token she keeps, which
3389 // the rename just freed the canonical key for (see the commit block below: the old key is
3390 // released by design). Bob's Buy still succeeds, at the agreed price, for a gnome now called
3391 // "Junk", and "Pebble" is permanently out of his reach because names are unique and
3392 // first-come. Buy cannot defend itself here: it never reads g.name and takes no expected-name
3393 // argument, so there is nothing for it to compare. Refusing the rename while a listing stands
3394 // is what makes the name a buyer reads part of what they are actually buying.
3395 //
3396 // The test is listingFor, the SAME predicate Cancel uses for "is this gnome for sale" -- not
3397 // a second notion of "listed" that could drift out of step with the marketplace's own. It is
3398 // deliberately presence-based rather than presence-plus-unexpired: an expired listing is
3399 // unbuyable (Buy refuses it) so renaming under one is harmless, but copying Buy's inline
3400 // expiry comparison here would be a second copy of a liveness rule that could later drift,
3401 // and refusing slightly too much costs the owner one Cancel call while refusing slightly too
3402 // little reopens the hole on an immutable realm.
3403 if _, isListed := listingFor(tokenID); isListed {
3404 panic("garden: that gnome is listed for sale and cannot be renamed while a buyer may already have a purchase in flight; cancel the listing first, then rename it")
3405 }
3406
3407 // All validation and every refusal happens in here, before any write below.
3408 assertNameAssignable(name, tokenID)
3409
3410 // ---- commit. Nothing below this line can fail. ----
3411
3412 // FREE THE OLD KEY BEFORE TAKING THE NEW ONE. Required, not tidiness -- see nameOwner's own
3413 // comment: without this, one token could hoard every name in the collection by renaming
3414 // repeatedly. Order matters for one specific case: renaming a gnome to a different
3415 // capitalisation of the name it already has ("fennick" -> "Fennick") canonicalises to the
3416 // SAME key, so removing first and setting second is the only order that does not either
3417 // delete the key it just wrote or refuse a legal, harmless re-capitalisation.
3418 if g.name != "" {
3419 nameOwner.Remove(canonicalNameKey(g.name))
3420 }
3421 nameOwner.Set(canonicalNameKey(name), tokenID)
3422
3423 previous := g.name
3424 g.name = name
3425 gnomes.Set(tokenID, g)
3426
3427 chain.Emit(
3428 "SetName",
3429 "owner", caller.String(),
3430 "tokenId", tokenID,
3431 "name", name,
3432 "previousName", previous,
3433 )
3434}
3435
3436// assertNameAssignable is SetName's refusal gate: it panics with nameAssignabilityProblem's
3437// message, or returns normally if name may be assigned to tokenID. The rules themselves are in
3438// nameAssignabilityProblem, not here, so the free NameAvailability query and this enforcing path
3439// can never disagree about what is allowed -- there is exactly one copy.
3440func assertNameAssignable(name string, tokenID string) {
3441 if problem := nameAssignabilityProblem(name, tokenID); problem != "" {
3442 panic(problem)
3443 }
3444}
3445
3446// nameAssignabilityProblem is THE one copy of the naming rules. It returns "" if name may be
3447// assigned to tokenID, or the exact player-meaningful message explaining which rule refused it.
3448//
3449// IT RETURNS ITS REFUSAL RATHER THAN PANICKING IT so that NameAvailability can offer the same
3450// answer as a free read without any recover() -- see NameAvailability's own note on why recover
3451// is avoided on this toolchain.
3452//
3453// ORDER IS DELIBERATE AND MATCHES app/js/config.js's nameError, which mirrors v4's realm order:
3454// length, then charset, then visible content, then (new in v6) reservation, then uniqueness. A
3455// name that fails several rules reports the first, so the app's own message and the realm's
3456// refusal always name the SAME rule for the same input rather than disagreeing about which
3457// problem is the problem.
3458func nameAssignabilityProblem(name string, tokenID string) string {
3459 if !validNameLength(name) {
3460 return "garden: name must be 1-" + strconv.Itoa(maxNameLen) + " characters"
3461 }
3462
3463 // Checked before the general charset refusal purely so the message is useful: iOS "smart
3464 // punctuation" is ON BY DEFAULT and silently rewrites a straight apostrophe to U+2018/U+2019
3465 // and a hyphen to U+2013/U+2014 (app/js/config.js's third audit round, finding Y8). A player
3466 // who typed "O'Hare" on an iPhone did not type anything strange, and telling them their name
3467 // "contains a character that is not an allowed letter" would be true but useless.
3468 //
3469 // THIS IS A REFUSAL, NOT A TRANSFORM, AND THAT IS THE POINT. config.js's normalizeName folds
3470 // those four code points back to ASCII in the BROWSER, before submitting -- a narrowing
3471 // transform whose own comment notes "the realm remains the sole authority". This realm
3472 // deliberately does NOT fold them itself: a name is now a reserved, unique, first-come
3473 // resource, so silently rewriting what somebody signed for would mean the key they got is not
3474 // the key they asked for, and two players could race for a name neither of them typed.
3475 // Refusing and naming the reason keeps the realm's accepted set exactly as narrow as v4's
3476 // while still telling an iPhone player what happened.
3477 if hasSmartPunctuation(name) {
3478 return "garden: name contains curly quotes or dashes, which a phone keyboard may have substituted for a plain apostrophe or hyphen; retype it with a straight ' or -"
3479 }
3480
3481 if !validNameCharset(name) {
3482 return "garden: name contains a character that is not an allowed letter, digit, space, hyphen or apostrophe"
3483 }
3484
3485 if !hasVisibleNameContent(name) {
3486 return "garden: name must contain at least one visible character"
3487 }
3488
3489 key := canonicalNameKey(name)
3490
3491 // RESERVATION. An ordinary gnome may not be named after a cap-one edition or the genesis
3492 // gnome, whether or not that edition has minted yet -- see reservedNameKeys' own comment for
3493 // why this is not already covered by the uniqueness check below.
3494 for _, reserved := range reservedNameKeys {
3495 if key == reserved {
3496 return "garden: \"" + name + "\" is reserved for a named one-of-one and cannot be taken by an ordinary gnome"
3497 }
3498 }
3499
3500 // UNIQUENESS. Checked against the CANONICAL key, so case and spacing variants of a taken name
3501 // are refused too -- the cheapest impersonation attempt there is.
3502 //
3503 // The `holder != tokenID` test is what makes a HARMLESS self-rename legal: re-capitalising
3504 // your own gnome's own name ("fennick" -> "Fennick") canonicalises to a key you already hold,
3505 // and refusing that as "already taken" would be refusing you your own name. Callers who want
3506 // the general "is this free for anybody" question pass tokenID "" (NameAvailability's own
3507 // documented shape), which can never equal a real tokenID, so nothing is accidentally
3508 // exempted by it.
3509 if holder, taken := nameOwnerOf(key); taken && holder != tokenID {
3510 return "garden: the name \"" + name + "\" is already taken by gnome " + holder + "; names are unique across the collection"
3511 }
3512
3513 return ""
3514}
3515
3516// ============================================================================
3517// SALE: List/Cancel/Buy. v4's shape, DESIGN.md section 9's reasoning unchanged, minus the
3518// one-gnome-per-address refusal in Buy (MINT.md section 2a: removed from Buy, never from Claim)
3519// and plus a tokenID parameter on List/Cancel, because an owner may now have more than one
3520// gnome to choose among, and plus the lockCaredDays check on the eight named one-of-ones.
3521// ============================================================================
3522
3523// List offers tokenID, which the caller must own, for sale. The lock: a named one-of-one
3524// (character != "") cannot be listed until it has lockCaredDays cared days; an ordinary gnome
3525// has no such restriction, trading from the moment it is minted (MINT.md section 12/13).
3526func List(cur realm, tokenID string, priceUgnot int64, buyer address, days int64) {
3527 if !unsafe.OriginSend().IsZero() {
3528 panic("garden: list accepts no coins; only Buy takes payment")
3529 }
3530
3531 if tokenID == genesisTokenID {
3532 panic("garden: the genesis gnome belongs to nobody and can never be listed")
3533 }
3534
3535 caller := cur.Previous().Address()
3536 if !caller.IsValid() {
3537 panic("garden: list requires a valid caller address")
3538 }
3539
3540 owner, ok := ownerOfToken(tokenID)
3541 if !ok || owner != caller {
3542 panic("garden: you do not own that gnome")
3543 }
3544
3545 gv := gnomes.Get(tokenID)
3546 g, isGnome := gv.(*gnome)
3547 if !isGnome || g == nil {
3548 panic("garden: that tokenID does not exist")
3549 }
3550
3551 if g.character != "" && g.caredDayCount < lockCaredDays {
3552 // THIS MESSAGE SAYS THE ROSTER SIZE OUT LOUD, which makes it the FOURTH place v7 change 10
3553 // had to edit (after namedEditionOrder, namedEditionDisplayNames and reservedNameKeys) and
3554 // the one the existing comments did not warn about. The test suite's lockedMsg helper
3555 // mirrors this string byte-for-byte, so a change here that is not made there fails
3556 // TestNamedOneOfOneCannotBeListedBeforeTheLockClears rather than shipping.
3557 panic("garden: " + g.character + " is one of the eight named one-of-ones and cannot be listed until it has " +
3558 strconv.FormatInt(lockCaredDays, 10) + " cared days; it has " + strconv.FormatInt(g.caredDayCount, 10))
3559 }
3560
3561 if priceUgnot < 0 {
3562 panic("garden: a price cannot be negative")
3563 }
3564 if priceUgnot > maxPriceUgnot {
3565 panic("garden: price exceeds the maximum of " + strconv.FormatInt(maxPriceUgnot, 10) + " ugnot")
3566 }
3567 if days < 1 || days > maxListingDays {
3568 panic("garden: a listing lasts between 1 and " + strconv.FormatInt(maxListingDays, 10) + " days")
3569 }
3570
3571 buyer = address(lowerASCII(string(buyer)))
3572 if buyer != address("") {
3573 if !buyer.IsValid() {
3574 panic("garden: reserved buyer is not a valid address; pass an empty address to let anyone buy")
3575 }
3576 if buyer == caller {
3577 panic("garden: you cannot reserve your own gnome for yourself")
3578 }
3579 }
3580
3581 now := time.Now().Unix()
3582 listings.Set(tokenID, &listing{
3583 seller: caller,
3584 priceUgnot: priceUgnot,
3585 buyer: buyer,
3586 listedAt: now,
3587 expiresAt: now + days*daySeconds,
3588 })
3589
3590 feeUgnot := feeFor(priceUgnot)
3591 chain.Emit(
3592 "List",
3593 "seller", caller.String(),
3594 "tokenId", tokenID,
3595 "priceUgnot", strconv.FormatInt(priceUgnot, 10),
3596 "feeUgnot", strconv.FormatInt(feeUgnot, 10),
3597 "totalUgnot", strconv.FormatInt(priceUgnot+feeUgnot, 10),
3598 "buyer", buyer.String(),
3599 "expiresAt", strconv.FormatInt(now+days*daySeconds, 10),
3600 )
3601}
3602
3603// Cancel withdraws the caller's own listing on tokenID. Only the seller who created it may.
3604func Cancel(cur realm, tokenID string) {
3605 if !unsafe.OriginSend().IsZero() {
3606 panic("garden: cancel accepts no coins; only Buy takes payment")
3607 }
3608
3609 caller := cur.Previous().Address()
3610 if !caller.IsValid() {
3611 panic("garden: cancel requires a valid caller address")
3612 }
3613
3614 l, ok := listingFor(tokenID)
3615 if !ok {
3616 panic("garden: that gnome is not for sale")
3617 }
3618 if l.seller != caller {
3619 panic("garden: only the seller can cancel their own listing")
3620 }
3621
3622 listings.Remove(tokenID)
3623 chain.Emit("Cancel", "seller", caller.String(), "tokenId", tokenID)
3624}
3625
3626// Buy purchases the listed gnome tokenID. v4's validate-then-commit shape, unchanged, MINUS the
3627// one-gnome-per-address refusal: MINT.md section 2a removes the hold cap from Buy specifically,
3628// and only from Buy -- Claim's hasRolled gate is the cap that still matters, and this function
3629// never touches it.
3630func Buy(cur realm, tokenID string) {
3631 if !cur.Previous().IsUserCall() {
3632 panic("garden: buy must be called directly by a wallet, not through another realm; the coins attached to a transaction belong to the realm the wallet called")
3633 }
3634 buyer := cur.Previous().Address()
3635 if !buyer.IsValid() {
3636 panic("garden: buy requires a valid caller address")
3637 }
3638
3639 if tokenID == genesisTokenID {
3640 panic("garden: the genesis gnome belongs to nobody and can never be bought")
3641 }
3642
3643 l, ok := listingFor(tokenID)
3644 if !ok {
3645 panic("garden: that gnome is not for sale")
3646 }
3647
3648 now := time.Now().Unix()
3649 if now > l.expiresAt {
3650 panic("garden: that listing has expired; the seller must list again")
3651 }
3652 if l.seller == buyer {
3653 panic("garden: you cannot buy your own gnome")
3654 }
3655 if l.buyer != address("") && l.buyer != buyer {
3656 panic("garden: that gnome is reserved for a different buyer")
3657 }
3658
3659 tid := grc721.TokenID(tokenID)
3660 owner, err := token.OwnerOf(tid)
3661 if err != nil || owner != l.seller {
3662 panic("garden: that listing is stale; the seller no longer owns that gnome")
3663 }
3664 gv := gnomes.Get(tokenID)
3665 g, isGnome := gv.(*gnome)
3666 if !isGnome || g == nil {
3667 panic("garden: that listing names a gnome this realm does not hold")
3668 }
3669
3670 feeUgnot := feeFor(l.priceUgnot)
3671 total := l.priceUgnot + feeUgnot
3672 send := unsafe.OriginSend()
3673 if !exactUgnot(send, total) {
3674 panic("garden: buy requires exactly " + strconv.FormatInt(total, 10) + ugnotDenom +
3675 " attached (" + strconv.FormatInt(l.priceUgnot, 10) + " price + " +
3676 strconv.FormatInt(feeUgnot, 10) + " fee); got " + coinsString(send))
3677 }
3678
3679 // ---- commit. Nothing below this line can fail on chain. ----
3680
3681 transferGate = tokenID
3682 if err := ledger.TransferFrom(l.seller, l.seller, buyer, tid); err != nil {
3683 panic(err)
3684 }
3685 transferGate = ""
3686
3687 removeOwnerToken(l.seller, tokenID)
3688 addOwnerToken(buyer, tokenID)
3689 listings.Remove(tokenID)
3690
3691 // Active-token bookkeeping (mint one, hold many, play one). If this was the seller's awake
3692 // gnome, they fall asleep entirely rather than another of their gnomes quietly becoming
3693 // active -- switching is always a deliberate Wake, never a side effect of a sale. If the
3694 // buyer currently has nothing awake at all, this purchase becomes their active gnome
3695 // automatically, so a first-time buyer is never left with nothing playable.
3696 if av, isActive := activeToken.Get(l.seller.String()).(string); isActive && av == tokenID {
3697 activeToken.Remove(l.seller.String())
3698 }
3699 if !activeToken.Has(buyer.String()) {
3700 activeToken.Set(buyer.String(), tokenID)
3701 }
3702
3703 settled := int64(0)
3704 for i := 0; i < numPlots; i++ {
3705 p := &g.plots[i]
3706 if p.plantedAt == 0 {
3707 continue
3708 }
3709 if now >= p.readyAt {
3710 settled += harvestPerPlot
3711 } else {
3712 settled += plantCost
3713 }
3714 p.plantedAt = 0
3715 p.readyAt = 0
3716 }
3717 if settled > 0 {
3718 setBalance(l.seller, balanceOf(l.seller)+settled)
3719 }
3720
3721 g.acquiredAt = now
3722 g.saleCount++
3723 g.lastActionAt = now
3724 // lastWokenAt resets here too, same as lastActionAt, whether or not this purchase becomes
3725 // the buyer's active gnome below -- if it does not (buyer already has something awake), this
3726 // value is simply overwritten by the eventual explicit Wake that does activate it. Either
3727 // way, nothing the SELLER (or anyone else) did with this token before the sale can be
3728 // backfilled by the BUYER afterward: SaveDay's floor cannot reach earlier than this moment.
3729 g.lastWokenAt = now
3730 gnomes.Set(tokenID, g)
3731
3732 grantStartingPurseIfNew(buyer)
3733
3734 bank := banker.NewBanker(banker.BankerTypeOriginSend, cur)
3735 me := cur.Address()
3736 if l.priceUgnot > 0 {
3737 bank.SendCoins(me, l.seller, chain.Coins{{Denom: ugnotDenom, Amount: l.priceUgnot}})
3738 }
3739 bank.SendCoins(me, fee.recipient, chain.Coins{{Denom: ugnotDenom, Amount: feeUgnot}})
3740
3741 chain.Emit(
3742 "Sale",
3743 "tokenId", tokenID,
3744 "seller", l.seller.String(),
3745 "buyer", buyer.String(),
3746 "priceUgnot", strconv.FormatInt(l.priceUgnot, 10),
3747 "feeUgnot", strconv.FormatInt(feeUgnot, 10),
3748 "feeRecipient", fee.recipient.String(),
3749 "settledAcorns", strconv.FormatInt(settled, 10),
3750 )
3751}
3752
3753// ============================================================================
3754// Readers
3755// ============================================================================
3756
3757func Listing(tokenID string) (seller address, priceUgnot int64, buyer address, listedAtUnix, expiresAtUnix, feeUgnot, totalUgnot int64, found bool) {
3758 l, ok := listingFor(tokenID)
3759 if !ok {
3760 return address(""), 0, address(""), 0, 0, 0, 0, false
3761 }
3762 fee := feeFor(l.priceUgnot)
3763 return l.seller, l.priceUgnot, l.buyer, l.listedAt, l.expiresAt, fee, l.priceUgnot + fee, true
3764}
3765
3766func TotalListings() int64 {
3767 return int64(listings.Size())
3768}
3769
3770func ListingsPage(offset, limit int) []string {
3771 if offset < 0 {
3772 panic("garden: offset must not be negative")
3773 }
3774 if limit <= 0 {
3775 panic("garden: limit must be positive")
3776 }
3777 if limit > maxListingsPageSize {
3778 limit = maxListingsPageSize
3779 }
3780 tokenIDs := make([]string, 0, limit)
3781 listings.IterateByOffset(offset, limit, func(key string, value any) bool {
3782 tokenIDs = append(tokenIDs, key)
3783 return false
3784 })
3785 return tokenIDs
3786}
3787
3788func QuoteFee(priceUgnot int64) (feeUgnot, totalUgnot int64) {
3789 f := feeFor(priceUgnot)
3790 return f, priceUgnot + f
3791}
3792
3793func FeeRules() string {
3794 return "saleFeeRateBps=" + strconv.FormatInt(fee.rateBps, 10) +
3795 ";saleFeeFloorUgnot=" + strconv.FormatInt(fee.floorUgnot, 10) +
3796 ";saleFeeRecipient=" + fee.recipient.String()
3797}
3798
3799func Provenance(tokenID string) (adoptedAtUnix, acquiredAtUnix, saleCount int64, found bool) {
3800 g, ok := gnomeByTokenID(tokenID)
3801 if !ok {
3802 return 0, 0, 0, false
3803 }
3804 return g.adoptedAt, g.acquiredAt, g.saleCount, true
3805}
3806
3807// ---- v6's new readers. FIVE additions, every one of them read-only. The existing readers'
3808// SIGNATURES ARE ALL UNCHANGED FROM v5 ON PURPOSE, including Provenance's and GnomeByToken's,
3809// even though the round and the name would both have fitted naturally into them: the app already
3810// reads v5's shapes, and widening a tuple return breaks every existing caller for no gain that a
3811// dedicated reader does not already give. Reads are free and unmetered, so "one more query" costs
3812// a caller nothing; a changed signature costs them a rewrite.
3813
3814// Name returns tokenID's name, or found=false if there is no such token.
3815//
3816// A name of "" is a REAL, EXPECTED ANSWER, not an error: it means this gnome has not been named
3817// yet, which is the state every ordinary gnome mints in. found reports whether the TOKEN exists,
3818// never whether it has a name -- check the string for that.
3819func Name(tokenID string) (name string, found bool) {
3820 g, ok := gnomeByTokenID(tokenID)
3821 if !ok {
3822 return "", false
3823 }
3824 return g.name, true
3825}
3826
3827// NameOwner resolves a name to the tokenID currently carrying it, matching the same way SetName
3828// refuses duplicates: on the canonical form, so any capitalisation or spacing of a taken name
3829// finds it. This is the uniqueness index read back, and the honest answer to "who is Fennick".
3830func NameOwner(name string) (tokenID string, found bool) {
3831 return nameOwnerOf(canonicalNameKey(name))
3832}
3833
3834// NameAvailability answers "would SetName accept this name, and if not why" WITHOUT a transaction
3835// -- a free query against the same assertNameAssignable the write path uses, so the realm itself
3836// is the authority the app checks against instead of a second copy of the rules that can drift
3837// from it. reason is "" when ok is true, and the realm's own refusal message otherwise.
3838//
3839// The tokenID argument matters for exactly one case: a name is available TO THE TOKEN THAT
3840// ALREADY HOLDS IT (re-capitalising your own gnome's name). Pass "" to ask the general question
3841// "is this name free for somebody new", which is what an app naming a fresh gnome wants.
3842//
3843// THIS IS A CONVENIENCE, NOT THE AUTHORITY, and the distinction is load-bearing: a true answer
3844// here can be made false by somebody else's transaction landing first. SetName re-checks
3845// everything itself, always -- this function cannot be used to skip a check, only to avoid
3846// wasting a player's gas on a refusal that was already knowable.
3847//
3848// NOTE ON SHAPE, because the obvious implementation is a trap on this toolchain: this does NOT
3849// call SetName's validator and recover() from its panic. `recover()` does not reliably catch a
3850// panic across a realm boundary in Gno (MEMORY.md records this costing this project real time),
3851// and a reader should not have to reason about whether a same-realm call is the exception. So the
3852// rules live in nameAssignabilityProblem, which RETURNS its refusal instead of panicking it, and
3853// both this reader and assertNameAssignable call that one copy. No recover anywhere.
3854func NameAvailability(name string, tokenID string) (ok bool, reason string) {
3855 reason = nameAssignabilityProblem(name, tokenID)
3856 return reason == "", reason
3857}
3858
3859// SettledRound returns the drand round whose published signature produced tokenID's parts --
3860// v6 FIX 2, and the one query that makes this project's "provably unrigged" claim (ECONOMY.md
3861// section 1) checkable by a holder rather than merely asserted at them. Fetch
3862// https://api.drand.sh/<chain>/public/<round>, hash its signature, and re-derive the parts with
3863// this file's own seedFor/drawWeighted against your own address: a round signed by drand before
3864// anybody knew who would pin it cannot have been chosen to produce a particular gnome.
3865//
3866// round == 0 means this token was never produced by a roll, which is true of exactly one token
3867// ever: the genesis gnome. found reports whether the token exists.
3868func SettledRound(tokenID string) (round int64, found bool) {
3869 g, ok := gnomeByTokenID(tokenID)
3870 if !ok {
3871 return 0, false
3872 }
3873 return g.settledRound, true
3874}
3875
3876// MaxNameLen is maxNameLen read back, so the app can show the limit it will be held to instead of
3877// hardcoding a second copy of the number (app/js/config.js's NAME_MAX_LEN is the other copy, and
3878// the two are kept equal by hand and by TestMaxNameLenMatchesTheAppsRule).
3879func MaxNameLen() int64 { return int64(maxNameLen) }
3880
3881// GnomeByToken: the basic facts for one token -- hat, coat, eyes, nose, character (empty for an
3882// ordinary gnome), level, calling and the moment it was minted. SIGNATURE UNCHANGED FROM v5 --
3883// the name is Name(tokenID) and the round is SettledRound(tokenID); see the note above.
3884func GnomeByToken(tokenID string) (hat, coat string, eyes, nose int64, character string, level int64, gnomeType string, adoptedAtUnix int64, found bool) {
3885 g, ok := gnomeByTokenID(tokenID)
3886 if !ok {
3887 return "", "", 0, 0, "", 0, "", 0, false
3888 }
3889 return g.hat, g.coat, g.eyes, g.nose, g.character, g.level, g.gnomeType, g.adoptedAt, true
3890}
3891
3892// ActiveGnome returns the tokenID that is awake for addr right now, or found=false if nothing
3893// is (never claimed or bought a gnome, or sold the only one and has not Woken another since).
3894func ActiveGnome(addr address) (tokenID string, found bool) {
3895 tid, isStr := activeToken.Get(addr.String()).(string)
3896 if !isStr || tid == "" {
3897 return "", false
3898 }
3899 return tid, true
3900}
3901
3902// OwnerTokens returns every tokenID addr currently holds, in no particular order. Unbounded and
3903// unpaginated deliberately: batchCap is 60 (v7's own number -- v5 and v6 argued this at 24) and
3904// the genesis gnome is permanently unreachable to any wallet, so no address can ever hold more
3905// than batchCap tokens. Sixty token ids in one return is still a small, fixed,
3906// compile-time-bounded list well inside what a single read returns. THE NUMBER IN THIS COMMENT
3907// IS PART OF THE DELIVERABLE: a false comment on an immutable realm is permanent.
3908func OwnerTokens(addr address) []string {
3909 v := ownerTokens.Get(addr.String())
3910 list, ok := v.(*[]string)
3911 if !ok || list == nil {
3912 return []string{}
3913 }
3914 out := make([]string, len(*list))
3915 copy(out, *list)
3916 return out
3917}
3918
3919// TokensPage: the global token enumerator, hollowmere/v1's own pattern (TotalClaimants/
3920// ClaimantsPage) applied here, because v4's own missing-enumerator lesson (DECISIONS.md:780-791,
3921// arena/v1 shipping with no challenge inbox and no way to ever add one) is unavailable to fix
3922// after deploy and twenty lines now is cheaper than "unavailable forever" later.
3923func TokensPage(offset, limit int) []string {
3924 if offset < 0 {
3925 panic("garden: offset must not be negative")
3926 }
3927 if limit <= 0 {
3928 panic("garden: limit must be positive")
3929 }
3930 if limit > maxGnomesPageSize {
3931 limit = maxGnomesPageSize
3932 }
3933 ids := make([]string, 0, limit)
3934 gnomes.IterateByOffset(offset, limit, func(key string, value any) bool {
3935 ids = append(ids, key)
3936 return false
3937 })
3938 return ids
3939}
3940
3941func GardenState(tokenID string) (plantedAtCsv, readyAtCsv string, readyCount int64, found bool) {
3942 g, ok := gnomeByTokenID(tokenID)
3943 if !ok {
3944 return "", "", 0, false
3945 }
3946 now := time.Now().Unix()
3947 planted := make([]string, 0, numPlots)
3948 ready := make([]string, 0, numPlots)
3949 for i := 0; i < numPlots; i++ {
3950 p := g.plots[i]
3951 planted = append(planted, strconv.FormatInt(p.plantedAt, 10))
3952 ready = append(ready, strconv.FormatInt(p.readyAt, 10))
3953 if p.plantedAt != 0 && now >= p.readyAt {
3954 readyCount++
3955 }
3956 }
3957 return strings.Join(planted, ","), strings.Join(ready, ","), readyCount, true
3958}
3959
3960func CareState(tokenID string) (belly, cheer, acorns, totalXp, lastPatAtUnix, lastHappyEventAtUnix, lastActionAtUnix int64, found bool) {
3961 g, ok := gnomeByTokenID(tokenID)
3962 if !ok {
3963 return 0, 0, 0, 0, 0, 0, 0, false
3964 }
3965 owner, hasOwner := ownerOfToken(tokenID)
3966 acornBalance := int64(0)
3967 if hasOwner {
3968 acornBalance = balanceOf(owner)
3969 }
3970 now := time.Now().Unix()
3971 return meterNow(g.belly, g.bellyAt, now, bellyDropEveryHours),
3972 meterNow(g.cheer, g.cheerAt, now, cheerDropEveryHours),
3973 acornBalance, g.totalXp, g.lastPatAt, g.lastHappyEventAt, g.lastActionAt, true
3974}
3975
3976func MeterState(tokenID string) (belly, bellyAtUnix, cheer, cheerAtUnix int64, found bool) {
3977 g, ok := gnomeByTokenID(tokenID)
3978 if !ok {
3979 return 0, 0, 0, 0, false
3980 }
3981 return g.belly, g.bellyAt, g.cheer, g.cheerAt, true
3982}
3983
3984func Progress(tokenID string) (xpGarden, xpTinker, harvestCount, lastTinkerAtUnix, caredDayCount, dailyCareXpSpent, dailyCareDay int64, found bool) {
3985 g, ok := gnomeByTokenID(tokenID)
3986 if !ok {
3987 return 0, 0, 0, 0, 0, 0, 0, false
3988 }
3989 return g.xpGarden, g.xpTinker, g.harvestCount, g.lastTinkerAt,
3990 g.caredDayCount, g.dailyCareXp, g.dailyCareDay, true
3991}
3992
3993func CaredDays(tokenID string) (daysCsv string, found bool) {
3994 g, ok := gnomeByTokenID(tokenID)
3995 if !ok {
3996 return "", false
3997 }
3998 return caredDaysCsv(g), true
3999}
4000
4001func Mood(tokenID string) (mood string, found bool) {
4002 g, ok := gnomeByTokenID(tokenID)
4003 if !ok {
4004 return "", false
4005 }
4006 now := time.Now().Unix()
4007 belly := meterNow(g.belly, g.bellyAt, now, bellyDropEveryHours)
4008 return moodNow(belly, g.lastPatAt, g.lastHappyEventAt, g.adoptedAt, now, hungryAtOrBelow), true
4009}
4010
4011// IsLocked reports whether tokenID is currently refused by List's lockCaredDays check -- true
4012// only for a named one-of-one with fewer than lockCaredDays cared days; false for every ordinary
4013// gnome, always, and false for a named one-of-one once it has enough cared days.
4014func IsLocked(tokenID string) (locked bool, found bool) {
4015 g, ok := gnomeByTokenID(tokenID)
4016 if !ok {
4017 return false, false
4018 }
4019 return g.character != "" && g.caredDayCount < lockCaredDays, true
4020}
4021
4022func CollectionInfo() (name, symbol string) {
4023 return collectionName, collectionSymbol
4024}
4025
4026func TotalSupply() int64 {
4027 return token.TotalSupply()
4028}
4029
4030func BalanceOf(addr address) int64 {
4031 n, err := token.BalanceOf(addr)
4032 if err != nil {
4033 return 0
4034 }
4035 return n
4036}
4037
4038func OwnerOfToken(tokenID string) (owner address, found bool) {
4039 return ownerOfToken(tokenID)
4040}
4041
4042func TotalGnomes() int64 {
4043 return token.TotalSupply()
4044}
4045
4046func Acorns(addr address) (acorns int64, found bool) {
4047 if !balances.Has(addr.String()) {
4048 return 0, false
4049 }
4050 return balanceOf(addr), true
4051}
4052
4053// ---- counted rarity readers (MINT.md section 13): "nine gnomes in the world have this nose"
4054// has to be a chain fact. These four, plus CharacterOwner below, are that fact.
4055
4056func EyesCount(id int64) int64 { return eyesCount(id) }
4057func NoseCount(id int64) int64 { return noseCount(id) }
4058func HatCount(hat string) int64 { return hatCount(hat) }
4059func CoatCount(coat string) int64 { return coatCount(coat) }
4060
4061// CharacterOwner resolves one of the eight named one-of-ones, or the genesis gnome under its
4062// own name "gaffer", to its tokenID, once minted. (Second-audit-pass nitpick: an earlier
4063// version of this comment said only "one of the three named one-of-ones", which stopped being
4064// true the moment init() registered "gaffer" here too -- see init()'s own comment for why that
4065// registration is safe and never touches the editions' cap. The count in it then went stale a
4066// second way when v7 change 10 took the roster to eight; both are fixed here.)
4067func CharacterOwner(name string) (tokenID string, found bool) {
4068 v := characterOwner.Get(name)
4069 s, ok := v.(string)
4070 if !ok {
4071 return "", false
4072 }
4073 return s, true
4074}
4075
4076// ---- the mint's own state, read back.
4077
4078func IsAllowlisted(addr address) bool {
4079 return allowlist.Has(addr.String())
4080}
4081
4082func HasRolled(addr address) bool {
4083 return hasRolled.Has(addr.String())
4084}
4085
4086func PendingClaim(addr address) (round, claimedAtUnix int64, found bool) {
4087 v := pendingClaims.Get(addr.String())
4088 pc, ok := v.(*pendingClaim)
4089 if !ok || pc == nil {
4090 return 0, 0, false
4091 }
4092 return pc.round, pc.claimedAt, true
4093}
4094
4095// BatchStatus: mintedCount is reservations made by Claim (gates batchCap); settledCount is
4096// completions made by Settle (always <= mintedCount, the gap being pending, unsettled claims);
4097// namedMinted is how many of the eight named one-of-ones have actually been minted so far --
4098// read live off characterOwner rather than a separate counter, so there is only ever one source
4099// of truth for "has this edition been taken".
4100func BatchStatus() (mintedSoFar, settledSoFar, batchCapOut, namedMinted, namedCapOut int64) {
4101 // NAMED RETURNS DELIBERATELY DO NOT SHARE A NAME WITH THE PACKAGE-LEVEL mintedCount/
4102 // settledCount VARS. An earlier version of this function used mintedCount/settledCount as
4103 // its own named returns, which SHADOWED the package vars for the whole function body, so
4104 // `return mintedCount, settledCount, ...` returned the zero-valued local named returns
4105 // instead of the real package state -- this function always reported a fresh batch,
4106 // silently, with no error, which TestBatchClosesAtCapAndRefusesFurtherClaims caught by
4107 // actually driving claims until the realm said the batch was full and finding it never did.
4108 // Found and fixed before audit; recorded here rather than silently corrected, the same
4109 // discipline this file's own package doc uses for the Settle timing flaw.
4110 nm := int64(0)
4111 for _, name := range namedEditionOrder {
4112 if characterOwner.Has(name) {
4113 nm++
4114 }
4115 }
4116 return mintedCount, settledCount, batchCap, nm, numNamedEditions
4117}
4118
4119// ============================================================================
4120// v7: THE TRAIT READERS, AND THE SHAPE RULE THEY ALL OBEY.
4121// ============================================================================
4122//
4123// GAMES.md:38 is unambiguous -- "ship enumerators on day one; they cannot be added later
4124// (arena/v1 lacks them, forever)" -- so this list is complete and this is the only chance to get
4125// it right. Granular, one axis-group per call, matching GnomeByToken's style: explicit typed
4126// returns, no parsing, and `found` reporting whether the TOKEN exists and never whether a field
4127// is interesting. No v6 tuple is widened: "the app already reads v5's shapes, and widening a
4128// tuple return breaks every existing caller for no gain that a dedicated reader does not already
4129// give. Reads are free and unmetered, so 'one more query' costs a caller nothing; a changed
4130// signature costs them a rewrite."
4131//
4132// THE SHAPE RULE, STATED ONCE SO IT CAN BE CHECKED. Enumerate-and-read is permitted; QUERY IS
4133// NOT. Every reader in v7 is one of exactly four shapes:
4134//
4135// 1. X(tokenID) -- the stored facts about one token the caller named
4136// 2. TotalX() / XPage(offset, limit) -- a count and a window over a collection, in its own order
4137// 3. X(someKey) -- one index lookup on a key that is NOT a trait: an
4138// address, a name, a batch index, a part id
4139// 4. X() -- a realm constant
4140//
4141// THERE IS NO FIFTH SHAPE. In particular: no reader that takes a trait, a stat, a secret, a kin
4142// type or a predicate and returns the gnomes matching it, and NO READER THAT TAKES TWO TOKEN
4143// IDS. That is what makes TraitCensus, rarityScore, LineCount, IsKin, KinOf, "find me every
4144// gnome with fear 3" and every kin-bonus/related-pair/shared-quest feature STRUCTURALLY ABSENT
4145// rather than merely omitted -- they all need a shape this realm does not have.
4146//
4147// DELIBERATELY ABSENT, and this is the list to check a future version against: CareThreshold,
4148// CareScore, Band, PointsToThreshold and anything returning a fraction or remainder of any of
4149// them (TRAITS.md section 5.1's whole design is that there is nothing to put a countdown on);
4150// SecretCount, SecretCensus, HoldersOfSecret, IsFirstSeventeen and anything exposing
4151// secretsDrawnMask (an owner who can see which secrets are taken deduces others' by
4152// elimination); Label and Archetype (TRAITS.md section 2.3: the label is derived off chain,
4153// never stored, and a realm function returning it would make the two label-word columns
4154// unrewritable forever); TraitsMatching and every filter or predicate; any unique-in-batch or
4155// scarcest-in-batch marker (TRAITS.md section 2.3 recommends against building it at all at
4156// n = 60, where it fires about once per four hundred cards, and notes the honest alternative is
4157// "a rarity count wearing a hat"); KinOf, IsKin, LineOf, LineCount, KinCount and anything
4158// returning a degree; and any name, label or definition for any trait id.
4159
4160// Traits returns the five rolled axis ids, in AXIS order (0 temperament, 1 obsession, 2 bond,
4161// 3 fear, 4 boast) and NOT in dominance order. All five are 0 for the genesis gnome, which is
4162// the one token never produced by a roll.
4163func Traits(tokenID string) (temperament, obsession, bond, fear, boast int64, found bool) {
4164 g, ok := gnomeByTokenID(tokenID)
4165 if !ok {
4166 return 0, 0, 0, 0, 0, false
4167 }
4168 return g.temperament, g.obsession, g.bond, g.fear, g.boast, true
4169}
4170
4171// Dominance returns the five axis indexes 0..4 in DOMINANCE order: `first` is the loudest.
4172// Where the loud/latent boundary falls is an OFF-CHAIN reading of this number and is not stored
4173// (TRAITS.md sections 2.1 and 2.2) -- which is exactly what keeps two-states-versus-three
4174// reversible after deploy, in either direction, for free.
4175func Dominance(tokenID string) (first, second, third, fourth, fifth int64, found bool) {
4176 g, ok := gnomeByTokenID(tokenID)
4177 if !ok {
4178 return 0, 0, 0, 0, 0, false
4179 }
4180 return g.dominance[0], g.dominance[1], g.dominance[2], g.dominance[3], g.dominance[4], true
4181}
4182
4183// Stats returns the four Top Trumps stats. They sum to 22, one is >= 8 and one is <= 3.
4184// NOTHING IN ANY REALM COMPARES TWO GNOMES' STATS: there is no Top Trumps game on chain, no
4185// reader here takes two token ids, and TRAITS.md section 4.5's tie rule is social.
4186func Stats(tokenID string) (pluck, nous, grumble, appetite int64, found bool) {
4187 g, ok := gnomeByTokenID(tokenID)
4188 if !ok {
4189 return 0, 0, 0, 0, false
4190 }
4191 return g.pluck, g.nous, g.grumble, g.appetite, true
4192}
4193
4194// Secret returns the gnome's secret id, 0..16. THE NAME AND THE LINE LIVE OFF CHAIN
4195// (TRAITS.md section 3.2); this realm stores and returns the id and nothing else. There is no
4196// count of who holds what and no way to ask which secrets are still unheld.
4197func Secret(tokenID string) (secretID int64, found bool) {
4198 g, ok := gnomeByTokenID(tokenID)
4199 if !ok {
4200 return 0, false
4201 }
4202 return g.secret, true
4203}
4204
4205// BornUnder returns the card-only sign id, 0..7. It is not an axis, has no slot in the dominance
4206// order, and is never handed to the dialogue layer (TRAITS.md section 1.6) -- the realm
4207// guarantees the middle clause structurally and the last one is an off-chain discipline.
4208func BornUnder(tokenID string) (sign int64, found bool) {
4209 g, ok := gnomeByTokenID(tokenID)
4210 if !ok {
4211 return 0, false
4212 }
4213 return g.bornUnder, true
4214}
4215
4216// BatchIndex returns this gnome's 1..60 position in Batch One, which is the key LINEAGE.md
4217// section 2.4's kinship table is written against. 0 for the genesis gnome, which is outside the
4218// batch and is the only token for which this is ever 0.
4219func BatchIndex(tokenID string) (index int64, found bool) {
4220 g, ok := gnomeByTokenID(tokenID)
4221 if !ok {
4222 return 0, false
4223 }
4224 return g.batchIndex, true
4225}
4226
4227// TokenByBatchIndex is BatchIndex inverted: the token carrying a batch position, or found=false
4228// if that position has not been settled yet. Both directions ship because a client rendering a
4229// holder's cousins has batch indexes from the kinship table and needs token ids, and a client
4230// rendering one gnome's card has a token id and needs its index.
4231func TokenByBatchIndex(index int64) (tokenID string, found bool) {
4232 tid, ok := batchIndexToToken.Get(strconv.FormatInt(index, 10)).(string)
4233 if !ok || tid == "" {
4234 return "", false
4235 }
4236 return tid, true
4237}
4238
4239// TraitsCsv returns all seventeen stored trait numbers in one query, in a fixed, test-pinned
4240// order, for a client rendering a card -- the same CSV shape GardenState and CaredDays already
4241// use. It exists because TRAITS.md section 6.2's card renders these fields at once and six round
4242// trips to draw one card is silly when one would do.
4243//
4244// It is a CONVENIENCE OVER THE SAME STORED VALUES, not a second source of truth, and
4245// TestTraitsCsvAgreesWithEveryGranularReader pins that -- the same discipline NameAvailability
4246// already follows against SetName.
4247//
4248// THE CARE THRESHOLD IS NOT IN IT and has no reader anywhere. Neither is anything derived from
4249// it, and neither is a name, a label or an archetype.
4250//
4251// temperament,obsession,bond,fear,boast,
4252// dom1,dom2,dom3,dom4,dom5,
4253// secret,pluck,nous,grumble,appetite,bornUnder,batchIndex
4254func TraitsCsv(tokenID string) (csv string, found bool) {
4255 g, ok := gnomeByTokenID(tokenID)
4256 if !ok {
4257 return "", false
4258 }
4259 fields := []int64{
4260 g.temperament, g.obsession, g.bond, g.fear, g.boast,
4261 g.dominance[0], g.dominance[1], g.dominance[2], g.dominance[3], g.dominance[4],
4262 g.secret, g.pluck, g.nous, g.grumble, g.appetite, g.bornUnder, g.batchIndex,
4263 }
4264 parts := make([]string, 0, len(fields))
4265 for _, v := range fields {
4266 parts = append(parts, strconv.FormatInt(v, 10))
4267 }
4268 return strings.Join(parts, ","), true
4269}
4270
4271// ============================================================================
4272// v7: THE KINSHIP ENUMERATORS, AND THEY ARE THE ONLY WAY IN.
4273// ============================================================================
4274//
4275// LINEAGE.md section 6.2 argues that the day-one enumerator rule is a TRAP here: "Store the web
4276// and you must ship KinOf(tokenID) on day one; ship KinOf and it exists forever; and the first
4277// third-party UI to find it will build LineCount() on top of it within a week." The OVERRULED
4278// box moved the position on storing but left section 6.5's prohibitions standing, so the design
4279// has to satisfy both -- and it does, by a distinction that is not a dodge:
4280//
4281// - KinOf(tokenID) is an ANSWER ABOUT A GNOME. KinEdgesPage(offset, limit) is THE TABLE. The
4282// first is the shape a contract calls. The second is the shape a RENDERER calls: it takes no
4283// token, no address and no pair, and it cannot be given one. A client that wants a holder's
4284// cousins reads 84 rows once, caches them, and filters locally -- precisely what it already
4285// does with the published Markdown today, except now the rows are owned, dated by a block
4286// height, and readable by any realm that wants the DATA rather than an answer.
4287// - NOTHING IS FORECLOSED THAT WAS NOT ALREADY OPEN. A third party can of course build KinOf
4288// on top of 84 rows; they could build it on top of the published document too. Section 6.2's
4289// concern was never that the data is derivable, it was that A REALM FUNCTION would exist for
4290// a contract to call and a UI to find without looking.
4291// - THE LOAD-BEARING LINE STILL HOLDS EXACTLY: "no entrypoint anywhere in any realm that
4292// behaves differently for two related gnomes than for two strangers." v7 has none, cannot
4293// grow one, and these two functions are not one -- they do not take a gnome.
4294
4295// TotalKinEdges returns 84. The table is fixed at deploy and this number never changes.
4296func TotalKinEdges() int64 { return numKinEdges }
4297
4298// KinEdgesPage returns rows "from,to,typeId" -- batch indexes, not token ids; type ids, not
4299// words -- in the table's own order (LINEAGE.md section 2.4's own row order), offset/limit
4300// paginated exactly as TokensPage and ListingsPage already are, with the same refusals.
4301func KinEdgesPage(offset, limit int) []string {
4302 if offset < 0 {
4303 panic("garden: offset must not be negative")
4304 }
4305 if limit <= 0 {
4306 panic("garden: limit must be positive")
4307 }
4308 if limit > maxKinEdgesPageSize {
4309 limit = maxKinEdgesPageSize
4310 }
4311 out := make([]string, 0, limit)
4312 for i := offset; i < len(kinEdges) && len(out) < limit; i++ {
4313 e := kinEdges[i]
4314 out = append(out, strconv.FormatInt(e.from, 10)+","+
4315 strconv.FormatInt(e.to, 10)+","+
4316 strconv.FormatInt(e.typ, 10))
4317 }
4318 return out
4319}
4320
4321// TotalKinTypes returns 9, LINEAGE.md section 2.4's closed vocabulary.
4322func TotalKinTypes() int64 { return numKinTypes }
4323
4324// KinTypeName maps a type id to its one word. The declared exception to "wording off chain" --
4325// see kinTypeNames' own comment for the three-part argument and for how to remove it in one
4326// edit if the owner prefers the strictest reading.
4327func KinTypeName(typeID int64) (name string, found bool) {
4328 if typeID < 0 || typeID >= numKinTypes {
4329 return "", false
4330 }
4331 return kinTypeNames[typeID], true
4332}
4333
4334func GenesisTokenID() string {
4335 return genesisTokenID
4336}
4337
4338func AdminState() (admin address, frozen bool) {
4339 return address(adminAddress), rulesFrozen
4340}
4341
4342func LastWakeDay(addr address) (day int64, found bool) {
4343 v, ok := lastWakeDay.Get(addr.String()).(int64)
4344 return v, ok
4345}
4346
4347// ============================================================================
4348// Render
4349// ============================================================================
4350
4351// Render DELIBERATELY ECHOES NO PLAYER-SUPPLIED STRING, and v6 keeps it that way even though it
4352// now has gnome names it could print. THIS IS THE DEFENCE AGAINST ABUSIVE NAMES ON A PUBLIC PAGE,
4353// and it is structural rather than a filter: gnoweb renders this output as markdown, so a page
4354// that listed names would be a markdown sink fed by arbitrary player input, and the project would
4355// then own a moderation problem on an immutable realm with no moderation surface. v5's own
4356// adversarial review (ATTACK-REVIEW.md, "Render sanitisation") recorded "no injection surface" as
4357// a property of this function; adding a name here would have been the one change in this file
4358// that falsified an audited claim.
4359//
4360// Belt and braces, because "nothing echoes names today" is only one edit away from being false:
4361// the name charset allowlist (letters, digits, space, hyphen, apostrophe -- see validNameRune)
4362// excludes every markdown metacharacter there is. No '[', ']', '(', ')', '<', '>', '*', '_',
4363// backtick, pipe or newline can appear in a name, so even a future Render that DID print one
4364// could not be made to inject link syntax, a raw HTML tag or a table break. An abusive name is
4365// therefore limited to being rude in words, visible only to someone who queries that specific
4366// token, never on a page this project publishes.
4367// v7 EXTENDS Render WITH ONE PATH -- a token id -- AND THREE CONSTRAINTS, all of which are the
4368// forbidden list restated for the one function that produces human-facing output:
4369//
4370// - NO THRESHOLD, NO SCORE, NO BAND, NO FRACTION, NO REMAINDER. TRAITS.md section 6.2: "Never
4371// on the card, in any version: ... the care score, the care threshold, or anything derived
4372// from either."
4373// - NO CENSUS, NO DISTRIBUTION, NO COUNT OF ANYTHING TRAIT-SHAPED, and specifically no
4374// unique-in-batch or scarcest-in-batch marker.
4375// - IDS, NOT NAMES. This function prints the numbers it stores. It does not get a copy of the
4376// thirty-two public names, the seventeen secret names or the eight signs, because that would
4377// put revisable wording on an immutable realm -- the whole point of the split. Rendering
4378// "Thistle" instead of 7 is the app's job, from TRAITS.md's own tables.
4379//
4380// AND IT STILL ECHOES NO PLAYER-SUPPLIED STRING: an unknown path returns a fixed message with
4381// the path omitted, exactly as v6's did, so the markdown sink stays unreachable from player
4382// input even though this function now takes a path that resolves.
4383func Render(path string) string {
4384 if path != "" {
4385 // A token id, or nothing. The path is never echoed back -- see the comment above.
4386 if csv, ok := TraitsCsv(path); ok {
4387 g, _ := gnomeByTokenID(path)
4388 out := "# Gnotchi gnome\n\n"
4389 out += "Batch One position: " + strconv.FormatInt(g.batchIndex, 10) + " of " + strconv.FormatInt(batchCap, 10) + "\n\n"
4390 out += "Settled at drand round: " + strconv.FormatInt(g.settledRound, 10) + "\n\n"
4391 out += "Axes (temperament, obsession, bond, fear, boast), as ids: " +
4392 strconv.FormatInt(g.temperament, 10) + ", " + strconv.FormatInt(g.obsession, 10) + ", " +
4393 strconv.FormatInt(g.bond, 10) + ", " + strconv.FormatInt(g.fear, 10) + ", " +
4394 strconv.FormatInt(g.boast, 10) + "\n\n"
4395 out += "Dominance order (axis indexes, loudest first): " +
4396 strconv.FormatInt(g.dominance[0], 10) + ", " + strconv.FormatInt(g.dominance[1], 10) + ", " +
4397 strconv.FormatInt(g.dominance[2], 10) + ", " + strconv.FormatInt(g.dominance[3], 10) + ", " +
4398 strconv.FormatInt(g.dominance[4], 10) + "\n\n"
4399 out += "Stats (pluck, nous, grumble, appetite): " +
4400 strconv.FormatInt(g.pluck, 10) + ", " + strconv.FormatInt(g.nous, 10) + ", " +
4401 strconv.FormatInt(g.grumble, 10) + ", " + strconv.FormatInt(g.appetite, 10) + "\n\n"
4402 out += "Secret id: " + strconv.FormatInt(g.secret, 10) + "\n\n"
4403 out += "Born under (sign id): " + strconv.FormatInt(g.bornUnder, 10) + "\n\n"
4404 out += "All of the above as one CSV: " + csv + "\n\n"
4405 out += "The names for these ids live in TRAITS.md, off chain and still revisable. " +
4406 "This realm stores and prints the numbers.\n"
4407 return out
4408 }
4409 return "garden/v9: unknown path"
4410 }
4411 minted, settled, cap_, namedMinted, namedCap := BatchStatus()
4412 out := "# Gnotchi gnomes -- garden/v9\n\n"
4413 out += "Batch One: " + strconv.FormatInt(minted, 10) + " claimed, " +
4414 strconv.FormatInt(settled, 10) + " settled, of " + strconv.FormatInt(cap_, 10) + " ever.\n\n"
4415 out += "Named one-of-ones minted: " + strconv.FormatInt(namedMinted, 10) + " of " + strconv.FormatInt(namedCap, 10) + ".\n\n"
4416 out += "Total supply (including the genesis gnome): " + strconv.FormatInt(TotalSupply(), 10) + "\n\n"
4417 out += "Free to claim, if your address is on the allowlist. One roll per wallet, ever. " +
4418 "Claim pins a future drand round; Settle, callable by anyone once that round is " +
4419 "published, mints the result.\n"
4420 return out
4421}
4422
4423// ============================================================================
4424// Internal helpers
4425// ============================================================================
4426
4427func gnomeByTokenID(tokenID string) (*gnome, bool) {
4428 v := gnomes.Get(tokenID)
4429 g, ok := v.(*gnome)
4430 if !ok || g == nil {
4431 return nil, false
4432 }
4433 return g, true
4434}
4435
4436// activeGnomeForAddr resolves addr's AWAKE gnome, if any. This is the one and only lookup
4437// SaveDay and ChooseType use -- there is deliberately no "the gnome for this address" any more.
4438func activeGnomeForAddr(addr address) (tid string, g *gnome, found bool) {
4439 tidStr, ok := activeToken.Get(addr.String()).(string)
4440 if !ok || tidStr == "" {
4441 return "", nil, false
4442 }
4443 gg, ok := gnomeByTokenID(tidStr)
4444 if !ok {
4445 return "", nil, false
4446 }
4447 return tidStr, gg, true
4448}
4449
4450func ownerOfToken(tokenID string) (owner address, found bool) {
4451 if !gnomes.Has(tokenID) {
4452 return address(""), false
4453 }
4454 o, err := token.OwnerOf(grc721.TokenID(tokenID))
4455 if err != nil {
4456 return address(""), false
4457 }
4458 return o, true
4459}
4460
4461// addOwnerToken / removeOwnerToken: the hold-many index. ownerTokens stores a *[]string per
4462// owner so repeated appends mutate one allocation rather than replacing the tree entry every
4463// time; removal does a linear scan, which is fine at this realm's permanent, tiny upper bound
4464// (batchCap = 60 tokens that will ever exist, minus the one permanently unreachable genesis
4465// gnome, so no owner's slice is ever longer than 60 entries for the whole life of this realm --
4466// v5 and v6 argued the identical point at 24; only the number moved).
4467func addOwnerToken(addr address, tokenID string) {
4468 v := ownerTokens.Get(addr.String())
4469 list, ok := v.(*[]string)
4470 if !ok || list == nil {
4471 fresh := []string{tokenID}
4472 ownerTokens.Set(addr.String(), &fresh)
4473 return
4474 }
4475 *list = append(*list, tokenID)
4476}
4477
4478func removeOwnerToken(addr address, tokenID string) {
4479 v := ownerTokens.Get(addr.String())
4480 list, ok := v.(*[]string)
4481 if !ok || list == nil {
4482 return
4483 }
4484 out := make([]string, 0, len(*list))
4485 for _, id := range *list {
4486 if id != tokenID {
4487 out = append(out, id)
4488 }
4489 }
4490 *list = out
4491}
4492
4493func balanceOf(addr address) int64 {
4494 v := balances.Get(addr.String())
4495 n, ok := v.(int64)
4496 if !ok {
4497 return 0
4498 }
4499 return n
4500}
4501
4502func setBalance(addr address, v int64) {
4503 if v < 0 {
4504 panic("garden: acorn balance cannot go negative")
4505 }
4506 balances.Set(addr.String(), v)
4507}
4508
4509func grantStartingPurseIfNew(addr address) {
4510 if !balances.Has(addr.String()) {
4511 balances.Set(addr.String(), acornsStart)
4512 }
4513}
4514
4515func feeFor(priceUgnot int64) int64 {
4516 return fee.floorUgnot + priceUgnot*fee.rateBps/10000
4517}
4518
4519func exactUgnot(send chain.Coins, want int64) bool {
4520 if len(send) != 1 {
4521 return false
4522 }
4523 return send[0].Denom == ugnotDenom && send[0].Amount == want
4524}
4525
4526func coinsString(c chain.Coins) string {
4527 if len(c) == 0 {
4528 return "nothing"
4529 }
4530 out := ""
4531 for i, coin := range c {
4532 if i > 0 {
4533 out += ","
4534 }
4535 out += strconv.FormatInt(coin.Amount, 10) + coin.Denom
4536 }
4537 return out
4538}
4539
4540func listingFor(tokenID string) (*listing, bool) {
4541 v := listings.Get(tokenID)
4542 l, ok := v.(*listing)
4543 if !ok || l == nil {
4544 return nil, false
4545 }
4546 if l.priceUgnot < 0 {
4547 return nil, false
4548 }
4549 return l, true
4550}
4551
4552// ============================================================================
4553// v6 FIX 1: name validation and canonicalisation. validNameLength/validNameCharset/
4554// hasVisibleNameContent/validNameRune/invisibleLetterRunes are v4's OWN IMPLEMENTATIONS, carried
4555// back byte-for-byte -- three audit rounds' worth of work (v4's own comment), including v2's
4556// audit fix F3 (\uXXXX escapes rather than literal invisible glyphs, so the denylist is readable
4557// in an editor and cannot be silently emptied by a formatter that strips default-ignorable
4558// characters). They are NOT re-derived here and must not be "tidied": every line of that
4559// allowlist was argued for once already.
4560//
4561// THE DENYLIST IS THE PART THAT MATTERS AND THE PART THAT LOOKS REDUNDANT. All four entries are
4562// genuinely LETTERS by Unicode's own classification, so unicode.IsLetter accepts every one of
4563// them -- and all four render as NOTHING AT ALL. They exist in fonts purely as filler. Without
4564// this list, "Hob" and "Hob<U+3164>" are different names that look identical in every UI either
4565// will ever be shown in, which is impersonation with no defence available to the person being
4566// impersonated. app/js/config.js's NAME_INVISIBLE_LETTERS is the same four code points, exposed
4567// there for the app's own tests; TestEveryInvisibleLetterIsRefused table-drives this map directly
4568// so neither side's coverage can silently fall behind the other (config.js's finding Y3: before
4569// that change every test on both sides of the stack only ever exercised U+3164).
4570//
4571// CARRIED FORWARD UNCHANGED, AND STILL AN OPEN PRODUCT QUESTION, NOT A SETTLED ONE: this
4572// allowlist excludes every COMBINING MARK (chiefly Unicode category Mn), which makes zalgo names
4573// structurally impossible -- and also makes NFD Latin, Devanagari, Thai, vocalised Hebrew and
4574// Arabic unspellable. realms/garden/NOTES.md records that trade-off in full as the owner's to
4575// revisit (v4's third audit round, finding Y7). v6 carries it forward rather than silently
4576// re-deciding it, exactly as v4 carried it forward from v1/v2. Precomposed letters (é as a single
4577// code point) are accepted fine; the exclusion is specifically and only about combining marks.
4578// ============================================================================
4579
4580func validNameLength(name string) bool {
4581 // RuneCountInString, never len(): len() counts BYTES, so a 24-character name in any
4582 // non-ASCII script would be refused for being "too long" while an ASCII one of the same
4583 // visible length passed.
4584 n := utf8.RuneCountInString(name)
4585 return n > 0 && n <= maxNameLen
4586}
4587
4588func validNameCharset(name string) bool {
4589 for _, r := range name {
4590 if !validNameRune(r) {
4591 return false
4592 }
4593 }
4594 return true
4595}
4596
4597func hasVisibleNameContent(name string) bool {
4598 for _, r := range name {
4599 if r != ' ' {
4600 return true
4601 }
4602 }
4603 return false
4604}
4605
4606// invisibleLetterRunes: the four code points that are Unicode LETTERS and render as nothing. See
4607// the section comment above for why a letter allowlist alone cannot catch these. Written as
4608// \uXXXX escapes (v2's audit fix F3) so they survive an editor or formatter that strips
4609// default-ignorable characters -- a denylist that can be silently emptied protects nothing.
4610//
4611// v3 through v5 carried this comment while the entries themselves were literal invisible glyphs,
4612// so the comment was false about its own code for three versions and the protection it describes
4613// was not actually in place. Restored here, with the escapes BACK IN THE SOURCE this time.
4614//
4615// The escapes alone are only half of F3. TestEveryInvisibleLetterIsRefused table-drives this map,
4616// which means it tests whatever the map happens to contain -- swap one entry for an ordinary
4617// letter and that test still passes while the realm ships banning the letter and accepting the
4618// invisible one. TestInvisibleLetterRunesIsExactlyFourPinnedCodepoints is the other half: it
4619// pins all four code points BY ESCAPE, independently of this map's contents, so that
4620// substitution fails loudly.
4621var invisibleLetterRunes = map[rune]bool{
4622 '\u115f': true, // HANGUL CHOSEONG FILLER
4623 '\u1160': true, // HANGUL JUNGSEONG FILLER
4624 '\u3164': true, // HANGUL FILLER
4625 '\uffa0': true, // HALFWIDTH HANGUL FILLER
4626}
4627
4628func validNameRune(r rune) bool {
4629 switch r {
4630 case ' ', '-', '\'':
4631 return true
4632 }
4633 if invisibleLetterRunes[r] {
4634 return false
4635 }
4636 return unicode.IsLetter(r) || unicode.IsDigit(r)
4637}
4638
4639// smartPunctuationRunes: the four code points iOS substitutes by default for a straight
4640// apostrophe or hyphen. NONE of them is accepted by validNameRune -- this map exists only so the
4641// refusal can SAY SO instead of reporting a generic charset failure to a player who typed an
4642// ordinary apostrophe on an ordinary phone. See nameAssignabilityProblem's own comment on why
4643// this realm refuses rather than folds them.
4644var smartPunctuationRunes = map[rune]bool{
4645 '‘': true, // LEFT SINGLE QUOTATION MARK
4646 '’': true, // RIGHT SINGLE QUOTATION MARK
4647 '–': true, // EN DASH
4648 '—': true, // EM DASH
4649}
4650
4651func hasSmartPunctuation(name string) bool {
4652 for _, r := range name {
4653 if smartPunctuationRunes[r] {
4654 return true
4655 }
4656 }
4657 return false
4658}
4659
4660// canonicalNameKey folds a name to the key nameOwner and reservedNameKeys compare on: lowercase
4661// ASCII, runs of spaces collapsed to one, leading and trailing spaces removed.
4662//
4663// THE STORED NAME AND THE KEY ARE DELIBERATELY TWO DIFFERENT STRINGS. The gnome keeps the name
4664// its owner typed, exactly as typed, because that is what a person chose and the app should show
4665// it back to them unaltered. The KEY exists only to answer "is this the same name as that one"
4666// for uniqueness and reservation, and for THAT question "Hob", "hob", "HOB" and "Hob" with two
4667// spaces in it are all plainly the same name. Keying on the raw string instead would make
4668// uniqueness worthless and would let anyone walk around the one-of-one reservation by changing
4669// one letter's case.
4670//
4671// WHAT IT DELIBERATELY DOES NOT DO, stated plainly rather than left for a reader to discover:
4672// it does not close cross-script HOMOGLYPHS. Cyrillic "А", Greek "Ο" and fullwidth "A" are all
4673// Unicode letters, all accepted, and all visually confusable with their Latin counterparts, so
4674// "Нob" with a Cyrillic Н is a different key from "Hob" and will be allowed. This is the SAME
4675// accepted residual app/js/config.js's NAME_CHARSET_RE documents in its own comment, carried
4676// forward knowingly: closing it properly needs a confusables table (thousands of mappings,
4677// versioned by Unicode, compiled into an immutable realm) and the honest cost/benefit at a
4678// 60-gnome collection does not justify it. The defence that IS in place for the case that
4679// actually matters -- the eight cap-one editions -- is that their names are reserved and
4680// unrenameable, so a homoglyph impostor can exist but can never hold the real key, and
4681// CharacterOwner (not a name lookup) is what resolves the genuine Hob.
4682//
4683// ASCII-only lowercasing, following lowerASCII's own v4 reasoning: strings.ToLower would apply
4684// locale-independent Unicode case folding whose behaviour is a toolchain-version detail on an
4685// immutable realm, and the keys that MUST match exactly (reservedNameKeys) are all ASCII.
4686func canonicalNameKey(name string) string {
4687 lowered := lowerASCII(name)
4688
4689 out := make([]byte, 0, len(lowered))
4690 prevSpace := true // true at the start, so leading spaces are dropped by the same test
4691 for i := 0; i < len(lowered); i++ {
4692 c := lowered[i]
4693 if c == ' ' {
4694 if prevSpace {
4695 continue
4696 }
4697 prevSpace = true
4698 out = append(out, c)
4699 continue
4700 }
4701 prevSpace = false
4702 out = append(out, c)
4703 }
4704 // One trailing space may survive the loop above (a name ending in a space emits it before the
4705 // loop ends); drop it.
4706 if len(out) > 0 && out[len(out)-1] == ' ' {
4707 out = out[:len(out)-1]
4708 }
4709 return string(out)
4710}
4711
4712func nameOwnerOf(key string) (tokenID string, found bool) {
4713 v := nameOwner.Get(key)
4714 s, ok := v.(string)
4715 if !ok || s == "" {
4716 return "", false
4717 }
4718 return s, true
4719}
4720
4721// lowerASCII: v4's own helper, unchanged -- ASCII-only on purpose (see v4's own comment on why
4722// not strings.ToLower).
4723func lowerASCII(s string) string {
4724 b := []byte(s)
4725 for i, c := range b {
4726 if c >= 'A' && c <= 'Z' {
4727 b[i] = c + ('a' - 'A')
4728 }
4729 }
4730 return string(b)
4731}
4732
4733func anyPlotPlanted(g *gnome) bool {
4734 for i := 0; i < numPlots; i++ {
4735 if g.plots[i].plantedAt != 0 {
4736 return true
4737 }
4738 }
4739 return false
4740}
4741
4742func eyesCount(id int64) int64 {
4743 v := eyesCounts.Get(strconv.FormatInt(id, 10))
4744 n, ok := v.(int64)
4745 if !ok {
4746 return 0
4747 }
4748 return n
4749}
4750
4751func noseCount(id int64) int64 {
4752 v := noseCounts.Get(strconv.FormatInt(id, 10))
4753 n, ok := v.(int64)
4754 if !ok {
4755 return 0
4756 }
4757 return n
4758}
4759
4760func hatCount(hat string) int64 {
4761 v := hatCounts.Get(hat)
4762 n, ok := v.(int64)
4763 if !ok {
4764 return 0
4765 }
4766 return n
4767}
4768
4769func coatCount(coat string) int64 {
4770 v := coatCounts.Get(coat)
4771 n, ok := v.(int64)
4772 if !ok {
4773 return 0
4774 }
4775 return n
4776}
4777
4778// ---- parsing, meters, mood, level: v4's own proven implementations, byte-for-byte, with
4779// rule() lookups replaced by this file's fixed constants (see the package doc: no SetRule here).
4780
4781func parseActions(raw string) []saveAction {
4782 if raw == "" {
4783 return nil
4784 }
4785
4786 count := 1
4787 for i := 0; i < len(raw); i++ {
4788 if raw[i] == ';' {
4789 count++
4790 }
4791 }
4792 if count > maxActionsPerCall {
4793 panic("garden: too many actions in one call; the limit is " + strconv.Itoa(maxActionsPerCall))
4794 }
4795
4796 parts := strings.Split(raw, ";")
4797 actions := make([]saveAction, 0, len(parts))
4798 for _, part := range parts {
4799 if part == "" {
4800 panic("garden: empty action entry")
4801 }
4802 fields := strings.Split(part, ":")
4803 switch fields[0] {
4804 case actionPat, actionFeed, actionTinker:
4805 if len(fields) != 2 {
4806 panic("garden: malformed action: " + quoteAction(part))
4807 }
4808 actions = append(actions, saveAction{kind: fields[0], at: parseUnixField(fields[1], part)})
4809 case actionPlant, actionHarvest:
4810 if len(fields) != 3 {
4811 panic("garden: malformed action: " + quoteAction(part))
4812 }
4813 actions = append(actions, saveAction{
4814 kind: fields[0],
4815 plot: parsePlotField(fields[1], part),
4816 at: parseUnixField(fields[2], part),
4817 })
4818 default:
4819 panic("garden: unknown action kind: " + quoteAction(part))
4820 }
4821 }
4822 return actions
4823}
4824
4825func quoteAction(part string) string {
4826 return "\"" + part + "\""
4827}
4828
4829func parseUnixField(s, part string) int64 {
4830 n, err := strconv.ParseInt(s, 10, 64)
4831 if err != nil || n < 0 {
4832 panic("garden: malformed timestamp in action: " + quoteAction(part))
4833 }
4834 return n
4835}
4836
4837func parsePlotField(s, part string) int {
4838 n, err := strconv.Atoi(s)
4839 if err != nil || n < 0 || n >= numPlots {
4840 panic("garden: malformed plot index in action: " + quoteAction(part))
4841 }
4842 return n
4843}
4844
4845func dayNumber(unixSeconds int64) int64 { return unixSeconds / daySeconds }
4846
4847// recordCaredDay reports, via banked, whether it actually incremented g.caredDayCount -- false
4848// on the no-op branch (day == g.lastCaredDay already, e.g. a second SaveDay call the same UTC
4849// day, or a token bought mid-day that the SELLER had already banked that exact day for before
4850// the sale). SaveDay's own caller-scoped gate (lastCaredDayForOwner) depends on this: it must
4851// only spend the owner's one-bank-per-day allowance on a call that actually banked something,
4852// never on a no-op -- a gno-auditor re-audit found that an unconditional advance let a buyer's
4853// SaveDay on a token the seller had already banked that day silently consume the buyer's own
4854// allowance for the rest of that day, denying an otherwise fully legitimate cared day on a
4855// DIFFERENT, genuinely active token later the same day. See the call site in SaveDay.
4856func recordCaredDay(g *gnome, day int64) (banked bool) {
4857 if g.lastCaredDay == 0 {
4858 g.lastCaredDay = day
4859 g.caredMask = 1
4860 g.caredDayCount = 1
4861 return true
4862 }
4863 if day == g.lastCaredDay {
4864 return false
4865 }
4866 shift := uint(day - g.lastCaredDay)
4867 if shift >= caredWindowDays {
4868 g.caredMask = 1
4869 } else {
4870 g.caredMask = (g.caredMask << shift) | 1
4871 }
4872 g.lastCaredDay = day
4873 g.caredDayCount++
4874 return true
4875}
4876
4877func caredDaysCsv(g *gnome) string {
4878 if g.lastCaredDay == 0 {
4879 return ""
4880 }
4881 days := make([]string, 0, caredWindowDays)
4882 for i := uint(0); i < caredWindowDays; i++ {
4883 if g.caredMask&(1<<i) != 0 {
4884 days = append(days, strconv.FormatInt(g.lastCaredDay-int64(i), 10))
4885 }
4886 }
4887 return strings.Join(days, ",")
4888}
4889
4890func meterNow(value, setAt, now, dropEveryHours int64) int64 {
4891 if dropEveryHours <= 0 {
4892 return value
4893 }
4894 elapsedHours := (now - setAt) / hourSeconds
4895 if elapsedHours <= 0 {
4896 return clampMeter(value)
4897 }
4898 drops := elapsedHours / dropEveryHours
4899 return clampMeter(value - drops)
4900}
4901
4902func clampMeter(v int64) int64 {
4903 if v < 0 {
4904 return 0
4905 }
4906 if v > meterMax {
4907 return meterMax
4908 }
4909 return v
4910}
4911
4912func canPat(lastPatAt, now, cooldownHours int64) bool {
4913 if lastPatAt == 0 {
4914 return true
4915 }
4916 return now-lastPatAt >= cooldownHours*hourSeconds
4917}
4918
4919func moodNow(belly, lastPatAt, lastHappyEventAt, adoptedAt, now, hungryThreshold int64) string {
4920 if belly <= hungryThreshold {
4921 return moodHungry
4922 }
4923 if lastHappyEventAt == 0 {
4924 if now-adoptedAt <= happyWindowHours*hourSeconds {
4925 return moodContent
4926 }
4927 return moodSulky
4928 }
4929 if now-lastHappyEventAt <= happyWindowHours*hourSeconds {
4930 return moodHappy
4931 }
4932 if now-lastHappyEventAt >= sulkyAfterHours*hourSeconds {
4933 return moodSulky
4934 }
4935 return moodContent
4936}
4937
4938func xpToNext(level int64) int64 { return levelBase + levelStep*level }
4939
4940func totalXpForLevel(level int64) int64 {
4941 if level <= 1 {
4942 return 0
4943 }
4944 total := int64(0)
4945 for l := int64(1); l < level; l++ {
4946 total += xpToNext(l)
4947 }
4948 return total
4949}
4950
4951func levelFromXp(totalXp int64) int64 {
4952 level := int64(1)
4953 for level < maxLevel {
4954 need := totalXpForLevel(level + 1)
4955 if totalXp < need {
4956 break
4957 }
4958 level++
4959 }
4960 return level
4961}