Skip to content

Reveal adjacent-mine counts, redeploy, and fix silent event-watching failure - #1

Merged
Gp845 merged 2 commits into
mainfrom
merge-adjacent-mine-hint
Aug 29, 2026
Merged

Reveal adjacent-mine counts, redeploy, and fix silent event-watching failure#1
Gp845 merged 2 commits into
mainfrom
merge-adjacent-mine-hint

Conversation

@Gp845

@Gp845 Gp845 commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

What this does

Safe tiles now publish their adjacent-mine count to every player, while the reward still goes only to the revealer. Before this the board had no numbers at all — no deduction was possible and every click was a blind coin flip.

The count is committed inside the Merkle leaf (tileIndex, isMine, adjacentMines, nonce), so it cannot be forged: a wrong count fails proof verification. revealBoard recomputes every count on-chain from the published layout, so the end-of-round reveal proves the hints served during play were honest, not just that the mine layout was unaltered. That required storing width/height, since neighbour counts are undefined without the grid shape.

Also included

  • MerkleInterop.t.sol — differential test that feeds the contract a Merkle root computed by the TypeScript broker. It only passes if the leaf encoding, tree shape, odd-node promotion and neighbour-count rule all agree byte-for-byte. Verified it fails when the Solidity adjacency is deliberately broken.
  • createRound bounds width * height to uint16 — tile indices are uint16, so larger boards would contain tiles nobody could ever reveal, leaving the round unable to finish.
  • useBoardSync polls revealedTiles() instead of watching TileRevealed. Monad's public RPC has no eth_newFilter and caps eth_getLogs at 100 blocks, so viem's event watching fails silently — reveals landed on-chain and paid out while the UI never updated.
  • startRound marks the round started immediately after the tx is mined. It previously did so only after a follow-up entrantsOf read, which hit the RPC's 15/sec limit and left the broker believing an InProgress round hadn't started — unrecoverable, since cancelRound is Open-only.
  • Broker refuses proofs to non-entrants, matching the contract's NotEntered check; otherwise a non-entrant gets a guaranteed-revert transaction and still pays gas_limit * price on Monad.
  • Frontend: wrong-network guard with a switch button, per-connector buttons via EIP-6963, and surfaced connect/broker errors that were previously swallowed.
  • NotEntered and InvalidDimensions added to both hand-maintained ABIs.
  • AGENT_CONTEXT.md corrected — its leaf-schema formula was stale, which the file itself warns means "every proof breaks".

Deployment

Redeployed to 0x1970bA7FceE762a529ED61D22880859F7a0E3Ab7, verified on Monadscan and MonadVision. Earlier addresses are ABI-incompatible and abandoned.

Verification

  • forge test — 17 passed, 0 failed
  • tsc --noEmit clean in both server/ and web/
  • Played end-to-end against the live contract: hints served, reveal submitted, tileHint publicly readable, non-entrant refused

Known-unfixed (documented in AGENT_CONTEXT.md)

Board seed lives only in RAM — restarting the broker mid-round strands the pool permanently, and cancelRound is Open-only so there's no escape hatch. This destroyed live testnet rounds twice during development. Worth fixing before this handles anything real.

🤖 Generated with Claude Code

https://claude.ai/code/session_011RD82Rtpo7UgwF8urKXAZA

TanishaSangwan and others added 2 commits August 29, 2026 16:29
Safe tiles now publish their neighbour-mine count to every player while the
reward still goes only to the revealer. The count is committed inside the
Merkle leaf (tileIndex, isMine, adjacentMines, nonce), so it cannot be forged
-- a wrong count fails proof verification -- and revealBoard recomputes every
count onchain from the published layout, proving the hints served during play
were honest. This required storing width/height, since neighbour counts are
undefined without the grid shape.

Before this the board had no adjacency numbers at all, so no deduction was
possible and every click was a blind coin flip.

Also in this change:

- MerkleInterop.t.sol: differential test feeding the contract a root computed
  by the TypeScript, pinning both implementations together.
- createRound bounds width*height to uint16, since tile indices are uint16 and
  larger boards would contain unrevealable tiles.
- useBoardSync polls revealedTiles() instead of watching TileRevealed. Monad's
  public RPC has no eth_newFilter and caps eth_getLogs at 100 blocks, so viem's
  event watching fails silently: reveals landed onchain while the UI never
  updated.
- startRound marks the round started immediately after the tx is mined. It
  previously did so only after a follow-up entrantsOf read, which hit the RPC
  rate limit and left the broker believing an InProgress round hadn't started
  -- unrecoverable, since cancelRound is Open-only.
- Broker refuses proofs to non-entrants, matching the contract's NotEntered
  check; otherwise a non-entrant gets a guaranteed-revert tx and still pays
  gas_limit * price.
- Frontend: wrong-network guard with a switch button, per-connector connect
  buttons via EIP-6963, and surfaced connect/broker errors that were previously
  swallowed.
- NotEntered and InvalidDimensions added to both hand-maintained ABIs.
- Redeployed to 0x1970bA7FceE762a529ED61D22880859F7a0E3Ab7, verified on
  Monadscan and MonadVision. Earlier addresses are ABI-incompatible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011RD82Rtpo7UgwF8urKXAZA
origin/main's changes (entry-fee enforcement, two-step round lifecycle,
receipt-waiting, roundId from RoundCreated) were already applied by hand in
this branch, so every conflict resolved to this side -- which contains both.
Verified afterwards that each of those changes is still present, and that
forge test (17) plus both typechecks pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011RD82Rtpo7UgwF8urKXAZA
Copilot AI lite review requested due to automatic review settings August 29, 2026 11:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

A confirmed address-normalization bug in server/src/roundManager.ts allows bypassing per-player freeze state by reconnecting with different address casing.

Pull request overview

This PR upgrades the Minesweeper tournament protocol to publish verifiable adjacent-mine hints for revealed safe tiles (by committing adjacentMines into each Merkle leaf) and updates the broker + frontend to stay in sync on Monad’s limited public RPC by polling on-chain state instead of relying on log filters.

Changes:

  • Commit and surface adjacentMines hints end-to-end (Solidity leaf schema + server Merkle implementation + UI rendering).
  • Add on-chain round dimensions and a revealedTiles(roundId) view for reliable mid-round state reconstruction.
  • Harden broker/frontend operational behavior (start-round semantics, entrant-gating, wallet/network UX, and improved error surfacing).
File summaries
File Description
web/lib/abi.ts Updates hand-maintained frontend ABI for new params/fields/errors and new views.
web/hooks/useGameSocket.ts Extends WS message types to include adjacentMines where needed.
web/hooks/useBoardSync.ts Switches board sync to polling revealedTiles(); keeps event watch best-effort for attribution.
web/components/Tile.tsx Renders classic Minesweeper hint numbers with per-count styling.
web/components/Board.tsx Plumbs per-tile adjacentMines into Tile.
web/app/page.tsx Adds wrong-network gating + per-connector connect buttons; submits adjacentMines with proofs; improves status/errors.
server/src/roundManager.ts Adds dimensions to round creation, entrant caching/gating, includes adjacentMines in safe replies, and broadcasts hints on reveal.
server/src/merkle.ts Commits adjacentMines into leaf hashing and board commitments.
server/src/chain.ts Updates chain calls/events to include dimensions and adjacentMines; adds entrant reads.
server/src/board.ts Computes adjacentMines for all tiles to match on-chain adjacency logic.
server/src/abi.ts Updates hand-maintained server ABI for new params/fields/errors and new views.
README.md Updates deployed address and documents ABI-breaking redeploy rationale.
contracts/test/MinesweeperTournament.t.sol Updates tests for new leaf schema, dimensions, and hint behavior; adds new coverage for hint integrity.
contracts/test/MerkleInterop.t.sol Adds differential test to pin Solidity/TypeScript Merkle + adjacency logic byte-for-byte.
contracts/src/MinesweeperTournament.sol Implements adjacentMines commitment, dimensions, tileHint, revealedTiles, updated events/errors, and on-chain adjacency recomputation in revealBoard.
AGENT_CONTEXT.md Updates agent documentation to the new commitment schema, deploy, and RPC-driven design constraints.
Review details

Suppressed comments (1)

server/src/roundManager.ts:184

  • player is normalized for the entrant check, but frozenUntil is keyed by the un-normalized address. A client can bypass the freeze timer (and potentially other per-player state keyed by address) by reconnecting with a differently-cased player query param. Normalize once and use the normalized value consistently for map lookups/updates.
    if (!round.entrants.has(normalizeAddress(player))) {
      return { type: "error", message: "you have not entered this round" };
    }
    const now = Date.now();

    const frozenUntil = round.frozenUntil.get(player) ?? 0;
    if (frozenUntil > now) {
      return { type: "frozen", remainingMs: frozenUntil - now };
    }
    if (round.revealed.has(tileIndex)) {
      return { type: "already-revealed", tileIndex };
    }
    if (tileIndex < 0 || tileIndex >= round.board.totalTiles) {
      return { type: "error", message: "tile index out of range" };
    }

    if (round.board.isMine[tileIndex]) {
      const freezeUntil = now + env.freezeMs;
      round.frozenUntil.set(player, freezeUntil);
  • Files reviewed: 16/16 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@Gp845
Gp845 merged commit ea9686a into main Aug 29, 2026
1 check passed
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.

3 participants