Skip to content

op-batcher: cover the safety-critical espresso batcher logic with tests - #509

Open
philippecamacho wants to merge 2 commits into
espresso/batcherfrom
espresso/batcher-496-tests
Open

op-batcher: cover the safety-critical espresso batcher logic with tests#509
philippecamacho wants to merge 2 commits into
espresso/batcherfrom
espresso/batcher-496-tests

Conversation

@philippecamacho

@philippecamacho philippecamacho commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Closes #496. Stacked on #459 (espresso/batcher), which is where all of the code under test lives.

What is covered

Active-batcher gate (espresso_active.go, shouldSkipPublishForActiveSeq)

  • Every quadrant of the isBatcherActive mode check (activeIsEspresso × Espresso.Enabled) — an inverted mode check compiles and passes CI, so each quadrant is pinned explicitly.
  • The identity gate for both modes, including the batcherHash low-20-byte slicing (with a nonzero-high-bytes case so a future versioned batcherHash can't break address extraction).
  • Every shouldSkipPublishForActiveSeq path: the fail-closed error paths, and proof that the pre-fork fallback batcher never consults the BatchAuthenticator (the mock rejects unregistered calls, and a consult would flip the result).
  • The contract mock dispatches per (contract address, method), so the tests also prove batcherHash is read from the contract systemConfig() returned — not merely that the method was called somewhere.

nextBlockRange reset/reorg/prune branches (espresso.go)

  • The safe-chain-reorg hash check — the sole defense against re-queueing orphaned blocks to Espresso — plus the caught-up, reversed-CurrentL1, derivation-ahead, safe-below-queue, gapped-queue, prune, and no-prune-boundary branches.

EnqueueBlocks reorg detection + clearState CAS handshake (espresso.go)

  • A reorged block must drop the queued chain and request a clear via the handshake, never perform it inline on the queueing loop — that inline clear was the earlier data race, and the requested-not-performed assertions are its regression test. The handshake's perform-once/coalescing semantics are locked separately.
  • The happy path runs end to end (fetch → parent-hash check → convert → sign → espresso job queue) using derivetest blocks and a fake ChainSigner, and the transient-fetch-failure path keeps the queue for the next tick.

evaluateVerification (espresso.go)

  • The full decision table: permanent-skip, ephemeral-retry, the HotShot block-count timeout (including its boundary and the zero-startHeight guard for a tracker with no height yet), the wall-clock safety backstop, and unclassified errors treated as retryable.
  • An end-to-end test with a client whose FetchLatestBlockHeight works — both existing mocks failed it, so startHeight stayed 0 and the re-submission path had never executed. The test drives a stuck transaction through the tracker, the worker's startHeight snapshot, and back onto the submission queue.

rollbackFailedStart / waitForLocalSafeHead (espresso_driver.go)

  • rollbackFailedStart clears the running flag and cancels both contexts, with and without a constructed streamer.
  • waitForLocalSafeHead covers the caffeination gate: anchoring at/past the point, retrying through transient sync-status failures and empty local-safe heads, the give-up error naming the height, and the zero-height (flag unset) semantics.

Production change

One, minimal: waitForLocalSafeHead hardcoded its one-minute anchor timeout and one-second retry interval, so its retry/give-up paths were untestable as written (as noted in the issue). The timeout and retry interval are now parameters, passed as the same constants at the single production call site — behavior unchanged, and the tests exercise exactly the function production runs.

Testing

  • go test ./op-batcher/... -count=1 — all green.
  • New tests run under -race (repeated runs on the timing-sensitive ones for flake-checking).
  • gofmt / go vet clean.

🤖 Generated with Claude Code

Covers the untested logic listed in #496:

- The active-batcher gate: every quadrant of the isBatcherActive mode
  check (an inverted check compiles and passes CI), the identity gate for
  both modes, the batcherHash low-20-byte slicing, and every
  shouldSkipPublishForActiveSeq path including the fail-closed error
  paths and the pre-fork fallback batcher never touching the contract.
- nextBlockRange's reset/reorg/prune branches, in particular the
  safe-chain-reorg hash check that is the sole defense against
  re-queueing orphaned blocks to Espresso.
- EnqueueBlocks reorg detection (reset must be requested via the
  clearState handshake, never performed inline) and the
  requestClearState/performClearState CAS handshake, with a concurrent
  regression test for the earlier data-race fix that runs under -race.
- evaluateVerification's decision table: the HotShot block-count timeout
  with its zero-startHeight guard, the wall-clock safety backstop, and
  an end-to-end re-submission test where FetchLatestBlockHeight works,
  so the timeout path is exercised through the whole worker pipeline.
- rollbackFailedStart and waitForLocalSafeHead. The latter hardcoded its
  one-minute anchor timeout, so the timing is now injected through
  waitForLocalSafeHeadWithTiming (production behavior unchanged) to make
  the retry and give-up paths testable, covering the caffeination gate.

Closes #496.

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

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

Review pass over the tests added for #496:

- Dispatch the contract mock per (address, method) instead of selector
  alone: the two ABIs share several selectors (owner, version, ...), so
  the old table's uniqueness premise was false, and address keying makes
  the tests prove batcherHash is read from the contract systemConfig()
  returned, not merely called somewhere. Handlers now receive the
  resolved abi.Method, which removes the duplicated selector plumbing
  and the double-naming in every table entry.
- Reuse eth.AddressAsLeftPaddedHash instead of a local addressAsBytes32.
- Fold waitForLocalSafeHeadWithTiming back into waitForLocalSafeHead
  with the timing as parameters: one function, and the tests exercise
  exactly what production runs.
- Replace the clearState handshake's goroutine choreography with a
  coalescing check: its bounds could not fail (atomics are treated as
  synchronization by the race detector), and the actual data-race
  regression is locked by TestEnqueueBlocksReorgDetection's
  requested-not-performed assertions; the comment now says so.
- Drop derivable state from stuckReceiptEspressoClient, a redundant
  context wrapper, and a redundant nil L1Client override.

Co-Authored-By: Claude Fable 5 <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.

1 participant