SaveDay
Ask AI
SaveDay is the brief's "one daily save": the day's pats, feeds, plants, harvests and tinker task submitted as a single list in one signed call.
THE WIRE FORMAT IS UNCHANGED FROM v2, deliberately. It is proven on chain (TESTNET.md:51-60, heights 310579/310601/310681) and app/js/live.js already builds it, so changing it would cost app work and buy nothing. Semicolon-separated entries, each colon-separated:
Example
1"pat:<unixSeconds>"
2"feed:<unixSeconds>"
3"tinker:<unixSeconds>"
4"plant:<plotIndex>:<unixSeconds>"
5"harvest:<plotIndex>:<unixSeconds>"
e.g. "pat:1791600000;feed:1791601000;plant:0:1791602000". An empty string is a legal no-op. A list, rather than a []Action parameter, because gno.land's own call-argument decoder (gno.land/pkg/sdk/vm/convert.go, convertArgToGno) can only build primitive scalars and []byte from a transaction -- there is no case there for a slice of structs, and no Gno-side type cleverness changes what the wire format can carry. No gnomeID parameter either: one gnome per address, no delegation, so there is no second gnome a caller could mean and no path by which this could legitimately act on anyone else's. A sale changes which address that is, and nothing else here: the seller's next SaveDay finds no gnome, and the buyer's must date every action after the sale (Buy resets lastActionAt).
TIMESTAMPS ARE BLOCK TIME, NOT WALL CLOCK, AND A CLIENT THAT FORGETS THAT FAILS EVERY TIME. DECISIONS.md:500-515: the first on-chain SaveDay failed with "action timestamp cannot be in the future" because the client sent `date -u +%s` while block time ran ~3 seconds behind wall clock, measured not assumed, and "a browser sending Date.now() will therefore fail essentially always". The fix is client-side -- read the chain's own latest block time and subtract a small margin. This realm deliberately does NOT add a future tolerance: DECISIONS' own warning is that a tolerance near midnight would let a caller claim the next UTC day's XP allowance early.
THREAT MODEL. actionsRaw is client-supplied and this realm is the only authority over it. Carried over from v2 whole, including both of its audit fixes:
- Replay, in whole or in part: every timestamp must be strictly greater than lastActionAt, the gnome's own high-water mark. A replayed batch's first action is by definition <= that mark.
- Duplicate actions inside one call: the same strict-increase rule applies action to action, so two actions sharing a timestamp are rejected whatever kinds they are.
- Future-dated actions: every timestamp must be <= this call's own block time.
- Actions at or before adoption: subsumed by the replay check, since lastActionAt starts at adoptedAt.
- Unlimited backfill after a long dormancy (v2's audit fix F2, an owner ruling): maxBackfillHours closes this twice over. Any action older than the window is rejected outright with a message a player can understand, AND the floor the replay check enforces is itself clamped forward to now-maxBackfillHours before the loop runs, so the bound does not rest on a single comparison a future edit could weaken.
- Inflated action counts: pat spacing bounds pats per day arithmetically regardless of what the list claims (two a day at the default 12-hour cooldown, four if the owner ratchets it to the band floor of 6 -- v2's version of this note said "more than two a day is impossible", which was true of v2's fixed constant and is not true here), and feed/plant/tinker are each gated by the caller's own acorn balance, which this exact set of rules is the only thing that can increase. The economy is the rate limit. Even the floor case is far under dailyCareCap, so nothing here is a way around the cap.
- A thousand actions in one list: rejected on the delimiter COUNT, before strings.Split allocates anything proportional to the input (v2's audit fix F5).
- XP inflation via the daily cap (v2's audit fix F1): the whole call's raw care XP is mood-adjusted, then clamped to whatever remains of dailyCareCap FOR THE UTC CALENDAR DAY, tracked by the gnome's own accumulator rather than anything scoped to one call.
- The acorn softlock (new in v4, AUDIT-DEPLOYED.md G9): a batch that spends acorns may not end with nothing planted and fewer than acornReserve left. Checked once, after the apply pass, against the batch's END state, so "feed then plant" in one call is fine and the refusal names the whole batch rather than one action. See acornReserve.
ATOMICITY: every check runs against local copies -- `work` for the gnome, `acorns` for the balance -- taken at the top. Nothing is written back until the whole list has validated and applied cleanly, so any panic before the final commit leaves persisted state completely untouched. A plain value copy, not a recover()-and-rollback; see the package SECURITY note.
Command
# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v4" -func "SaveDay" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v4" -func "SaveDay" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx