collectionName
untyped stringValue
"Gnotchi gnomes"
collectionSymbol
untyped stringValue
"GNOME"
maxNameLen
untyped bigintValue
(24 <untyped> bigint)
hourSeconds
int64// rules.config.json time.hourMs / 1000
Value
3600
patCooldownHours
int64// time.patCooldownHours
Value
12
happyWindowHours
int64// time.happyWindowHours
Value
2
sulkyAfterHours
int64// time.sulkyAfterHours
Value
72
meterMax
int64// meters.max
Value
10
bellyDropEveryHours
int64// meters.bellyDropEveryHours
Value
3
cheerDropEveryHours
int64// meters.cheerDropEveryHours
Value
6
feedBelly
int64// meters.feedBelly
Value
4
patCheer
int64// meters.patCheer
Value
3
hungryAtOrBelow
int64// meters.hungryAtOrBelow
Value
2
acornsStart
int64// acorns.start
Value
10
plantCost
int64// acorns.plantCost
Value
1
harvestPerPlot
int64// acorns.harvestPerPlot
Value
3
numPlots
untyped bigint// acorns.plots
Value
(3 <untyped> bigint)
growHours
int64// acorns.growHours
Value
24
feedCost
int64// acorns.feedCost
Value
1
tinkerCost
int64// acorns.tinkerCost
Value
5
xpPat
int64// xp.pat
Value
5
xpFeed
int64// xp.feed
Value
5
xpHarvestPerPlot
int64// xp.harvestPerPlot
Value
10
xpTinker
int64// xp.tinker
Value
15
dailyCareCap
int64// xp.dailyCareCap
Value
60
levelBase
int64// xp.levelBase
Value
50
levelStep
int64// xp.levelStep
Value
25
maxLevel
int64// xp.maxLevel
Value
20
typeAtLevel
int64// xp.typeAtLevel
Value
5
maxActionsPerCall
untyped bigintValue
(32 <untyped> bigint)
maxBackfillHours
int64Value
48
moodHungry
untyped stringValue
"hungry"
moodSulky
untyped stringValue
"sulky"
moodHappy
untyped stringValue
"happy"
moodContent
untyped stringValue
"content"
actionPat
untyped stringValue
"pat"
actionFeed
untyped stringValue
"feed"
actionPlant
untyped stringValue
"plant"
actionHarvest
untyped stringValue
"harvest"
actionTinker
untyped stringValue
"tinker"
validHats
map[string]boolvalidHats/validCoats: unchanged from v1.
- OID
- 038150…b42d:4
validHats details
validCoats
map[string]bool- OID
- 038150…b42d:6
validCoats details
plotState
typeplotState: v2. One of this gnome's numPlots garden plots. growing=false means empty (plantable); growing=true means occupied since plantedAt, harvestable once growHours have passed.
Value
v2.plotState
gnome
typegnome: v1's fields (name, hat, coat, level, gnomeType, adoptedAt) are unchanged in meaning and position; v2 adds the care-state fields SaveDay reads and writes. Per the brief ("Store meters as timestamps, not as decaying numbers") belly/cheer are stored as a last-known value plus the unix second it was set at, and meterNow below works out the current value from elapsed time whenever anything reads it -- nothing ticks on this realm's own clock, there is no scheduled job, and nothing here is updated by anything other than a SaveDay call the owning address itself signed.
Value
v2.gnome
token
*v0.Token- OID
- 038150…b42d:43
token details
ledger
*v0.PrivateLedger- OID
- 038150…b42d:45
ledger details
nextID
v0.IDValue
<zero>
gnomes
*v0.Tree- OID
- 038150…b42d:11
gnomes details
ownerIndex
*v0.Tree- OID
- 038150…b42d:14
ownerIndex details
init.49
func()- OID
- 038150…b42d:16
init.49 details
soulboundExtension
typesoulboundExtension: unchanged from v1. Rule 6 ("every transfer fails") made structural. See the package doc above and v1's own long comment (realms/garden/garden.gno) for the full writeup of why this relies on the transaction boundary rather than hook ordering, and why TransferFrom being a plain method (not a crossing call) is exactly why the package-level SECURITY note applies to it.
Value
v2.soulboundExtension
Adopt
func(name string, hat string, coat string)Adopt: unchanged from v1 in every validation it performs (same order, same messages, same OriginSend refusal, same caller derivation). v2 only adds initialising the new care-state fields at the bottom, where v1 only set level/gnomeType/adoptedAt.
- OID
- 038150…b42d:18
Adopt details
alreadyAdopted
func(addr .uverse.address) boolalreadyAdopted: unchanged from v1. See that file's comment for why both ownerIndex and token.BalanceOf are consulted, not either alone.
- OID
- 038150…b42d:19
alreadyAdopted details
validNameLength
func(name string) boolvalidNameLength: unchanged from v1.
- OID
- 038150…b42d:20
validNameLength details
validNameCharset
func(name string) boolvalidNameCharset: unchanged from v1.
- OID
- 038150…b42d:21
validNameCharset details
hasVisibleNameContent
func(name string) boolhasVisibleNameContent: unchanged from v1.
- OID
- 038150…b42d:22
hasVisibleNameContent details
invisibleLetterRunes
map[int32]boolinvisibleLetterRunes: unchanged from v1 in content -- same four codepoints -- but audit fix F3 restores v1's own \\uXXXX escape form instead of the literal invisible glyphs this file had drifted to. The content was still correct (verified by codepoint grep at audit time), but a map keyed on literal default-ignorable characters is by definition unreadable in an editor or a diff, and any formatter or copy/paste step that strips default-ignorable characters could silently empty this denylist with no failing test to catch it. \\uXXXX escapes are plain ASCII source, so that failure mode no longer exists, and TestInvisibleLetterRunesIsExactlyFourPinnedCodepoints below pins both the count and every individual codepoint so a truncated map fails loudly instead of silently. See v1's own comment for the exhaustive scan that proves this list complete against the Unicode version this toolchain ships today, and for what would make it worth re-running.
- OID
- 038150…b42d:24
invisibleLetterRunes details
validNameRune
func(r int32) boolvalidNameRune: unchanged from v1. See that file's comment for the full rationale (allowlist not denylist, what it guarantees, what it deliberately does not -- the combining-mark exclusion recorded as an open product question in NOTES.md, and the cross-script-homoglyph residual that is accepted, not closed).
- OID
- 038150…b42d:25
validNameRune details
gnomeForAddr
func(addr .uverse.address) (tid string, g *v2.gnome, found bool)gnomeForAddr: unchanged from v1.
- OID
- 038150…b42d:26
gnomeForAddr details
GnomeByAddress
func(addr .uverse.address) (tokenID string, name string, hat string, coat string, level int64, gnomeType string, adoptedAtUnix int64, found bool)GnomeByAddress: unchanged from v1 -- same signature, same fields, same meaning. v2's new care-state fields are deliberately NOT added to this tuple, so that nothing about what v1 already promised here changes; see CareState below for the new read.
- OID
- 038150…b42d:27
GnomeByAddress details
CareState
func(addr .uverse.address) (belly int64, cheer int64, acorns int64, totalXp int64, lastPatAtUnix int64, lastActionAtUnix int64, found bool)CareState is v2's new read: the SaveDay-relevant state GnomeByAddress deliberately doesn't carry. belly/cheer are returned already decayed to time.Now() (per the brief, "work out meter values from stored timestamps when they're read" applies to every reader, not just SaveDay's own internal bookkeeping) -- a caller never sees a stale stored value and has to re-derive the decay itself.
- OID
- 038150…b42d:28
CareState details
Render
func(path string) stringRender: unchanged from v1 except one new bullet (Acorns) -- see this function's own history in v1 for the full writeup of why path is bech32-validated before anything derived from it is ever echoed back into the page. v2 deliberately does not also render belly/cheer/garden-plot state here: CareState above is the read for that, and growing gnoweb's single markdown page to show every care-state field is a product decision for a later pass, not an engineering requirement this assignment's brief asks for.
- OID
- 038150…b42d:29
Render details
saveAction
typesaveAction is one parsed entry from a SaveDay call's action list.
Value
v2.saveAction
SaveDay
func(actionsRaw string)SaveDay is brief section 5.2's "one daily save": the day's actions -- pats, feeds, plants, harvests and a tinker task -- submitted as a single list in one signed call. ENCODING, and why it looks like this rather than like the brief's own "SaveDay(gnomeID, actions)": a public realm function reachable by \`maketx call\` or Adena's DoContract can only take arguments gno.land's own call-argument decoder knows how to build -- read directly from this toolchain's gno.land/pkg/sdk/vm/convert.go (convertArgToGno): primitive scalars (bool, string, every sized int/uint/float) and fixed-size-or-slice \[]byte (base64). There is no case there for a slice of structs, and no amount of Gno-side type cleverness changes what the transaction's wire format can carry. So "actions as a list" has to arrive as a single string this function parses itself, not as a \[]Action the chain's own tooling has no way to construct from a gnokey/Adena call. actionsRaw is semicolon-separated entries, each colon-separated: "pat:<unixSeconds>" "feed:<unixSeconds>" "tinker:<unixSeconds>" "plant:<plotIndex>:<unixSeconds>" "harvest:<plotIndex>:<unixSeconds>" e.g. "pat:1791600000;feed:1791601000;plant:0:1791602000". An empty string means an empty action list (a legal no-op call). See NOTES.md for this decision in full. Also absent from the brief's own signature: gnomeID. v1 and v2 both enforce one gnome per address with no transfer and no delegation of any kind -- there is no second gnome an address could ever mean by "gnomeID", and no path by which SaveDay could legitimately be asked to act on any gnome but the caller's own. Dropping the parameter removes a whole class of "whose gnome is this call actually touching" bug before it can exist, rather than adding a check that would only ever have one correct answer anyway. See NOTES.md. THREAT MODEL -- this assignment's own brief asks for this explicitly: actionsRaw is client-supplied and this realm is the only authority over it, full stop. Specifically defended below, each against a concrete hostile input: - Replay (resubmitting an already-applied batch, in whole or in part): every action's timestamp must be strictly greater than lastActionAt, the gnome's own high-water mark of the latest action any past SaveDay call has ever actually applied. A replayed batch's first action is, by definition, \<= that mark. - Duplicate actions within one call: the same strict-increase rule applies action to action within the list (actions\[i].at \< actions\[i+1].at required), so two actions sharing one timestamp -- the only way a client could claim two things happened at once -- is rejected regardless of which two kinds they are. - Future-dated actions: every timestamp must be \<= time.Now().Unix(), this call's own block time. Nothing claimed to have happened hasn't happened yet. - Past-dated actions, at or before adoption: subsumed by the replay check above -- lastActionAt starts at adoptedAt (see Adopt), so no action can ever be dated before adoption either. - Unlimited backfill after a long dormancy (audit fix F2, an owner decision -- see NOTES.md): the two checks above bound an action relative to lastActionAt (below) and to now (above), but say nothing about how far \*below\* now an action may be, so nothing stopped a player dormant for a year from backfilling a year of daily care in one sitting. maxBackfillHours (48) closes this with two parts, both needed: (1) any action older than the window is rejected outright, with a message a player can understand, and (2) the floor the ordering/replay check above enforces is itself clamped forward to at least now-maxBackfillHours before that loop runs, rather than left at the gnome's raw, possibly year-old lastActionAt -- so even if (1) were ever weakened or removed by a future edit, the floor the replay check already enforces would independently keep the backfillable span bounded. See the validation loop below for both parts and NOTES.md for the owner's reasoning on the 48-hour number. - Inflated action counts (e.g. fifty pats claimed in one day): canPat's 12-hour spacing makes more than two pats a day arithmetically impossible inside one call regardless of what the list claims, and feed/plant/tinker are each gated by this gnome's own acorn balance, which this exact set of rules is the only thing that can ever increase -- there is no action, in this call or any other exported function, that credits acorns without the realm itself having applied the matching cost somewhere upstream. The realm's own economy is the rate limit; no separate per-type daily counter is needed on top of it. - A thousand actions in one list: rejected by maxActionsPerCall before a single entry is even parsed -- parseActions counts the ";" delimiters first and panics on an oversized count before calling strings.Split at all, so an oversized actionsRaw never pays the cost of allocating a huge split result just to be rejected a moment later. - XP inflation via the daily cap: rawCareXp for the whole call is mood-adjusted, then clamped to whatever remains of dailyCareCap for the UTC calendar day this call's own actions fall on -- tracked by the gnome's own dailyCareXp/dailyCareDay accumulator, not by anything scoped to one call. Audit fix F1: earlier code capped only the current call's own XP at dailyCareCap and had no record of what a PREVIOUS call the same day had already spent, so a second same-day call got its own independent 60-XP allowance (two calls, 60 each, 120 against a cap of 60). See the cap/mood block below and the gnome struct's own comment on dailyCareXp/dailyCareDay. ATOMICITY: every check below runs against a local copy (work) of the caller's own gnome, taken by value at the top of this function. Nothing is written back to the gnomes tree until every action in the list has validated and applied cleanly against that copy; any panic before the final commit leaves the persisted gnome completely untouched, with no partial effect to undo. This is deliberately a plain Go/Gno value copy, not a recover()-and-rollback: see the package-level SECURITY note for why a recover() anywhere in this realm is never the right tool, including here.
- OID
- 038150…b42d:30
SaveDay details
parseActions
func(raw string) []v2.saveActionparseActions parses and validates the wire format SaveDay's own comment documents, and enforces maxActionsPerCall before doing any per-entry work -- a caller who sends a thousand ";"-separated entries is rejected on the delimiter COUNT alone, before strings.Split ever runs. Audit fix F5: this used to call strings.Split(raw, ";") first and check len(parts) > maxActionsPerCall second -- but Split itself allocates the full \[]string result (one allocation per entry) before that length is ever inspected, so a megabyte of semicolons allocated half a million short-lived strings before being rejected. SaveDay's own comment claims an oversized list is rejected "before a single entry is even parsed," which was true of per-entry parsing but not of this allocation. strings.Count scans the raw bytes for the delimiter without allocating a result slice, so the oversized case is now rejected without allocating anything proportional to the attacker's input at all. The caller still pays their own gas for whatever they send, so this was never a DoS on other users -- this fix makes the comment's own claim true, it does not close a cross-user vulnerability.
- OID
- 038150…b42d:31
parseActions details
parseUnixField
func(s string, part string) int64- OID
- 038150…b42d:32
parseUnixField details
parsePlotField
func(s string, part string) int- OID
- 038150…b42d:33
parsePlotField details
dayNumber
func(unixSeconds int64) int64dayNumber is the UTC calendar day index for a unix-second timestamp -- integer division by 86400 lands exactly on 00:00 UTC boundaries, matching brief 3.1 ("a day for daily limits runs from 00:00 to 24:00 UTC") without any timezone handling, because unix time is UTC by definition.
- OID
- 038150…b42d:34
dayNumber details
meterNow
func(value int64, setAt int64, now int64, dropEveryHours int64) int64meterNow ports app/js/rules.js's meterNow exactly: work out the current value from a last-known value, the unix second it was set, and how many whole dropEveryHours periods have elapsed since, clamped to \[0, meterMax].
- OID
- 038150…b42d:35
meterNow details
clampMeter
func(v int64) int64- OID
- 038150…b42d:36
clampMeter details
canPat
func(lastPatAt int64, now int64) boolcanPat ports app/js/rules.js's canPat: lastPatAt == 0 means never patted (see gnome's own comment for why 0 is an unambiguous sentinel here, unlike in the JS version).
- OID
- 038150…b42d:37
canPat details
moodNow
func(belly int64, lastPatAt int64, lastHappyEventAt int64, now int64) stringmoodNow ports app/js/rules.js's moodNow, in the same first-match-wins order as brief table 3.3: hungry, then sulky, then happy, then content. "Asleep" is display-only (brief 3.1) and has no on-chain representation at all, exactly as in rules.js. A gnome that has never been patted (lastPatAt == 0) reads as sulky here, immediately, even on the day it was adopted, if that day's batch never includes a pat -- ported faithfully from rules.js's own \`Math.max(g.lastPatAt || 0, ...)\`, which has the identical property (a never-patted gnome's "last attention" is treated as unix-epoch-zero, so "no pat for 72 hours" is trivially true). rules.test.js never exercises this edge case (every mood fixture there sets lastPatAt to a real recent-ish value), so this is a faithful port of the answer key's actual formula, not a verified-by-the-answer-key's-own- tests behaviour -- flagged here rather than silently relied on. See NOTES.md.
- OID
- 038150…b42d:38
moodNow details
xpToNext
func(level int64) int64xpToNext/totalXpForLevel/levelFromXp port app/js/rules.js's functions of the same name, line for line in spirit: 50 + 25\*level to reach the next level, level 20 the maximum.
- OID
- 038150…b42d:39
xpToNext details
totalXpForLevel
func(level int64) int64- OID
- 038150…b42d:40
totalXpForLevel details
levelFromXp
func(totalXp int64) int64- OID
- 038150…b42d:41
levelFromXp details
assignType
func(gardenXp int64, explorerXp int64, tinkerXp int64) stringassignType ports app/js/rules.js's assignType. On a genuine tie (including the trivial three-way tie of a gnome that reached level 5 on pats and feeds alone, which count towards no type at all) this returns "" rather than guessing: the brief says "the player chooses", and this version has no chooser endpoint (StartRetrain/an explicit "choose type" call is out of scope here) -- see NOTES.md. This mirrors v1's own existing convention that an empty gnomeType is a valid, documented, permanent-until-resolved state (Render already prints "(not yet set)" for it).
- OID
- 038150…b42d:42