Skip to content

feat(exit-certificate): split Step G into G1/G2 with off-chain LER + shadow-fork verification#1633

Merged
joanestebanr merged 28 commits into
feature/exit-certificate-toolfrom
feat/exit_certificate_f15_deposit_order
Jun 9, 2026
Merged

feat(exit-certificate): split Step G into G1/G2 with off-chain LER + shadow-fork verification#1633
joanestebanr merged 28 commits into
feature/exit-certificate-toolfrom
feat/exit_certificate_f15_deposit_order

Conversation

@joanestebanr

@joanestebanr joanestebanr commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

🔄 Changes Summary

Speeds up and reworks Step G (NewLocalExitRoot), splitting it into G1 and G2 and adding an off-chain computation path.

Step G split

  • G1 (step_g1.go): lite-syncs the L2 bridge history from genesis up to the target block into a persistent lite DB, using the new bridgesyncerlite package (reads BridgeEvent logs in parallel and builds a bridge exit tree byte-for-byte compatible with bridgesync). Resolves the shadow-fork block.
  • G2 (step_g2.go, formerly step_g.go): computes NewLocalExitRoot.
    • Default (verifyNewLocalExitRootUsingShadowFork=true): spins up the Anvil shadow-fork, replays every bridge exit in parallel (send/collect pipeline), reorders the certificate to the on-chain deposit order, and verifies the lite exit tree root against the contract's getRoot().
    • Off-chain (=false): computes the root purely from the lite tree (G1 bridges + the certificate's exits, in order) — no Anvil.

Step I always uses the reordered certificate

  • In single-step mode, Step I now always reads step-g-reordered-certificate.json (run Step G first) instead of falling back to the capped/Step-E certificates, so the final certificate always matches the computed NewLocalExitRoot. (runAll already flowed the in-memory reordered cert.)

Removed

  • options.depositOrderSource (the events/bridgesync modes) and the production-bridgesync recovery (step_g_bridgesync.go). Deposit order now comes from the replay's BridgeEvents (shadow-fork) or the certificate order (off-chain). StepGResult.ShadowForkFirstBlock dropped.

New bridgesyncerlite package

  • Minimal bridge syncer: parallel eth_getLogs, persists BridgeEvent leaves and builds the exit tree. Supports a DB-only mode (no RPC) so G2 can insert pre-collected leaves and build the tree without touching Anvil. Aborts on events that invalidate a BridgeEvent-only reconstruction (SetSovereignTokenAddress, MigrateLegacyToken, RemoveLegacySovereignTokenAddress, BackwardLET, ForwardLET) unless ignoreUnsupportedL2Events is set.

⚠️ On mainnet Step G replays ~915k bridge exits; the previous serial execution took ~4 days (~2.8 bridges/s). The parallel replay + off-chain option address this.

⚠️ Breaking Changes

  • 🛠️ Config: exitAddress is now mandatoryLoadConfig errors when it is missing or set to the zero address (0x00…00). Configs that previously omitted it (it defaulted to the zero address) now fail. SC-locked value is bridged to this address and can only be recovered by signing from an address whose private key the operator controls.
  • 🛠️ Config: option renames (to the ignore* convention) — abortOnGenesisBalanceignoreGenesisBalance (polarity inverted: default false = abort), continueOnTraceErrorignoreOnTraceError, continueIfBalanceMismatchignoreBalanceMismatch.
  • 🗑️ Deprecated Features: removed options.depositOrderSource; removed the config-examples/ .json variants (converted to .toml).

📋 Config Updates

Config accepts JSON or TOML

  • LoadConfig selects the format by file extension: .toml is parsed as TOML, anything else (.json/no extension) as JSON. TOML is normalized to JSON internally (tomlToJSON) so both formats share one parsing/validation path, including signerConfig (json.RawMessage) and agglayerClient. Field names are identical in both formats.
  • Added parameters.toml.example (each field commented with its description + default) and converted the config-examples/ to TOML (zkevm-cardona.toml, zkevm-mainnet.toml); removed the .json variants. .gitignore now also ignores parameters.toml.

exitAddress validation

  • LoadConfig now rejects a missing or zero-address exitAddress. Docs/examples updated (the field was previously documented as optional, defaulting to the zero address) and exitAddress ships commented-out in the example configs so the operator must set their own.

New options

  • options.verifyNewLocalExitRootUsingShadowForktrue (default). true verifies the LER on the Anvil shadow-fork (requires Anvil); false computes it off-chain from the lite tree (no Anvil, trusts off-chain leaf encoding/metadata).
  • options.ignoreUnsupportedL2Eventsfalse (default). Downgrades the lite syncer's abort on unsupported events to a warning.

Renamed options (to the ignore* convention)

  • abortOnGenesisBalanceignoreGenesisBalance (polarity inverted: default false = abort)
  • continueOnTraceErrorignoreOnTraceError
  • continueIfBalanceMismatchignoreBalanceMismatch

Removed

  • options.depositOrderSource.

✅ Testing

  • 🤖 Automatic: go test ./tools/exit_certificate/... passes (incl. bridgesyncerlite, step_g_order_test.go, and config_test.go with the new TestLoadConfig_MissingExitAddress / TestLoadConfig_ZeroExitAddress). go build, go vet, gofmt, and golangci-lint clean.
  • 🖱️ Manual: run --step g (G1+G2) and Step I; confirm step-g-new-local-exit-root.json + step-g-reordered-certificate.json are produced and the lite tree root matches the contract getRoot() in verify mode.
  • 🌐 Mainnet: the off-chain computation was tested on mainnet against the shadow-fork and both produced the same LocalExitRoot (shadow-fork verification took 13.5h, with a total of 975,646 bridges generated).

🐞 Issues

📝 Notes

  • --step g runs G1+G2; g1/g2 run individually; g expands to g1,g2 in ranges.
  • Anvil (Foundry) is required only in the default shadow-fork verification mode.
  • Targets feature/exit-certificate-tool (the exit-certificate integration branch), not develop.
  • Step G1 ETA refinement: the fetch-progress ETA now measures throughput over a trailing time window instead of the lifetime average, so it is not skewed by the fast empty low-block windows at the start.

🤖 Generated with Claude Code

…l Step G replay

The parallel Anvil shadow-fork replay in Step G emits BridgeEvents in a
non-deterministic order, so the certificate's BridgeExits no longer match the
exit-tree deposit order. Recover the canonical order from the shadow-fork and
reorder BridgeExits to match the computed NewLocalExitRoot.

- Add Options.DepositOrderSource ("events" default, or "bridgesync") to select
  how the order is recovered, with validation in LoadConfig.
- step_g_events.go: read BridgeEvent logs directly from the replayed fork blocks
  (lightweight, no full L2 sync).
- step_g_bridgesync.go: reuse the production bridgesync component as an
  alternative source.
- step_g_order.go: shared reorder logic keyed by DepositCount.
- Expose StepGResult.ShadowForkFirstBlock so later steps can scan from the
  fork head.
- Persist step-g-reordered-certificate.json (single + all-steps modes) and have
  Step I prefer it over the pre-G ordering.
- Silence expected context.Canceled noise when cancelling the worker pool after
  a real failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr joanestebanr changed the title feat(exit-certificate): recover canonical deposit order after parallel Step G replay feat(exit-certificate): speed up step G (NewLocalExitRoot) executing in parallel Jun 2, 2026
…optional shadow-fork verification

Reworks Step G (NewLocalExitRoot) and renames several options.

Step G split:
- G1 (step_g1.go): lite-syncs the L2 bridge history from genesis up to the
  target block into a persistent lite DB (new bridgesyncerlite package, which
  reads BridgeEvent logs in parallel and builds a bridge exit tree byte-for-byte
  compatible with bridgesync). Resolves the shadow-fork block.
- G2 (step_g2.go, formerly step_g.go): computes NewLocalExitRoot. By default
  (verifyNewLocalExitRootUsingShadowFork=true) it spins up the Anvil shadow-fork,
  replays every bridge exit, reorders the certificate to the on-chain deposit
  order, and verifies the lite exit tree root against the contract's getRoot().
  With the option false it computes the root off-chain from the lite tree
  (G1 bridges + certificate exits) without Anvil.

Removed options.depositOrderSource (events/bridgesync modes) and the production
bridgesync-based recovery (step_g_bridgesync.go deleted); deposit order now comes
from the replay's BridgeEvents (shadow-fork) or the certificate order (off-chain).

New option:
- options.verifyNewLocalExitRootUsingShadowFork (default true).

Renamed options to the ignore* convention:
- abortOnGenesisBalance      -> ignoreGenesisBalance      (polarity inverted)
- continueOnTraceError       -> ignoreOnTraceError
- continueIfBalanceMismatch  -> ignoreBalanceMismatch
- (lite syncer) allowUnsupportedEvents -> ignoreUnsupportedL2Events

Lite DB files renamed to step-g1-/step-g- prefixes; G2 works on a copy so G1's
DB stays intact. Tests, docs (CLAUDE.md, README), config examples and scripts
updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr joanestebanr changed the title feat(exit-certificate): speed up step G (NewLocalExitRoot) executing in parallel feat(exit-certificate): split Step G into G1/G2 with off-chain LER + shadow-fork verification Jun 3, 2026
@joanestebanr joanestebanr marked this pull request as draft June 4, 2026 08:49
joanestebanr and others added 2 commits June 4, 2026 11:19
…d of aborting

A receipt timeout during the shadow-fork replay (block not mined within the
poll window, typically a slow remote fork backend under load) previously
fail-fasted the whole Step G2 run. Now such exits are deferred and recovered
after the send/collect phase drains.

- Bump receiptPollTimeout 120s -> 300s.
- Mark timeouts with the errReceiptTimeout sentinel so collectors can tell them
  apart from reverts/hard RPC errors (which still abort).
- Defer timed-out exits to a list instead of aborting; retry them once Anvil is
  idle via retryDeferredExit.
- retryDeferredExit loops unbounded until the exit mines: re-poll the current tx
  first, and only re-send the bridgeAsset if the receipt is still absent. The
  re-poll-before-resend ordering prevents double-counting a tx that did mine
  (which would diverge the reconstructed tree from the contract's getRoot).
  Exits only on success, a revert, or context cancellation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es reordered cert

Config files can now be JSON or TOML, selected by file extension (.toml parsed
as TOML, anything else as JSON). TOML is normalized to JSON internally so both
formats share one parsing/validation path, including signerConfig and
agglayerClient. Added parameters.toml.example (commented) and converted the
config-examples to TOML.

Step I (single-step mode) now always reads step-g-reordered-certificate.json
instead of falling back to the capped/Step-E certificates, so the final
certificate always matches the computed NewLocalExitRoot.

Also refines the Step G1 fetch-progress ETA to measure throughput over a
trailing window rather than the lifetime average.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr joanestebanr self-assigned this Jun 4, 2026
joanestebanr and others added 2 commits June 4, 2026 16:50
…o address

LoadConfig now errors when exitAddress is missing or set to the zero
address (0x00..00), since SC-locked funds can only be recovered by
signing from an address whose private key the operator controls.

Adds TestLoadConfig_MissingExitAddress and TestLoadConfig_ZeroExitAddress,
and updates the existing config tests/fixtures and the docs
(README, CLAUDE.md, config-examples, parameters.toml.example) to mark
exitAddress (and l2RpcUrl/l2BridgeAddress in the examples) as required.

Closes agglayer/pm#348

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that exitAddress is mandatory and rejects the zero address, the
example configs ship it commented out so the operator must uncomment
and set an address whose private key they control before running.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@agglayer agglayer deleted a comment from claude Bot Jun 4, 2026
@joanestebanr joanestebanr added the exit_certificate_tool Tool to create a final exit certificate label Jun 4, 2026
- errcheck: explicitly discard the failFast return value at the three
  call sites that intentionally continue/break (it already records the
  replay error and cancels the context).
- lll: wrap the replay-start log line under 120 chars.
- mnd: extract the uint256 bit width (256) into a named constant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr

Copy link
Copy Markdown
Collaborator Author

@claude

@joanestebanr joanestebanr requested a review from Copilot June 4, 2026 15:28
@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown

Claude finished @joanestebanr's task in 2m 14s —— View job


PR Review Complete ✅

Reviewed PR #1633: Split Step G into G1/G2 with off-chain LER + shadow-fork verification


🚀 What's New

This PR significantly reworks Step G by splitting it into G1 (lite-sync L2 bridge history) and G2 (compute NewLocalExitRoot), adds a new bridgesyncerlite package for parallel bridge event fetching, introduces an off-chain computation path for faster LER calculation, and makes exitAddress mandatory with validation. The config system now supports both JSON and TOML formats.

🐛 Bug Fixes

None

📋 Config Updates

  • exitAddress - Now MANDATORY (was optional). Rejects zero address. Must be an address whose private key the operator controls for SC-locked fund recovery.
  • verifyNewLocalExitRootUsingShadowFork - New option (default: true). When true, uses Anvil shadow-fork verification; when false, computes LER purely off-chain (faster, no Anvil required).
  • ignoreUnsupportedL2Events - New option (default: false). Downgrades abort on unsupported L2 events to warnings.
  • Renamed options (polarity changes):
    • abortOnGenesisBalanceignoreGenesisBalance (default: false = abort)
    • continueOnTraceErrorignoreOnTraceError
    • continueIfBalanceMismatchignoreBalanceMismatch
  • Removed: options.depositOrderSource

⚠️ Breaking Changes

  1. exitAddress is now mandatory - Config loading fails if missing or set to zero address
  2. Config option renames with polarity inversions - See above
  3. Removed depositOrderSource feature - Deposit order now comes from replay or certificate order

🔍 Review Feedback

🟢 Strengths

  1. Excellent Architecture - The G1/G2 split is well-designed: G1 handles the expensive full-history sync once, G2 can be re-run quickly
  2. Performance Focus - Parallel bridge fetching with configurable concurrency, send/collect pipeline for Anvil replay, and off-chain computation path address the mainnet scale problem (~915k exits)
  3. Robust Error Handling - Receipt timeout retry mechanism in retryDeferredExit (step_g2.go:596-632) prevents false failures from slow fork backends
  4. Test Coverage - Good unit test coverage including TestHashMatchesBridgesync ensuring byte-for-byte compatibility, config validation tests for exitAddress, and deposit order reordering tests
  5. Documentation - Comprehensive inline comments explaining complex logic (e.g., ETA calculation over trailing window, send/collect pipeline rationale)

🔴 Required Issues

Security: Unchecked Error in Database Transaction - step_g1.go:155

The copyFile function in step_g1.go:141-156 closes the output file but doesn't properly check/handle the out.Close() error when io.Copy fails:

if _, err := io.Copy(out, in); err != nil {
    out.Close()  // ⚠️ Close error is silently ignored
    return err
}
return out.Close()

If the copy fails and the subsequent close also fails (e.g., disk full), you lose the close error. The defer pattern would be safer:

func copyFile(src, dst string) error {
    in, err := os.Open(src)
    if err != nil {
        return err
    }
    defer in.Close()
    out, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer func() {
        if cerr := out.Close(); cerr != nil && err == nil {
            err = cerr
        }
    }()
    _, err = io.Copy(out, in)
    return err
}

Reference: tools/exit_certificate/step_g1.go:141-156


🟡 Suggested Improvements

1. Database Transaction Rollback in syncer.go - bridgesyncerlite/syncer.go:209-216, 255-262

Both StoreBridges and BuildTree have identical deferred rollback patterns. The rollback error logging is helpful, but the pattern is repeated. Consider extracting to a helper:

func (s *BridgeSyncerLite) withTx(ctx context.Context, fn func(*sql.Tx) error) error {
    tx, err := db.NewTx(ctx, s.db)
    if err != nil {
        return fmt.Errorf("begin transaction: %w", err)
    }
    committed := false
    defer func() {
        if !committed {
            if rerr := tx.Rollback(); rerr != nil {
                s.log.Errorf("rollback failed: %v", rerr)
            }
        }
    }()
    if err := fn(tx); err != nil {
        return err
    }
    if err := tx.Commit(); err != nil {
        return fmt.Errorf("commit transaction: %w", err)
    }
    committed = true
    return nil
}

References:

  • tools/exit_certificate/bridgesyncerlite/syncer.go:205-232
  • tools/exit_certificate/bridgesyncerlite/syncer.go:251-287

2. Magic Numbers in Constants - step_g2.go:28-86

Several timeouts and configuration values are well-documented but could benefit from clearer naming or comments about their derivation:

  • receiptPollTimeout = 300 * time.Second (5 minutes) - Very generous, but mainnet scale justifies it
  • replayInFlightWindow = 2000 - Good value, but document why 2000 specifically (block size? memory? empirical testing?)
  • anvilTxGasLimit = "0x4c4b40" (5M) - Comment says ~300k for bridgeAsset, so 5M is ~16x. Document this safety margin.

The comments are excellent, but adding brief justifications for the specific values would help future tuning.

Reference: tools/exit_certificate/step_g2.go:28-86

3. Error Message Context in downloader.go - bridgesyncerlite/downloader.go:208-212

The forbidden event detection provides good detail, but could benefit from actionable guidance:

return nil, fmt.Errorf("unsupported %s event detected at block %d (tx %s, log index %d): "+
    "bridge state cannot be reconstructed from BridgeEvent logs alone. "+
    "Set ignoreUnsupportedL2Events=true to skip this event (may produce incorrect NewLocalExitRoot), "+
    "or use the full bridgesync path if available",
    name, l.BlockNumber, l.TxHash.Hex(), l.Index)

Reference: tools/exit_certificate/bridgesyncerlite/downloader.go:208-212

4. Config Validation Order - config.go:139-151

The exitAddress validation comes after checking l2RpcUrl and l2BridgeAddress. Consider validating all mandatory fields first, then semantic checks (zero address), so users get comprehensive error messages:

var missing []string
if raw.L2RPCURL == "" {
    missing = append(missing, "l2RpcUrl")
}
if raw.L2BridgeAddress == "" {
    missing = append(missing, "l2BridgeAddress")
}
if raw.ExitAddress == "" {
    missing = append(missing, "exitAddress")
}
if len(missing) > 0 {
    return nil, fmt.Errorf("missing required parameters: %s", strings.Join(missing, ", "))
}

Reference: tools/exit_certificate/config.go:139-151

5. Potential Race in Progress Logging - step_g2.go:466-478

The maybeLogProgress function uses atomic operations, but the pattern mixing atomic.AddInt64 with atomic.LoadInt64 and atomic.CompareAndSwapInt64 could be clearer. The logic is correct, but the lastLogNanos updates could race in edge cases where multiple goroutines hit the replayLogMaxGap threshold simultaneously. Not a critical bug (just extra log lines), but worth noting.

Reference: tools/exit_certificate/step_g2.go:466-478


🟢 Optional Observations

  1. ETA Calculation Refinement - The trailing window ETA (bridgesyncerlite/downloader.go:114-177) is a clever solution to the "fast empty start, slow dense tail" problem. Nice work!

  2. Deferred Exit Retry Logic - The retryDeferredExit unbounded retry with re-poll-before-resend ordering (step_g2.go:596-632) correctly prevents double-counting exits. The comment explaining why this ordering matters is excellent.

  3. Parallel Testing - All tests use t.Parallel(), which is great for CI performance.

  4. Fail-Fast Pattern - The failFast closure with sync.Once in replayBridgeExits (step_g2.go:449-457) correctly captures the first error and cancels context to stop all workers.


📝 Summary

This is a high-quality PR that addresses a critical performance bottleneck (mainnet's ~915k bridge exits). The architecture is sound, error handling is robust, and the code is well-tested and documented.

Action Items

  1. 🔴 Fix the copyFile error handling (step_g1.go:141-156) - Required before merge
  2. 🟡 Consider the suggested improvements for code quality and maintainability

The breaking changes (mandatory exitAddress, config renames) are well-documented in the PR description. The off-chain computation path is a significant optimization while maintaining correctness through optional shadow-fork verification.

Recommendation: Approve with the required fix to copyFile error handling.


| Branch: feat/exit_certificate_f15_deposit_order

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the exit-certificate tool’s Step G (NewLocalExitRoot computation) into a two-phase pipeline (G1/G2) to improve performance and optionally allow an off-chain computation path, while also updating configuration handling (including TOML support) and renaming several options to an ignore* convention.

Changes:

  • Split Step G into Step G1 (lite-sync L2 bridge history into a persistent DB) and Step G2 (compute/verify NewLocalExitRoot, reorder certificate by deposit order, and persist reordered output).
  • Add bridgesyncerlite (parallel eth_getLogs sync + persisted leaves + exit-tree build) and wire it into Step G1/G2.
  • Update config parsing/validation: TOML support via .toml extension, make exitAddress required/non-zero, rename multiple options (abortOnGenesisBalanceignoreGenesisBalance, etc.).

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tools/exit_certificate/worker.go Suppress noisy context.Canceled logging from worker pools after fail-fast cancellation.
tools/exit_certificate/types.go Update Step F comment for renamed option; add StepG1Result.
tools/exit_certificate/step_g1.go New Step G1: lite-sync L2 bridge history into a persistent sqlite DB and emit shadow-fork block info.
tools/exit_certificate/step_g2.go New Step G2: compute NewLocalExitRoot via shadow-fork replay (default) or off-chain lite-tree path; includes replay pipeline + verification.
tools/exit_certificate/step_g.go Remove legacy monolithic Step G implementation (replaced by G1/G2).
tools/exit_certificate/step_g_order.go New helper to reorder certificate exits by replayed depositCount.
tools/exit_certificate/step_g_order_test.go Tests for certificate reordering by deposit count.
tools/exit_certificate/step_g_events.go Off-chain lite-tree construction from certificate exits; DB-copy + replayed leaf insertion + tree build.
tools/exit_certificate/step_f.go Rename mismatch-continue option and update messaging accordingly.
tools/exit_certificate/step_f_test.go Update tests for renamed Step F option.
tools/exit_certificate/step_b.go Apply ignoreGenesisBalance semantics (polarity inversion) in Step B1.
tools/exit_certificate/step_a.go Apply ignoreOnTraceError option rename in Step A1.
tools/exit_certificate/step_a_test.go Update tests/docs for renamed trace-error option.
tools/exit_certificate/scripts/reproduce_sc_locked.sh Update config option naming and Step G file references.
tools/exit_certificate/scripts/configuration_based_on_kurtosis.sh Update config option naming and include new ignore flags.
tools/exit_certificate/run.go Add g1/g2 steps, g alias expansion, and persist reordered certificate output for Step I.
tools/exit_certificate/run_test.go Update step-range parsing tests for g1/g2 and g alias behavior.
tools/exit_certificate/README.md Update docs for JSON/TOML config, new Step G split, option renames, and exitAddress validation (but still needs alignment with Step I/G outputs).
tools/exit_certificate/parameters.toml.example New TOML example config with annotated fields and defaults.
tools/exit_certificate/parameters.json.example Update JSON example for renamed/new options.
tools/exit_certificate/config.go Add TOML support, enforce exitAddress required/non-zero, add new options, rename ignore flags.
tools/exit_certificate/config-examples/zkevm-mainnet.toml Convert mainnet example config to TOML and update required fields/options.
tools/exit_certificate/config-examples/zkevm-mainnet.json Remove JSON example variant (replaced by TOML).
tools/exit_certificate/config-examples/zkevm-cardona.toml Convert Cardona example config to TOML and update required fields/options.
tools/exit_certificate/config-examples/zkevm-cardona.json Remove JSON example variant (replaced by TOML).
tools/exit_certificate/config-examples/README.md Update examples README for TOML focus and new required fields.
tools/exit_certificate/config_test.go Add tests for exitAddress validation and TOML parsing/round-tripping of signer config.
tools/exit_certificate/cmd/main.go Update CLI --config help text to reflect JSON/TOML support.
tools/exit_certificate/CLAUDE.md Update internal docs for Step G split, option renames, and TOML support.
tools/exit_certificate/bridgesyncerlite/types.go New lite syncer types + leaf hashing compatible with canonical bridgesync tree.
tools/exit_certificate/bridgesyncerlite/syncer.go New lite syncer core: persist leaves, build exit tree, DB-only mode support.
tools/exit_certificate/bridgesyncerlite/syncer_test.go Tests for hashing compatibility, persistence, and tree build correctness.
tools/exit_certificate/bridgesyncerlite/migrations.go DB schema/migrations for lite syncer bridge table + shared tree tables.
tools/exit_certificate/bridgesyncerlite/downloader.go Parallel log downloader with forbidden-event detection and trailing-window ETA.
tools/exit_certificate/.gitignore Ignore parameters.toml in addition to JSON and output artifacts.

Comment thread tools/exit_certificate/step_g2.go Outdated
Comment thread tools/exit_certificate/step_g1.go Outdated
Comment thread tools/exit_certificate/step_g_events.go Outdated
Comment thread tools/exit_certificate/config.go
Comment thread tools/exit_certificate/README.md Outdated
Comment thread tools/exit_certificate/README.md Outdated
Comment thread tools/exit_certificate/README.md Outdated
joanestebanr and others added 5 commits June 4, 2026 17:42
The doc comment described the off-chain (no-Anvil) path as the default,
but defaultOptions sets verifyNewLocalExitRootUsingShadowFork=true, so
the default is the Anvil shadow-fork verification path. Swap the
description so the documented default matches the dispatch in the code.

Addresses PR #1633 review comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
liteForkNextDepositCount loaded the entire genesis→fork bridge history
into memory (GetBridges) just to read the last leaf's deposit count,
an avoidable O(n) read/alloc that dominated off-chain mode startup on
mainnet-scale histories.

Add BridgeSyncerLite.NextDepositCount, which runs a single
MAX(deposit_count) aggregate query (O(1)), and use it instead. Adds
TestNextDepositCount.

Addresses PR #1633 review comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p G/I docs

- config: reject a malformed exitAddress with a clear "not a valid hex
  address" error via common.IsHexAddress, instead of letting
  common.HexToAddress silently coerce it to the zero address and surface
  the misleading zero-address error. Adds TestLoadConfig_InvalidExitAddress.
- README: Step G now documents the G1/G2 outputs (shadow-fork block,
  lite DBs, reordered certificate); Step I documents that it always
  reads step-g-reordered-certificate.json with no Step E/F fallback.

Addresses PR #1633 review comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
syncLiteToBlock loaded every persisted bridge into memory via
GetBridges just to log the count — hundreds of thousands of leaf
allocations on mainnet-scale runs for no functional benefit.

Add BridgeSyncerLite.CountBridges (single COUNT(*) query, O(1)) and use
it for the log line. Adds TestCountBridges.

Addresses PR #1633 review comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d return

copyFile dropped out.Close() errors when io.Copy succeeded but the
deferred flush failed (e.g. a full disk), so a partially-written lite DB
copy could be reported as success. Switch to a deferred close with a
named return that surfaces the close error when the copy itself
succeeded (a copy error still takes precedence).

Addresses the required issue from the PR #1633 automated review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr

Copy link
Copy Markdown
Collaborator Author

Addressed the 🔴 Required Issue (copyFile error handling, step_g1.go) in 84969cc.

copyFile now uses a deferred out.Close() with a named return: the close error is surfaced when the copy succeeded but the flush failed (e.g. full disk), while a copy error still takes precedence. This prevents a partially-written lite DB copy from being reported as success.

The 🟡 suggested improvements (tx helper extraction, constant justifications, richer forbidden-event error, batched config validation, progress-logging atomics) are noted; happy to fold in any you consider blocking — otherwise I'll leave them as follow-ups to keep this PR focused.

maybeLogProgress mixed atomic Add/Store/Load/CAS on the completed
counter and last-log timestamp; an interval-milestone Store could
interleave with the gap-based CAS and emit a duplicate progress line.
Replace the atomics with a single mutex that guards the counter and the
timestamp as a unit, collapsing the two branches into one condition. The
lock is uncontended (work between calls is a receipt fetch), so it is
not on a hot path.

Addresses suggested improvement #5 from the PR #1633 automated review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr

Copy link
Copy Markdown
Collaborator Author

Addressed 🟡 suggested improvement #5 (Potential Race in Progress Logging) in 8d60595.

maybeLogProgress previously mixed atomic.AddInt64 on the counter with a plain atomic.StoreInt64 (interval-milestone branch) and a CompareAndSwapInt64 (gap branch) on lastLogNanos. As the review noted, a milestone Store could interleave with the gap CAS and emit a duplicate progress line.

Replaced the atomics with a single sync.Mutex guarding the counter and the last-log timestamp as a unit, collapsing the two branches into one condition. The lock is uncontended in practice (the work between calls is a receipt fetch), so it is not on a hot path. sync/atomic is no longer needed in the file.

@joanestebanr joanestebanr marked this pull request as ready for review June 5, 2026 09:00
@joanestebanr joanestebanr requested a review from web3security June 8, 2026 07:08
joanestebanr and others added 2 commits June 8, 2026 10:49
… tools/ from Sonar

Add RPC-backed tests (fake JSON-RPC server), parseBridgeEvent/classifyLogs
unit tests and error-branch coverage, raising bridgesyncerlite from 41.2%
to 84.6%. Remove tools/** from sonar.exclusions so the tool's coverage is
actually measured instead of reported as 0.0%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ff-chain LER path

Add unit tests for runWorkerPool, the pure Step G2 helpers (event log
parsing, revert decoding, transient-error classification, failed-exit
persistence) and the off-chain NewLocalExitRoot path (buildLiteTreeFrom-
Certificate, buildLiteTreeWithReplayed, lite DB copy/remove helpers and
RunStepG2 dispatch) using an in-process JSON-RPC stub. Raises the main
package coverage from 31.0% to 37.1%; the remaining gaps are the Anvil
shadow-fork and RPC-driven steps that need live infrastructure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
joanestebanr and others added 10 commits June 8, 2026 16:16
Cover the pure Step B helpers (filterEOAs, padLeft, sumBalances,
isEOAResult, unmarshalHexBigInt, buildEOABalances/buildSingleEOABalance,
buildAccumulated) and the RPC fan-out functions (classifyAddresses,
fetchETHBalances, fetchAllTokenBalances) plus the RunStepB/RunStepB1
orchestration and the genesis-preload guard, driven by an in-process
batch JSON-RPC stub. step_b.go functions now sit at 75-100%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract recordProgressTick from reportFetchProgress so the per-tick ETA
math is unit-testable without waiting for the real ticker, and add tests
for it plus Step G1 helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p G2 and test the replay

Introduce forkBackend (Anvil RPC ops: read LER, resolve token, set balance,
prepare ERC-20, send bridgeAsset, poll receipt) and forkLauncher (Anvil
process spawn) interfaces, with anvilForkBackend/anvilLauncher as the
production implementations delegating to the existing helpers. The replay
orchestration (replayBridgeExits, retryDeferredExit, resolveTokenAddresses,
runStepG2ShadowFork) now depends on these seams instead of a raw anvil URL,
so a mock backend/launcher drives them in unit tests without Anvil or a live
node. Covers the send/collect pipeline, fail-fast on send error/revert/
missing BridgeEvent, the receipt-timeout defer-and-retry path, token
resolution (L2-native, LBT, contract fallback) and the root verification /
mismatch branches.

replayBridgeExits 0%->92%, retryDeferredExit 0%->100%, resolveTokenAddresses
0%->96%, runStepG2ShadowFork 0%->79%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive checkL2NetworkID, checkNativeGasToken, checkContractPrereqs and
RunStepCheck against an in-process JSON-RPC stub that ABI-encodes contract
return values by selector. Covers network-id match/mismatch, gas-token
present/absent, PP vs FEP, threshold and bridge-address mismatches, the
legacy-diagnostics fallback and the combined-failure aggregation. step_check.go
0% -> ~70%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add tests for the L1 deposit scan (fetchL1BridgeEvents/fetchBridgeEventsInRange),
isClaimed batching (checkClaimedBatch), L1 head resolution, token name/decimals
lookup, the unclaimed-asset summary and the bridge-service cross-check
(fetchZkevm/fetchAggkitPendingBridges, checkBridgeServicePendingBridges) using
httptest + an in-process JSON-RPC stub. Extend newBatchRPCServer to also answer
single (non-batched) requests so singleRPC callers can use it. step_e.go
~30% -> ~70%; package 46.9% -> 55.0%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a security recommendation to use a multisig (e.g. Gnosis Safe)
instead of a single EOA for exitAddress, since SC-locked funds can only
ever be recovered by signing from that address. Applied to the tool
README, config-examples README, and the example config comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive RunStep0 end-to-end against an in-process JSON-RPC stub (NewWrappedToken
log scan, SetSovereignTokenAddress overrides, totalSupply batch, native-balance
diff, gas-token and WETH lookups) plus unit tests for the event decoders,
constant target-block resolution and the empty-supplies path. step_0.go
~7% -> ~85%; package 55% -> ~59%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- step_g2: comment the intentionally-empty send-pool collector
- syncer.New: extract openDatabase/dialBridge to cut cognitive complexity
- LoadConfig: split into readRawConfig/validateRawConfig/buildConfig
- mergeOptions: split into mergeScalarOptions/mergeFlagOptions/mergeAgglayerClient
- centralize duplicated output filenames into filenames.go constants

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…const

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@web3security web3security 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.

LGTM

joanestebanr and others added 3 commits June 9, 2026 11:20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hestrators

Add httptest JSON-RPC stub tests for the anvilForkBackend RPC functions
(setSenderBalance, readLocalExitRoot, callGetTokenWrappedAddress,
sendAnvilTransaction, sendBridgeAssetTx, waitForReceipt, fetchRevertReason,
ensureERC20Balance, prepareERC20Token) and their backend wrappers.

Cover run.go single-step orchestrators: missing-input guards for
submit/wait/f/h/i/g1/g2/sign, the Step E l1RpcUrl guard, runSingleG2's
empty-certificate EmptyLER path, and resolveLatestBlock.

Package coverage 61.3% -> 66.4%. Remaining step_g2 gaps (startAnvil/
waitForAnvil/Start) require the real anvil binary (integration only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace fmt.Sprintf-built read queries (CountBridges, NextDepositCount,
GetBridges) with compile-time constant query strings so there is no runtime
SQL string formatting — the only interpolated token is the trusted
bridgeTableName constant. Clears the SonarCloud dynamic-SQL hotspot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr joanestebanr merged commit 4b96d43 into feature/exit-certificate-tool Jun 9, 2026
18 of 19 checks passed
@joanestebanr joanestebanr deleted the feat/exit_certificate_f15_deposit_order branch June 9, 2026 10:02
@sonarqubecloud

sonarqubecloud Bot commented Jun 9, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

exit_certificate_tool Tool to create a final exit certificate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants