Skip to content

fix(nonce-manager): serialise refresh() under the per-account lock - #228

Merged
N-thnI merged 1 commit into
Vero-protocol:mainfrom
godamongstmen897:fix/nonce-manager-refresh-lock
Aug 29, 2026
Merged

fix(nonce-manager): serialise refresh() under the per-account lock#228
N-thnI merged 1 commit into
Vero-protocol:mainfrom
godamongstmen897:fix/nonce-manager-refresh-lock

Conversation

@godamongstmen897

Copy link
Copy Markdown
Contributor

Overview

This Pull Request resolves a high-priority concurrency vulnerability in NonceManager where refresh() bypassed the per-account serialization lock (this.locks).

By introducing a shared withLock mutex helper and routing both reserve() and refresh() through it, this patch guarantees mutual exclusion across network I/O suspensions, preventing sequence collisions and cache corruption during manual recovery and fee-bump flows.

Linked Issue


Summary of Changes

1. Per-Account Mutex Helper (engine-bridge/src/nonce-manager.ts)

  • Shared withLock Execution: Factored per-account lock acquisition, execution, and release logic into a reusable private async withLock<T>(accountId: string, fn: () => Promise<T>): Promise<T> helper.
  • Wait Loop Protection: Replaced single-shot promise awaits with a re-checking loop before lock acquisition to prevent multiple parked callers from concurrently entering the critical section upon release.
  • Synchronized Mutation: Routed both reserve() and refresh() through withLock, ensuring that an in-flight getAccount network round-trip cannot be interleaved with or overwritten by a concurrent refresh operation.

2. Concurrency Regression Tests (engine-bridge/src/__tests__/nonce-manager.test.ts)

  • Mid-Flight Interleave Test: Added a test that suspends getAccount during reserve() and triggers refresh() mid-flight, verifying that the second operation queues behind the lock and preserves the latest sequence.
  • Mirror Race Test: Added coverage for triggering reserve() during an in-flight refresh().
  • Baseline Invariants: Added tests for standard caching behavior and fresh re-reads.

Verification Logs

  • Reproduction & Fix Confirmation:
    • Both concurrency race tests fail on unpatched code (2 failed / 2 passed).
    • Both concurrency race tests pass with the withLock implementation.
  • Full Test Suite: 98 tests passed across 14 test suites.
  • Type Safety & Linting: tsc --noEmit is clean; zero new ESLint warnings or errors introduced (pre-existing warnings in untouched files remain unchanged).

Notes for Maintainers

  • release() still performs cache adjustments outside withLock. Because release() does not perform asynchronous network I/O, it was left untouched to maintain minimal diff scope, but it can be wrapped in withLock in a future cleanup.

refresh() deleted the cache entry and issued its own getAccount round-trip
without touching this.locks, so it could interleave with an in-flight
reserve() for the same account. The suspended reserve() then overwrote the
cache with its older read, discarding refresh()'s value and handing back a
colliding sequence — on the recovery path an operator invokes after
something has already gone wrong.

Factor the wait/acquire/release dance out of reserve() into a shared
withLock() helper and route both reserve() and refresh() through it. The
wait is now a loop rather than a single await so multiple parked callers
re-check the slot instead of waking together.

Adds regression tests that suspend getAccount mid-reserve() and call
refresh() (and the mirror case); both fail on the previous implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 29, 2026 14:03

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@N-thnI
N-thnI merged commit be5c531 into Vero-protocol:main Aug 29, 2026
4 checks 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.

[BUG] NonceManager.refresh() bypasses the per-account lock, reopening sequence-collision races during fee-bump/manual-tx recovery

3 participants