Harden the battle and inventory seam, consent, and receipt signing - #75
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🧪 Coverage
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
What this is
Started as a review of the roadmap §4 inventory work against the backend-authoritative
battle path (
docs/plan-battle-inventory-hardening.md), and grew to cover the signing andconsent machinery that path depends on. 36 commits across 8 packages.
The review found one blocker and a cluster of correctness bugs; running the result against a
live Base Sepolia deployment found several more that only appear with real data. Both sets
are here, with the plan doc updated in place to record what was wrong and what was decided.
Deploy notes, read before merging
Four things change behaviour on an existing deployment.
1.
go-verifieris now a required attester by default.BATTLE_SIGNER_REQUIRED_ATTESTERSdefaults totypescript-engine,go-verifier. §F is aprecondition, not a fallback: the backend will not sign a receipt the independent Go port has
not confirmed. A deployment without
INDEXER_GRPC_ADDRset andindexer-gorunning willhave every battle stall at
computedand then forfeit after eight attempts. This is theintended posture, but it is a live change. Boot now says which state you are in.
2. Serving more than one chain family now requires per-domain signing keys.
§G separates keys per reward domain, so
BATTLE_SIGNER_EVM_KEY_IDandBATTLE_SIGNER_SOLANA_KEY_IDmust both be set whenBATTLE_CHAIN_IDSspans both. Note thedefault
BATTLE_CHAIN_IDSiseip155:31337,solana:localnet, which spans both, so a localdev environment relying on defaults is affected. A single-family deployment keeps inheriting
the shared
BATTLE_SIGNER_KEY_ID. The signer refuses to start rather than quietly puttingone key across both domains, which is threat T4.
3. Two migrations, applied with
prisma migrate deploy.20260810120000_battle_ruleset_version_not_uniquedrops the unique constraint onbattle_ruleset.version. Two catalog states can share a ruleset version and differ only byhash, and the constraint made publishing the second one fail.
20260810140000_add_session_delegationaddssession_delegation, withENABLE ROW LEVEL SECURITYand noFORCE, per the repo rule.4. One wire code renamed.
POST /api/battle/intentsreturnedexpiredfor a timed-out request, while the accept pathreturns
expiredfromCoverageFailuremeaning the defender's authorization lapsed. Onecode carried two meanings and the client maps a code to text with no idea which endpoint
answered, so one of the two was always wrong. The intent side is now
intent-expired,matching what accept already called it.
What changed, by theme
Inventory seam (the original review)
equipment, so nothing settled. Proved with two differing hashes, fixed with one shared
codec, and the fix is pinned by reintroducing the bug in a test.
it. An unreadable equipment row moves
rulesetHashand invalidates every outstandingdefence authorization; an uncatalogued equipped item produces a receipt saying the pet
fought bare while chain state disagrees.
equipment.json.an entitlement twice; spending a consumable refreshes the pet's own numbers.
Consent and matchmaking
hashRuleset(SOURCE_DEFAULT_RULESET)while defenders signagainst the served ruleset. The two are equal only while the item catalog is empty, so on a
seeded deployment the filter matched no authorization ever written and the opponent list
came back empty. Silent, because empty is also the correct answer when nobody has
consented.
servedRulesetHash()now derives the hash once beside the ruleset it belongsto, and
hashRulesetno longer appears anywhere inbackend/src.diagnoseEmpty()names which of six situations produced a blank picker.stale.
Signing (§G)
against the published address.
BATTLE_SIGNER_PRIVATE_KEYis refused in production.published forever, since a receipt signed under a rotated key must still verify.
Session delegation (§D)
Battles no longer prompt the wallet per fight. The owner signs one
SessionDelegationand aclient-held ephemeral key signs intents under it. Scope is battle-intent only and explicitly
excludes defence consent; the key lives in
sessionStorageunder a 24h cap and is neversent anywhere, so the operator still cannot produce an intent. A key the server rejects is
discarded locally and the intent re-signed once with the wallet, which is the pre-session
path.
Frontend
dashed voids became bays that draw an empty arena and show the shape of what will fill
them; the floating VS and detached win-rate box became one odds rail spanning both bays.
and stylesheet differing only in an accent colour. Now one
TabSwitchprimitive incomponents/ui, styled as a segmented control.Operator visibility
Three failures in this branch were hard to diagnose for the same reason: the cause was known
in-process and never reported.
configureSignerrecorded why it refused and returned, so adeployment that could not sign anything booted clean. Boot now reports the signer and the
verifier, both lookups carry the configuration error into
failureReason, andbackend/scripts/gains three read-only diagnostics (diagnose-signer,diagnose-battles,diagnose-stuck). None prints key material.indexer-go
Both EVM loops polled a hosted subgraph on a fixed ticker with no backoff, so an HTTP 429 was
retried at full rate indefinitely and the limit never cleared. Adds a pacer with doubling
backoff capped at 60s, cleared on the first success, preferring the server's own
Retry-After. The loops also ticked in phase and arrived as simultaneous pairs, which iswhy both error lines carried the same millisecond; inventory now starts half a period out
with its own pacer.
Boundaries
contracts/test-vectors/{battle,xp,equipment}.jsonare unchanged. One case was addedto
equipment.jsonfor the clamp fix, none edited.protocol,verifier,services/indexer-go.protocolgainssrc/session/and takes no new dependencies, so the no-PolyForm-import test still holds.sessionDelegationis a new versioned object at version 1. No existing schema versionmoved, so no receipt is re-encoded.
Review guidance
The commits are themed and mostly stack in order. If this is too large as one unit, it splits
cleanly at: inventory seam, consent and matchmaking, signing and §G, session delegation,
frontend. The two migrations and the attester default are the parts worth the most scrutiny.
Verification
go build,go vet,go test ./...clean across 9 packagespnpm lintclean. Golden vectors pass in both live ports and Anchor's frozen suite.