Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
1f7d56c
docs: split backend-authoritative battle plan into commit-sized steps
heyradcode Jul 26, 2026
bbdb04b
docs: add backend battle threat model and key-compromise runbook
heyradcode Jul 26, 2026
a81be93
docs: align future-features roadmap with backend-authoritative battles
heyradcode Jul 26, 2026
313ecc1
feat(protocol): scaffold MIT protocol package
heyradcode Jul 26, 2026
6e75460
refactor(protocol): move TS combat engine out of shared into MIT prot…
heyradcode Jul 26, 2026
08e8354
feat(protocol): add canonical binary encoding and keccak hashing prim…
heyradcode Jul 26, 2026
4fff0f5
feat(protocol): bind signed objects to chainId and deploymentId
heyradcode Jul 26, 2026
679f788
feat(protocol): add wallet-signed battle intent schema and hashing
heyradcode Jul 26, 2026
66470a8
feat(protocol): add standing defense-authorization schema and hashing
heyradcode Jul 26, 2026
96ee54e
feat(protocol): add frozen pet snapshot schema and snapshot hashing
heyradcode Jul 26, 2026
f491e71
feat(protocol): derive battle seed from drand randomness with domain …
heyradcode Jul 26, 2026
ce0347a
feat(protocol): verify drand quicknet BLS beacon signatures against a…
heyradcode Jul 26, 2026
e32efa9
feat(protocol): add battle commitment schema, hashing, and chain link
heyradcode Jul 26, 2026
301241f
feat(protocol): port XP and progression math to TypeScript with golde…
heyradcode Jul 26, 2026
9020113
feat(protocol): add content-addressed ruleset versioning and hashing
heyradcode Jul 26, 2026
c74dee1
feat(protocol): add signed battle receipt schema, hashing, and hash c…
heyradcode Jul 26, 2026
f0c0c9d
feat(protocol): add canonical Merkle leaf encoding, proofs, and vectors
heyradcode Jul 26, 2026
a304143
feat(backend): add battle ledger, commitment, receipt, and progress m…
heyradcode Jul 26, 2026
fedd3d2
feat(backend): add transactional battle ledger state machine and outbox
heyradcode Jul 26, 2026
a0d310f
feat(backend): accept wallet-signed battle intents
heyradcode Jul 26, 2026
014bd4f
feat(backend): add standing defender consent with immediate revocation
heyradcode Jul 26, 2026
8a4ad30
feat(backend): add verified drand quicknet round client with same-rou…
heyradcode Jul 26, 2026
7a3d64a
feat(backend): add schema-restricted KMS signer for commitments and r…
heyradcode Jul 26, 2026
4c40838
feat(backend): sign and deliver battle commitment before the drand ro…
heyradcode Jul 26, 2026
2160626
feat(backend): compute battles from verified drand seeds in a worker
heyradcode Jul 26, 2026
bba2527
feat(indexer-go): verify backend battle results independently before …
heyradcode Jul 26, 2026
6ff69db
feat(backend): sign battle receipts and append global and per-pet has…
heyradcode Jul 26, 2026
eee8eac
feat(backend): expose battle state, commitment, receipt, and key endp…
heyradcode Jul 26, 2026
8dadfef
feat(backend): publish a paginated public receipt corpus
heyradcode Jul 26, 2026
4f51bb2
feat(backend): scope live-battle notifications to a per-room WebSocket
heyradcode Jul 26, 2026
3e6e22b
feat(verifier): scaffold standalone MIT receipt verifier CLI
heyradcode Jul 26, 2026
39c1c0e
feat(verifier): verify beacon, seed, combat replay, and progression
heyradcode Jul 26, 2026
dcecc8a
feat(verifier): pin ruleset artifacts and run receipt verification in CI
heyradcode Jul 26, 2026
cf1f730
feat(frontend): submit signed battle intents and persist the signed c…
heyradcode Jul 26, 2026
ed1f067
feat(frontend): verify battle receipts client-side before replaying t…
heyradcode Jul 26, 2026
2c225c8
feat(backend): shadow-compute settled on-chain battles against the ba…
heyradcode Jul 26, 2026
63422c1
feat: launch rewardless backend battle mode behind a flag
heyradcode Jul 26, 2026
7f1f4ea
feat(contracts): add battle batch root registry
heyradcode Jul 26, 2026
88e86b8
feat(backend): aggregate signed receipts into anchored Merkle batches
heyradcode Jul 26, 2026
eea1417
feat(backend): anchor batch roots in the on-chain registry
heyradcode Jul 26, 2026
858897d
feat(contracts): add capped aggregate reward claims with nullifiers
heyradcode Jul 26, 2026
8f93e2c
feat(backend): compute reward seasons and serve claim proofs
heyradcode Jul 26, 2026
25a2d5c
feat: enable bounded aggregate season rewards
heyradcode Jul 26, 2026
ef4c47b
fix(backend): persist the signing-key registry across restarts
heyradcode Jul 26, 2026
dc1fd05
chore(backend): add safe prisma migrate status and deploy scripts
heyradcode Jul 26, 2026
775acda
refactor(contracts): remove on-chain battle settlement from EVM contr…
heyradcode Jul 26, 2026
7303b60
refactor(backend): remove on-chain battle settlement and shadow mode
heyradcode Jul 26, 2026
28bc05e
refactor(indexer-go): stop ingesting on-chain battles
heyradcode Jul 26, 2026
600610b
refactor(shared): resolve battles through the backend, not the chain …
heyradcode Jul 26, 2026
17666ad
refactor(frontend): resolve battles from the verified receipt, not ch…
heyradcode Jul 27, 2026
b426488
refactor(solana,subgraph): remove the on-chain battle path
heyradcode Jul 27, 2026
b9cffe4
feat(backend,shared): merge backend battle progression into pet reads
heyradcode Jul 27, 2026
695daad
fix(backend): band matchmaking on merged progression, not frozen chai…
heyradcode Jul 27, 2026
2418845
fix(backend): record battle history from the receipt, not the client
heyradcode Jul 27, 2026
2a0ae7a
refactor(contracts): drop GameLogic's retired battle storage
heyradcode Jul 28, 2026
060cc16
refactor(contracts,indexer): drop the retired battle record from PetCore
heyradcode Jul 28, 2026
d9cf7fa
refactor(contracts): delete CombatSim and its Solidity vector suite
heyradcode Jul 28, 2026
9f9c196
feat(contracts): deploy BattleBatchRegistry and SeasonRewardDistributor
heyradcode Jul 28, 2026
04e838a
refactor(indexer-go,proto): remove the dead battle pipeline
heyradcode Jul 28, 2026
fd6ccc8
refactor(contracts): drop GameConfig.levelBandWidth
heyradcode Jul 28, 2026
41449db
docs: correct the reference docs after the battle-path removals
heyradcode Jul 28, 2026
74c5550
feat(contracts): warn before reconciling onto an existing deployment
heyradcode Jul 28, 2026
c441e24
cleanup: remove dead metrics and correct references to deleted code
heyradcode Jul 29, 2026
3af8404
refactor(solana): delete open_to_challenges and level_band_width
heyradcode Jul 29, 2026
bdd9385
Merge branch 'main' into feat/backend-battle-logic
heyradcode Aug 1, 2026
3e51f61
fix: read back the deployment just written, not chain-<id>
heyradcode Aug 1, 2026
f1afe24
fix(subgraph): resolve addresses from the deployment actually in use
heyradcode Aug 1, 2026
6ea6eac
feat(backend): add a dev script to grant standing defence consent
heyradcode Aug 1, 2026
741d756
feat(frontend): let owners grant standing defence consent from the app
heyradcode Aug 1, 2026
13811e9
fix(backend): keep the signing key window at its first-signing time
heyradcode Aug 2, 2026
39574be
feat(contracts): add the CPET reward token
heyradcode Aug 2, 2026
284c81f
ci: run the golden-vector suites, and fix a filter that hid a real bug
heyradcode Aug 4, 2026
82a675f
fix(backend): stop battles ignoring levels bought on chain after firs…
heyradcode Aug 4, 2026
7adf5ba
fix(backend): hide opponents whose owner has not consented to defend
heyradcode Aug 4, 2026
ab644f0
fix(frontend): link the battle to the room it minted
heyradcode Aug 4, 2026
763f47d
fix(frontend): drop an opponent that refuses on consent, and re-read …
heyradcode Aug 4, 2026
3248a5c
ci: run the contracts job on Node 22
heyradcode Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
13 changes: 13 additions & 0 deletions .github/workflows/image-generator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@ name: Image generator
# image-generator/README.md), so the root `pnpm lint` / `pnpm test` aggregates and
# the Coverage workflow do not reach it. Without this workflow nothing runs its
# suite at all.
# The path filter must also list the files `src/solanaLayout.test.ts` reads, not just
# this package. That suite pins the PetAccount byte layout against the Anchor IDL and
# `pet.rs`, so a Solana account change breaks it while touching nothing under
# `image-generator/`. Filtered on this package alone the suite simply does not run, which
# is how `open_to_challenges` was removed from the program while the decoder kept its
# byte — every field after it misaligned, and a pet rendered as a different pet.
#
# The two lists below are duplicated on purpose: GitHub Actions does not support YAML
# anchors, so factoring them out would silently disable the filter rather than share it.
on:
pull_request:
branches: [main]
paths:
- 'image-generator/**'
- '.github/workflows/image-generator.yml'
- 'contracts/solana/cryptopets/programs/cryptopets/src/state/**'
- 'indexer-go/internal/solana/idl/**'
push:
branches: [main]
paths:
- 'image-generator/**'
- '.github/workflows/image-generator.yml'
- 'contracts/solana/cryptopets/programs/cryptopets/src/state/**'
- 'indexer-go/internal/solana/idl/**'

permissions:
contents: read
Expand Down
102 changes: 102 additions & 0 deletions .github/workflows/parity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Combat parity

# The golden vectors in `contracts/test-vectors/` are what AGENTS.md calls the
# cross-language enforcement for combat-simulator parity — and until this workflow
# existed, nothing ran them. Coverage covers backend/frontend/shared, Verifier covers
# `verifier`, and the three suites that actually replay the vectors were covered by
# neither:
#
# protocol tests/combat/goldenVectors.test.ts (the canonical TS engine)
# indexer-go internal/combat/combat_golden_test.go (the independent Go port)
# contracts/ethereum test/XpFormula.test.ts (the XP fixture)
#
# §F's circuit breaker only has value while the TS and Go ports are independent and both
# match the vectors. A drift that CI never runs is a circuit breaker nobody armed.
#
# Deliberately not path-filtered. A parity break is caused precisely by changing one side
# and not the other, so filtering on either side's paths would skip the run that matters.
# See the image-generator workflow for what path filtering costs here.
#
# Anchor's frozen Rust suite is the fourth witness and is NOT run here: it needs a Solana
# toolchain this runner does not have. That gap is real — those tests are the only
# remaining independent evidence that the vectors describe what actually settled on chain.
on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

concurrency:
group: parity-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
typescript:
name: protocol + contracts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
# Hardhat 3 needs >= 22.10. It calls `.flatMap` on the iterator from
# `Map.values()`, which is an Iterator Helpers method that does not exist
# before Node 22, so `compile` dies with a TypeError rather than a version
# check. Do not drop this back to 20 to match the other workflows.
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

# The canonical engine, including tests/combat/goldenVectors.test.ts.
- name: Protocol tests
run: pnpm --filter @cryptopets/protocol test

- name: Protocol lint
if: always()
run: pnpm --filter @cryptopets/protocol lint

# MIT boundary: protocol must not import from a PolyForm package, or the public
# verifier that depends on it cannot be distributed. Enforced by its own test, run
# above — this step exists so the typecheck failure is separately legible.
- name: Protocol typecheck
if: always()
run: pnpm --filter @cryptopets/protocol typecheck

- name: Compile contracts
if: always()
run: pnpm --prefix contracts/ethereum compile

- name: Contract tests
run: pnpm --prefix contracts/ethereum test

go:
name: indexer-go
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: indexer-go/go.mod
cache-dependency-path: indexer-go/go.sum

- name: Vet
working-directory: indexer-go
run: go vet ./...

# Unit tests only. The Postgres-backed tests are gated on TEST_DATABASE_URL and
# truncate tables, so they are deliberately not given one here.
- name: Test
working-directory: indexer-go
run: go test ./...

- name: Build
working-directory: indexer-go
run: go build -o /dev/null ./cmd/indexer
59 changes: 59 additions & 0 deletions .github/workflows/verifier.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Verifier

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

concurrency:
group: verifier-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

# The verifier's suite builds real receipts through @cryptopets/protocol — real
# signatures, real drand fixtures, real combat simulation — so a protocol regression
# surfaces here too, without needing a separate step for it.
- name: Verifier tests
run: pnpm --filter @cryptopets/verifier test

- name: Verifier lint
if: always()
run: pnpm --filter @cryptopets/verifier lint

# The committed corpus, run through the actual CLI rather than the library, so the
# thing a third party would run is the thing CI proves still works. No network
# access: the ruleset these battles were fought under is pinned in the checkout.
- name: Honest corpus must verify
if: always()
run: pnpm --filter @cryptopets/verifier cli -- fixtures/corpus.json --keys fixtures/signing-keys.json

# The half that stops this from being theatre. "The honest corpus verifies" is also
# true of a verifier that has quietly degraded into always passing; only this step
# notices that.
- name: Tampered corpus must be rejected
if: always()
run: |
if pnpm --filter @cryptopets/verifier cli -- fixtures/corpus-tampered.json --keys fixtures/signing-keys.json; then
echo "::error::The tampered corpus verified successfully. The verifier is not actually checking anything."
exit 1
fi
echo "Tampered corpus rejected, as expected."
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ Normative language: `MUST`/`MUST NOT` are mandatory. `SHOULD`/`SHOULD NOT` are e

## Non-Negotiables

- `MUST NOT` edit the golden test vectors in `contracts/test-vectors/{battle,xp}.json` to make a failing test pass. If a vector fails, the Go or Rust port has drifted from the Solidity contract; fix the drifted port, never the vector.
- `MUST` update all four combat-simulator ports together (`contracts/ethereum/src/CombatSim.sol`, Solana's `combat.rs`, `indexer-go/internal/combat/`, `shared/src/utils/combat/`) when changing combat logic. Never patch one leg alone. The TS port (`shared/src/utils/combat/`) covers fight math only, not XP — see its package doc.
- `MUST NOT` change Solana's frozen combat port (`game/battle_sim.rs`, `game/xp.rs`). It has no caller left in the program, but its golden-vector tests are what still prove `contracts/test-vectors/{battle,xp}.json` describe what actually settled on that chain. A bug found there is fixed forward in the live ports below, under a new `rulesetVersion`, never by patching the frozen one. **The Solidity port is gone**: `CombatSim.sol` was deleted once it had no on-chain caller, which also removed `battle.json`'s Solidity generator and validator. `battle.json` itself is unchanged and still gates the live ports.
- `MUST` keep the two **live** combat ports in step with each other and with the golden vectors: `protocol/src/combat/` (the canonical engine, re-exported from `shared/src/utils/combat` for existing importers) and `indexer-go/internal/combat/` (the independent verifier). Changing one without the other re-breaks the circuit breaker in §F, whose whole value is that the two were written to disagree if either drifts. This covers XP and level progression too (`protocol/src/combat/xp.ts`, validated against `contracts/test-vectors/xp.json`), so an XP or decay change is a both-ports change. `indexer-go/internal/combat/xp.go` still covers the formula and the decay but not level-up.
- `MUST NOT` edit `contracts/test-vectors/{battle,xp}.json` to make a failing test pass — this holds more strongly now, not less. The vectors are the only mechanical link left between the frozen ports and the live ones. A live port that fails them has drifted away from the rules real battles were settled under.
- `MUST NOT` assume the `ChainAdapter` interface (`shared/src/hooks/adapters/`) covers more than pet-action mutations and reads. It is a real, shared interface (`useEvmAdapter`/`useSolanaAdapter` both implement it) and every public pet-action hook consumes it chain-blind, but the low-level chain wiring in `frontend/src/chains/{ethereum,solana}/`, the async battle/breed VRF flows, and the combat simulator remain intentionally separate per chain. See CLAUDE.md's cross-chain interfaces section for the exact boundary.
- `MUST` match the license of the package being edited when adding new files: `contracts/ethereum`, `contracts/solana`, `indexer-go`, and `proto` are MIT; everything else, `image-generator` included, is PolyForm Noncommercial 1.0.0 (root `LICENSE`). See the table in `README.md`.
- `MUST` match the license of the package being edited when adding new files: `contracts/ethereum`, `contracts/solana`, `indexer-go`, `proto`, `protocol`, and `verifier` are MIT; everything else, `image-generator` included, is PolyForm Noncommercial 1.0.0 (root `LICENSE`). See the table in `README.md`. `protocol` is MIT on purpose (third parties have to be able to replay signed battle receipts), so it `MUST NOT` import from a PolyForm package; a test in that package enforces it. `verifier` is MIT for the same reason and depends on nothing but `protocol`.
- `MUST NOT` assume the root `pnpm lint` / `pnpm test` cover `image-generator`, and `MUST NOT` verify it with `pnpm --filter image-generator <script>`. It is not a pnpm workspace member, so that command prints `No projects matched the filters` **and exits 0**: it reports success having run nothing. Run its scripts from `image-generator/` instead. It keeps its own lockfile, installs with `pnpm install --ignore-workspace`, and is checked by its own CI workflow.
- `MUST NOT` treat the v1 contract gaps documented in `contracts/plan-contract-upgrade.md` (no battle authorization, the `changeDna` cheat, client-supplied Solana starter-pet DNA) as bugs to silently patch. They are the known baseline the v2 rewrite is designed around.
- `MUST` run the smallest scoped lint/test/build command for the package you touched (see Command Baseline below), not a full monorepo run, unless the change is broad.
Expand Down Expand Up @@ -46,6 +47,6 @@ Full per-package lint/test/build matrix and single-test syntax: see [CLAUDE.md](
Mechanical checks over prose, where they exist:

- ESLint per package (`frontend`, `shared`, `website`, `mobile`), plus a custom CSS-naming check in `frontend` (`lint:css`).
- Golden test vectors (`contracts/test-vectors/{battle,xp}.json`), run by Hardhat, Anchor, `indexer-go`'s `combat_golden_test.go`, and `shared`'s `tests/utils/combat/goldenVectors.test.ts` (Vitest), are the cross-language enforcement for combat-simulator parity.
- CI coverage workflow (`.github/workflows/coverage.yml`) runs frontend/backend/shared vitest coverage on every PR and posts a combined comment.
- Golden test vectors (`contracts/test-vectors/{battle,xp}.json`), run by Anchor, `indexer-go`'s `combat_golden_test.go`, and `@cryptopets/protocol`'s `tests/combat/goldenVectors.test.ts` (Vitest), are the cross-language enforcement for combat-simulator parity. Anchor's frozen suite proves the vectors still describe what really settled on Solana; the two live ports prove they have not drifted from it. Hardhat no longer checks `battle.json` — that leg went with `CombatSim.sol`.
- CI coverage workflow (`.github/workflows/coverage.yml`) runs frontend/backend/shared vitest coverage on every PR and posts a combined comment. The verifier workflow (`.github/workflows/verifier.yml`) replays a committed receipt corpus through the standalone verifier, and asserts a tampered corpus is rejected.
- There is no repo-wide `agents:check` or module-boundary lint yet. Rely on the per-package commands above and the golden vectors until one exists.
Loading
Loading