Skip to content

Contract optimizations + security fixes (k-1 on 3 hot-path circuits) - #3

Merged
Lambdanaut merged 2 commits into
mainfrom
contract/optimizations-and-security
Mar 30, 2026
Merged

Lambdanaut merged 2 commits into
mainfrom
contract/optimizations-and-security

Conversation

@acedward

Copy link
Copy Markdown
Contributor

Summary

Circuit optimizations and security hardening for the Go Fish Midnight contract. The three most expensive hot-path circuits each dropped one k-level (~50% faster proofs).

Circuit cost changes

Circuit Old k New k Row change
applyMask 17 16 -37%
respondToAsk 16 15 -29%
askForCard 15 14 -34%
goFish 13 removed draw merged into respondToAsk

Per-turn cost: 61K → 42K rows (-31%), Go Fish path: 4 tx → 3 tx

Optimizations

  • A: Hoist getSecretFromPlayerId in respondToAsk (1 call instead of 4)
  • B: Deduplicate askForCard (single countCardsOfRank_withSecrets)
  • C: Inline count+removal in checkAndScoreBook
  • E3: Coordinate-based grand product in shuffle (eliminates 44 transient hashes)
  • G: Batch discoverHand circuit (21-card scan in 1 call)
  • H: Unmasked transfers (no opponent secret needed)
  • I: Merge goFish draw into respondToAsk (3 tx per turn instead of 4)

Security fixes

  • V1: ownPublicKey() authentication on all state-mutating circuits
  • V2: Each circuit only needs caller's own secret (verified by player-isolated witness tests)
  • V3: afterGoFish verifies drawn card rank programmatically (no self-report)
  • V4: Winner tracking in ledger
  • V6/V7: Removed dead code and deprecated circuits

Test plan

156 tests covering all 30 exported circuits with positive + negative paths, simulation-embedded runtime invariants, and player-isolated witness proxy tests.

deno task -f @go-fish/midnight-contracts contract:compile
deno task -f @go-fish/midnight-contracts test

🤖 Generated with Claude Code

acedward and others added 2 commits March 29, 2026 17:04
Optimizations:
- Opt A: Hoist getSecretFromPlayerId in respondToAsk (1 call instead of 4)
- Opt B: Deduplicate askForCard (single countCardsOfRank_withSecrets)
- Opt C: Inline count+removal in checkAndScoreBook
- Opt D: Remove redundant phase/turn checks from transfer variants
- Opt E3: Coordinate-based grand product in shuffle (eliminates 44 transient hashes)
- Opt G: Batch discoverHand circuit (21-card scan in 1 call, 27 ec_mul vs 147)
- Opt H: Unmasked transfers (insert baseCard directly, no opponent secret needed)
- Opt I: Merge goFish draw into respondToAsk (3 tx per Go Fish turn instead of 4)

Security fixes:
- V1: Player authentication via ownPublicKey() on all state-mutating circuits
- V2: Each circuit only needs caller's own secret (verified by player-isolated P-tests)
- V3: afterGoFish verifies drawn card rank programmatically (no self-report boolean)
- V4: Winner tracking in ledger (timeout + 7-book endgame)
- V6: Removed dead code (moveCardFromPlayerToPlayerCardInHand)
- V7: Removed goFish circuit, made getTopCardForOpponent internal

Circuit cost changes:
  applyMask:    k=17 → k=16  (-37% rows)
  respondToAsk: k=16 → k=15  (-29% rows)
  askForCard:   k=15 → k=14  (-34% rows)
  goFish:       removed (draw merged into respondToAsk)

Test suite: 156 tests, full game simulation with S-series runtime invariants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Q-series: 8 auth tests verifying ownPublicKey() prevents impersonation
  (P1 can't act as P2, third party rejected, registration prevents slot reuse)
- S1 invariant now uses contract getHandSizes/getScores instead of local tracking
- S15 score check uses contract scores as source of truth
- isGameOver uses contract circuit instead of local book count
- Book scoring in simulation uses contract checkAndScoreBook directly per rank
  instead of local checkAndScoreBooks function

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Lambdanaut
Lambdanaut merged commit d4f1f8c into main Mar 30, 2026
acedward added a commit that referenced this pull request Apr 16, 2026
… double-count

Three contract changes resolve BACKEND_ISSUES #3 (game stalls in
TurnStart with empty hand) and #4 (winner ledger never written):

1. GoFish.compact:addScore — replace the 7-book cap with an early-win
   at score ≥ 4 (majority of 7). Also fixes a read-after-write
   double-count bug: Compact's queryLedgerState returns the post-write
   value on re-read, so the old `lookup → insert → lookup → +1`
   pattern was triggering game-over at 3 books instead of 4. Now
   computes post-increment scores from the pre-read locally.

2. game.compact:askForCard — relax rule 5 when the asker's hand is
   empty. Routes empty-hand turns through respondToAsk's existing
   go-fish branch (no new circuit needed). Matches real Go Fish rules.

3. game.compact — add top-level getWinner wrapper so the managed
   contract exposes the V4 winner ledger reader.

Client: e2e helpers drop the empty-hand bail, game-round test adds
hard assertions for finalPhase==GameOver and winner∈{1,2}.

Validated: 164/164 contract tests pass, 27/27 node tests pass,
ec_mul guard detection clean, 3 consecutive sim runs all end at
exactly 4 books with correct winner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants