Skip to content

perf(node): cut redundant chain-state work in info/nonce/persist paths - #72

Open
v0id-byte wants to merge 9 commits into
mainfrom
perf/node-persist-nonce-batch
Open

perf(node): cut redundant chain-state work in info/nonce/persist paths#72
v0id-byte wants to merge 9 commits into
mainfrom
perf/node-persist-nonce-batch

Conversation

@v0id-byte

Copy link
Copy Markdown
Owner

Summary

Low-risk half of a two-part perf fix, following up on an external audit doc (v0idchain-perf-fix-list.md) claiming several O(chain)-per-call hotspots at 100k-block scale. Every claim below was independently re-verified against the current code (not taken at face value — a few of the doc's specific mechanisms turned out to be wrong even though the underlying perf problem was real) and confirmed live with a real node + game-server integration test, not just scripts/smoke.ts.

Deliberately excludes the highest-value fix (Blockchain.cachedState + incremental addBlock, which would make computeState() O(1) instead of O(chain)) — that touches consensus/validation code directly reachable from untrusted P2P peers, so it's staged as a separate follow-up PR with its own dedicated equivalence tests rather than bundled here.

  • info() computed chain state twice (balanceOf called once for self, once for the burn address, each a full chain walk) — now one computeState() call, read twice.
  • No /nonce endpoint existed on the node. game-server's getNonce() and, separately, faucet.ts each forced a full chain fetch + JSON parse + local Blockchain rebuild just to read one address's nonce. Added /nonce, plus /account (nonce+balance from a single computeState() call so both values come from the same chain height — fixes a real atomicity gap a reviewer caught: two separate /nonce+/balance calls could straddle a block boundary). Both game-server call sites now hit the node directly.
  • persist() wrote the entire chain+mempool to disk synchronously on every send/tx/received-block (7 call sites). Now debounced 500ms and flushed immediately on SIGINT/SIGTERM/uncaughtException/unhandledRejection, so neither a normal shutdown nor a crash silently drops the last half-second of state.
  • seenTx (P2P gossip-echo dedup) was memory-only, cleared every restart — now persisted to seenTx.json.
  • game-server's chainCache was a single-entry 2s-TTL cache with no request coalescing — concurrent cache misses each independently re-fetched and re-parsed the full chain (cache stampede). Now stale-while-revalidate with a shared in-flight promise.
  • Graceful shutdown added to both node and game-server (neither had any signal handling before).

Also tried lowering the PoW mining BATCH constant (20000→5000) per the audit doc; reverted after review turned up a legitimate hashrate-cost concern that wasn't worth trading for an unquantified responsiveness gain — left at its original value.

Test plan

  • pnpm -r run typecheck — clean across all 6 packages
  • pnpm run smoke — 163/163 checks pass
  • Live integration test: real node + game-server processes, mining enabled
    • /nonce, /balance, /account cross-checked for consistency
    • info() balance/burned correctness after the dedup change
    • persist debounce: confirmed chain.json mtime unchanged immediately after a send, flushed shortly after; correct final content
    • seenTx.json created with correct content + 0600 perms
    • SIGTERM on node: prompt exit, state flushed, chain resumes correctly from persisted state on restart
    • SIGTERM on game-server: prompt exit; double-SIGTERM sent rapidly confirmed the re-entrancy guard suppresses the duplicate close/timer
    • faucet dispense end-to-end through the new /account path, including duplicate-claim rejection (pre-existing logic, unaffected)
    • repeated /api/chain calls spanning the cache TTL boundary: fast responses throughout, eventually consistent, no errors
  • Self-review: 8-angle automated diff review (correctness, removed-behavior, cross-file, reuse, simplification, efficiency, altitude, CLAUDE.md conventions) run before opening this PR; findings applied (combined /account endpoint, dead force param removed, shutdown-handler asymmetry fixed, BATCH reverted) rather than shipped as-is

🤖 Generated with Claude Code

v0id-byte and others added 3 commits July 10, 2026 03:09
Social design docs live in the private v0id-social repository.
This monorepo keeps a pointer and the multi-pillar roadmap narrative.
Add memo-convention public posts: contentHash on-chain, body off-chain.
parseSocialPosts is deterministic; protocol memos stay out of DMs.
Includes social-selftest for hash stability, mint path, and supply checks.
Fixes confirmed-real items from a perf audit at 100k-block scale, deliberately
scoped to changes that don't touch consensus/validation logic (the bigger
computeState-caching + incremental-addBlock rework is a separate, higher-risk
follow-up PR).

- node info() computed full chain state twice (balanceOf called separately
  for self + burned address); now one computeState() call, read twice.
- Node had no /nonce endpoint at all; game-server's getNonce() and faucet.ts
  each independently forced a full chain fetch+parse+local-recompute to get
  a single address's nonce. Added /nonce, /balance-shaped /account (nonce+
  balance from one computeState so both come from the same chain height),
  switched both callers to hit the node directly instead.
- persist() wrote the full chain+mempool to disk synchronously on every
  send/tx/block (~7 call sites) - now debounced 500ms, flushed immediately
  on SIGINT/SIGTERM/uncaughtException/unhandledRejection so normal and
  crash-path shutdown don't silently drop the last half-second of state.
- seenTx (P2P gossip dedup set) was memory-only, cleared every restart;
  now persisted to seenTx.json alongside chain state.
- game-server's chainCache was a single-entry 2s-TTL cache with no
  request coalescing - concurrent misses each independently re-fetched the
  full chain. Now stale-while-revalidate with a shared in-flight promise.
- Added graceful shutdown to both node and game-server (neither had one).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Add computeSocialBurnMin so post/reply minimum burns fall as difficulty
rises (scheme 2), with hard floors. Parse uses inclusion-block difficulty.
Social funding can credit coinbase to a user address without importing
their private key.
Difficulty retarget now aims for ~1s blocks so post/mine-enough
needs fewer wall-clock seconds. Soft-fork: all validators must upgrade.
HTTP /mine (social coinbase-to-user) no longer races continuous --mine
on the same tip. Continuous mining with interval 0 uses setImmediate.
Changing the retarget target rewrites historical expectedDifficulty and
makes loadChain reject the whole chain. Document that --mine-interval 0
is the non-consensus way to avoid artificial gaps between blocks.
Social mine-enough (miner≠node wallet) now holds a demand lock so
background --mine does not interleave and double wall-clock time.
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.

1 participant