Skip to content

feat: new tool - exit_certificate#1582

Open
krlosMata wants to merge 49 commits into
developfrom
feature/exit-certificate-tool
Open

feat: new tool - exit_certificate#1582
krlosMata wants to merge 49 commits into
developfrom
feature/exit-certificate-tool

Conversation

@krlosMata

@krlosMata krlosMata commented Apr 14, 2026

Copy link
Copy Markdown
Member

🔄 Changes Summary

New exit-certificate CLI tool under tools/exit_certificate/ that generates exit certificates for aggchain migration. It scans L2 state from genesis to a target block, discovers all addresses with value (ETH + wrapped tokens), computes smart-contract-locked balances, detects unclaimed L1→L2 bridge deposits, verifies balances against the agglayer, computes the new LocalExitRoot via a shadow-fork, and produces a fully signed agglayer Certificate ready for submission.

Full pipeline: CHECK → 0 → A → B → C → D → E → F → G → H → I → SIGN → SUBMIT → WAIT

  • Step CHECK — prerequisite verification (Anvil installed, L1 RPC reachable, L2 network ID, PP network type, threshold=1, no gas token)
  • Step 0 — LBT generation: scans NewWrappedToken / SetSovereignTokenAddress events, fetches totalSupply per token
  • Step A — address collection via debug_traceTransaction with prestateTracer+diffMode
  • Step B — EOA classification + ETH/ERC-20 balance scanning
  • Step C — SC-locked value computation (LBT total − EOA accumulated)
  • Step D — build agglayer Certificate with BridgeExit entries
  • Step E — unclaimed L1→L2 deposit detection; optional cross-check against bridge service REST API
  • Step F — agglayer token balance verification via admin_getTokenBalance; proportional capping on mismatch
  • Step GNewLocalExitRoot computation via Anvil shadow-fork of the L2 chain
  • Step H — fetch PreviousLocalExitRoot from agglayer via gRPC
  • Step I — assemble final certificate (NewLER + PrevLER + L1InfoTreeLeafCount from L1)
  • Step SIGN — ECDSA certificate signing via go_signer
  • Step SUBMIT — send certificate to agglayer via gRPC
  • Step WAIT — poll agglayer until the certificate is Settled or InError

Steps can be run individually or as a full pipeline via --step flag. Intermediate results are persisted to an output directory, enabling step-by-step execution and resumability.

Additional changes in this PR:

  • Bearer token auth support for agglayer admin API protected by Google Cloud IAP (options.agglayerAdminToken)
  • Unit test coverage for hex, rpc, config, step_f, and step_g utilities
  • Lint fixes across the repo (lll, mnd, gci, whitespace, goconst, gocritic, makezero, unparam, prealloc, gosec)

⚠️ Breaking Changes

  • None. This is a new standalone tool with no impact on existing aggkit components.

📋 Config Updates

  • 🧾 New config file: tools/exit_certificate/parameters.json (standalone JSON). Key fields:
{
    "l2RpcUrl": "https://your-l2-rpc.example.com",
    "l1RpcUrl": "https://your-l1-rpc.example.com",
    "l2BridgeAddress": "0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe",
    "l2NetworkId": 1,
    "targetBlock": "latest",
    "exitAddress": "0x0000000000000000000000000000000000000001",
    "destinationNetwork": 0,
    "sovereignRollupAddr": "0x...",
    "l1GlobalExitRootAddress": "0x...",
    "signerConfig": { "Method": "local", "Path": "keystore.json", "Password": "..." },
    "options": {
        "blockRange": 5000,
        "concurrencyLimit": 20,
        "rpcBatchSize": 200,
        "outputDir": "./output",
        "agglayerAdminURL": "https://admin-agglayer.example.com",
        "agglayerAdminToken": "<IAP bearer token>",
        "agglayerClient": { "GRPC": { "URL": "agglayer.example.com:50051", "UseTLS": true } }
    }
}

✅ Testing

  • 🤖 Automatic: unit tests in tools/exit_certificate/ (hex, rpc, config, step_c, step_f, step_g)
  • 🖱️ Manual: tested against spec/bali environments with real L2 chains

🐞 Issues

📝 Notes

  • Requires anvil (Foundry) in $PATH for Step G
  • Requires archive node for Step A (debug_traceTransaction)
  • FEP chains are not supported — only Pessimistic Proof certificates

@claude

claude Bot commented Apr 14, 2026

Copy link
Copy Markdown

Claude finished @krlosMata's task in 4m 37s —— View job


PR Review: exit_certificate Tool ✅


🚀 What's New

A new standalone CLI tool (exit-certificate) that generates exit certificates for aggchain migration. The tool scans L2 state from genesis to a target block, discovers all value (ETH + wrapped tokens), computes smart-contract-locked balances, detects unclaimed L1→L2 bridge deposits, and produces a standard agglayer Certificate with BridgeExit entries that bridge all value back to L1. Includes a 6-step resumable pipeline with ~1,315 lines of tests (34% of total code).

🐛 Bug Fixes

None.

📋 Config Updates

New config file: tools/exit_certificate/parameters.json (standalone JSON, not part of aggkit's main config).

Required fields:

  • l2RpcUrl (string): L2 JSON-RPC endpoint (must support debug_traceTransaction)
  • l2BridgeAddress (hex address): L2 bridge contract address
  • targetBlock (string): Target block number or "latest"

Optional fields:

  • l1RpcUrl (string): L1 JSON-RPC endpoint (required for Step E)
  • l1BridgeAddress (hex address): L1 bridge contract (defaults to l2BridgeAddress)
  • l2NetworkId (uint32): L2 network ID (default: 1)
  • exitAddress (hex address): Address for SC-locked value exits (default: zero address)
  • destinationNetwork (uint32): Destination network (default: 0)
  • lbtFile (string): Path to pre-generated LBT JSON file (optional)
  • options.blockRange (int): Block range per eth_getLogs (default: 5000)
  • options.concurrencyLimit (int): Max concurrent RPC requests (default: 20)
  • options.rpcBatchSize (int): Max calls per batch (default: 200)
  • options.rpcDelayMs (int): Delay between batches (default: 0)
  • options.outputDir (string): Output directory (default: "./output")
  • options.l1StartBlock (uint64): L1 scan start block (default: 0)

⚠️ Breaking Changes

None. This is a new standalone tool with no impact on existing aggkit components.


Code Quality Assessment

✅ Strengths

  1. Excellent Architecture: Clean 6-step pipeline with clear separation of concerns. Each step has well-defined inputs/outputs and can run independently.

  2. Strong Test Coverage: ~1,315 lines of tests (34% of codebase) with good unit and integration coverage. Tests use proper table-driven patterns and edge case validation.

  3. Resumability: Intermediate results persisted to disk enable step-by-step execution and recovery from failures.

  4. Performance-Conscious: Custom HTTP client with unlimited connections, worker pools with proper concurrency control, and batched RPC calls.

  5. Error Handling: Comprehensive error wrapping with context throughout. Retry logic with exponential backoff for RPC calls.

  6. Good Documentation: Detailed README with clear usage examples, config documentation, and pipeline explanations.


🔴 Required Issues

1. Security: Unchecked Big Number Conversions

Location: step_0.go:163, step_e.go:229-236, hex.go:10-25

The code uses unchecked type conversions from *big.Int to uint32/uint64 which can silently truncate values:

// step_0.go:163
OriginNetwork: uint32(new(big.Int).SetBytes(data[0:32]).Uint64())

// step_e.go:229
LeafType: uint8(new(big.Int).SetBytes(data[0:32]).Uint64())

// hex.go - no bounds checking
func hexToUint64(s string) uint64 {
    var n uint64
    for _, c := range s {
        n <<= 4  // Can overflow silently
        // ...
    }
}

Impact: If a malicious or corrupted event contains a value larger than uint32_MAX or uint64_MAX, it will be silently truncated, potentially causing incorrect certificate generation or missing funds.

Recommendation: Add overflow checks before conversions:

val := new(big.Int).SetBytes(data[0:32])
if val.Uint64() > math.MaxUint32 {
    return wrappedTokenEvent{}, fmt.Errorf("originNetwork value too large: %s", val)
}
originNetwork := uint32(val.Uint64())

Fix this →

2. Security: Unvalidated Array Bounds in Event Decoding

Location: step_0.go:156-166, step_e.go:209-239

Event decoding functions check minimum data length but don't validate upper bounds when accessing dynamic metadata:

// step_e.go:218-225
metadataOffset := new(big.Int).SetBytes(data[192:224]).Uint64()
var metadata []byte
if metadataOffset+32 <= uint64(len(data)) {  // Check offset+32
    metadataLen := new(big.Int).SetBytes(data[metadataOffset : metadataOffset+32]).Uint64()
    metadataStart := metadataOffset + 32
    if metadataStart+metadataLen <= uint64(len(data)) {  // Check length
        metadata = make([]byte, metadataLen)
        copy(metadata, data[metadataStart:metadataStart+metadataLen])  // But no max size limit!
    }
}

Impact: A malicious event with an extremely large metadataLen could cause excessive memory allocation (DoS) or integer overflow in the offset calculation.

Recommendation: Add maximum size limits:

const maxMetadataSize = 1024 * 1024 // 1MB
if metadataLen > maxMetadataSize {
    return L1Deposit{}, fmt.Errorf("metadata too large: %d bytes", metadataLen)
}

Fix this →

3. Bug: Potential Integer Overflow in Batch Size Calculation

Location: step_0.go:189

batchSize := max(len(calls)/concurrency, 1)

If len(calls) is very large and concurrency is 1, this could create an extremely large batch that exceeds RPC limits.

Recommendation: Cap batch size to the configured RPCBatchSize:

batchSize := min(max(len(calls)/concurrency, 1), cfg.Options.RPCBatchSize)

Fix this →

4. Bug: Step C Doesn't Handle Native Token (Zero Address) Correctly

Location: step_c.go:83-88

The native token has WrappedTokenAddress == common.Address{} (zero address), but the indexing function uses strings.ToLower(e.WrappedTokenAddress.Hex()) which converts it to "0x0000000000000000000000000000000000000000". This is fine, but Step C doesn't explicitly handle the native token case:

func indexByAddress(entries []LBTEntry) map[string]LBTEntry {
    m := make(map[string]LBTEntry, len(entries))
    for _, e := range entries {
        m[strings.ToLower(e.WrappedTokenAddress.Hex())] = e  // Zero address becomes "0x000...000"
    }
    return m
}

In step_d.go:54-56, there's special handling for native tokens, but Step C doesn't distinguish between native and wrapped tokens when computing SC-locked values.

Recommendation: Add explicit handling for the native token entry or add a comment explaining that native tokens are intentionally treated the same way.

5. Bug: Missing Validation for TargetBlock Format

Location: run.go:69-79

The parseBlockNumber function silently returns 0 for invalid inputs:

func parseBlockNumber(s string) uint64 {
    // ... parse logic ...
    var n uint64
    if _, err := fmt.Sscanf(s, "%d", &n); err == nil {
        return n
    }
    return 0  // Returns 0 on error!
}

Impact: If a user provides an invalid targetBlock value (e.g., "abc"), it silently becomes block 0 instead of returning an error.

Recommendation: Return an error from this function and propagate it.

Fix this →


🟡 Suggested Improvements

6. Performance: RPC Batch Size Not Respected in Step A

Location: step_a.go:61, step_a.go:96

Step A uses concurrentBatchRPC with a hardcoded batch size calculation, ignoring cfg.Options.RPCBatchSize:

headerResults, err := concurrentBatchRPC(ctx, rpcURL, headerCalls, batchSize, concurrency)

But batchSize is set to cfg.Options.RPCBatchSize in the caller. However, in Step 0, the batch size is calculated dynamically. This inconsistency could lead to performance issues.

Recommendation: Consistently use cfg.Options.RPCBatchSize across all steps or document why some steps calculate it differently.

7. Code Quality: Inconsistent Error Handling in Worker Pools

Location: worker.go:66-83

The worker pool returns only the first error encountered but continues processing:

if r.err != nil {
    if firstErr == nil {
        firstErr = r.err
    }
    log.Warnf("%s job failed: %v", label, r.err)
    continue  // Keeps going even after first error
}

Behavior Concern: If many jobs fail, the tool continues processing and only returns the first error at the end. This could waste time on a fundamentally broken operation.

Recommendation: Consider adding a failFast parameter or error threshold to stop early on repeated failures.

8. Code Quality: Magic Numbers in hex.go

Location: hex.go:14-24, step_e.go:114

Several magic numbers lack clarity:

leafIndexMask := new(big.Int).SetUint64(0xFFFFFFFF) //nolint:mnd
mainnetFlag := new(big.Int).Lsh(big.NewInt(1), 64) //nolint:mnd

While the //nolint:mnd comment suppresses the linter, it would be clearer to define these as package-level constants with descriptive names:

const (
    globalIndexLeafMask    = 0xFFFFFFFF
    globalIndexMainnetBit  = 64
)

Fix this →

9. Observability: Missing Progress Logging in Step D

Location: step_d.go:17-75

Step D processes EOA balances and SC-locked values but doesn't log progress during iteration (unlike other steps). For large migrations with thousands of exits, this could leave users wondering if the process is hung.

Recommendation: Add progress logging similar to other steps.

10. Code Quality: Inconsistent Nil Check Pattern

Location: step_b.go:220-228

func unmarshalHexBigInt(result json.RawMessage) *big.Int {
    if result == nil {
        return nil
    }
    var hex string
    if json.Unmarshal(result, &hex) != nil || hex == "" || hex == "0x" {
        return nil
    }
    return hexToBigInt(hex)
}

This function returns nil for invalid inputs, which is then checked in step_0.go:198-200:

supply := unmarshalHexBigInt(result)
if supply == nil {
    supply = new(big.Int)  // Convert nil to zero
}

Pattern Concern: Treating nil as "zero" is semantically different from "error". A missing RPC result vs. a zero balance should be distinguished.

Recommendation: Consider returning (*big.Int, error) to distinguish between zero values and errors.


🟢 Optional Enhancements

11. Go Best Practices: Use crypto/rand for Worker Pool Channel Cap

Location: worker.go:9

const workerPoolChannelCap = 10000

This is a reasonable constant, but the value seems arbitrary. Consider documenting why 10,000 was chosen or making it configurable.

12. Testing: Missing Tests for Edge Cases

While test coverage is good overall, some edge cases lack explicit tests:

  • Step A with chain that has never had a transaction
  • Step E with L2ClaimEvents that have non-mainnet globalIndex flags
  • Boundary condition where metadataOffset is exactly at the end of data

13. Documentation: Add Sequence Diagram

The README is excellent, but a sequence diagram showing the data flow through all 6 steps would help users understand dependencies.

14. Code Style: Inconsistent Use of Named Return Values

Most functions don't use named return values, but classifyAddresses (step_b.go:75) does:

func classifyAddresses(...) (eoas, contracts []common.Address, err error)

For consistency with the rest of the codebase, consider using unnamed returns.

15. Performance: Consider Adding Rate Limiting

The tool has rpcDelayMs but it's applied between batches, not per request. For RPC providers with strict rate limits, adding a proper rate limiter (e.g., golang.org/x/time/rate) would be more robust.


📊 CLAUDE.md Compliance

Formatting: Uses gofmt, goimports
Imports: Properly grouped (stdlib, external, internal)
Naming: CamelCase for exports, no stuttering
Errors: Uses fmt.Errorf with %w for wrapping
Line length: All lines under 120 chars
Comments: Exported functions have doc comments
Tests: Uses testify/require (not assert) ✅
File naming: All test files use _test.go suffix

Minor deviation: Uses require consistently (good), but CLAUDE.md says "Use require (not assert)" which is satisfied.


🏗️ Architecture Assessment

The tool demonstrates excellent software engineering:

  1. Modularity: Each step is independently testable and resumable
  2. Concurrency: Thoughtful use of worker pools, semaphores, and batching
  3. Reliability: Retry logic, error handling, and graceful degradation
  4. Usability: Clear CLI, good defaults, comprehensive README
  5. Maintainability: Clean code structure, good test coverage

The main areas for improvement are security validations around untrusted input (event decoding) and more consistent error handling patterns.


🎯 Summary

This is a high-quality implementation of a complex migration tool. The code is well-structured, thoroughly tested, and production-ready with some security hardening.

Priority fixes:

  1. 🔴 Add overflow checks for big.Int conversions (Unify L1 syncers #1)
  2. 🔴 Add bounds validation for event metadata (Split L1 / L2 responsibilities for lastgersync #2)
  3. 🔴 Cap batch size to respect RPCBatchSize (Add UTs for lastgersync #3)
  4. 🔴 Add validation for targetBlock format (Add monitoring #5)

Recommended improvements:

Test status: CI checks are currently in progress. I'll monitor for any failures.


@krlosMata krlosMata force-pushed the feature/exit-certificate-tool branch from 6d18df4 to 85a1fa9 Compare April 14, 2026 08:58
@krlosMata krlosMata changed the title new tool: exit_certificate feat: new tool - exit_certificate Apr 14, 2026
@joanestebanr joanestebanr requested a review from Copilot April 15, 2026 09:01

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

Adds a new standalone exit-certificate CLI tool under tools/exit_certificate/ to generate Agglayer exit certificates for aggchain migration by scanning L2/L1 state and producing an agglayer/types.Certificate with BridgeExit entries.

Changes:

  • Implements a 6-step pipeline (0 → A → B → C → D → E) for LBT generation, address discovery, balance scanning, SC-locked computation, certificate building, and unclaimed L1→L2 deposit detection.
  • Adds JSON-RPC batching/concurrency utilities plus step-wise resumable output persisted to an output directory.
  • Adds unit/integration tests and documentation/config examples for running the tool.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tools/exit_certificate/worker.go Generic worker-pool helper used to parallelize step workloads.
tools/exit_certificate/types.go Shared data structures for step outputs and deposit/event models.
tools/exit_certificate/step_0.go Step 0 implementation: generate LBT by scanning bridge logs + supplies.
tools/exit_certificate/step_a.go Step A implementation: scan blocks/txs and trace touched addresses.
tools/exit_certificate/step_b.go Step B implementation: EOA/contract classification and balance scanning.
tools/exit_certificate/step_c.go Step C implementation: compute SC-locked values from LBT vs EOA totals.
tools/exit_certificate/step_d.go Step D implementation: build the exit certificate BridgeExits.
tools/exit_certificate/step_e.go Step E implementation: scan L1 BridgeEvents and add unclaimed deposits.
tools/exit_certificate/rpc.go JSON-RPC client utilities: batch/single RPC, retries, concurrency batching.
tools/exit_certificate/hex.go Hex/decimal parsing helpers and ABI safety conversions.
tools/exit_certificate/config.go Config loading/validation, defaults, and LBT file parsing helpers.
tools/exit_certificate/run.go CLI execution wiring: full pipeline + step-by-step resumability and I/O.
tools/exit_certificate/cmd/main.go CLI binary entrypoint using urfave/cli.
tools/exit_certificate/README.md Tool documentation: config, steps, usage, outputs, testing.
tools/exit_certificate/.gitignore Ignore local parameters/output/binary artifacts for this tool.
tools/exit_certificate/parameters.json.example Example standalone JSON config for running the tool.
tools/exit_certificate/step_a_test.go Unit tests for hex block parsing helper(s) used in Step A.
tools/exit_certificate/step_b_test.go Unit tests for hex-to-bigint helper used in Step B.
tools/exit_certificate/step_c_test.go Unit tests for SC-locked computation behavior and edge cases.
tools/exit_certificate/step_d_test.go Unit tests for certificate construction from EOA + SC-locked inputs.
tools/exit_certificate/step_e_test.go Unit tests for BridgeEvent decoding and claimed-set filtering logic.
tools/exit_certificate/rpc_test.go Unit tests for batch/single RPC, retry behavior, and error handling.
tools/exit_certificate/run_test.go Unit tests for block parsing and JSON save/load helpers.
tools/exit_certificate/config_test.go Unit tests for config parsing, defaults, and LBT parsing helpers.
tools/exit_certificate/integration_test.go Integration-style tests for production-like config/data shapes (skippable).

Comment thread tools/exit_certificate/step_d.go Outdated
Comment thread tools/exit_certificate/run.go Outdated
Comment thread tools/exit_certificate/step_b.go Outdated
Comment thread tools/exit_certificate/rpc.go Outdated
Comment thread tools/exit_certificate/step_e.go Outdated
Comment thread tools/exit_certificate/step_a.go Outdated
Comment thread tools/exit_certificate/run.go Outdated
Comment thread tools/exit_certificate/step_e.go Outdated
@krlosMata krlosMata force-pushed the feature/exit-certificate-tool branch from 4936ffa to 63a43be Compare April 17, 2026 09:47
@sonarqubecloud

Copy link
Copy Markdown

@joanestebanr joanestebanr force-pushed the feature/exit-certificate-tool branch from ac0936d to 2122813 Compare May 4, 2026 16:12
@joanestebanr joanestebanr self-assigned this May 11, 2026
@sonarqubecloud

Copy link
Copy Markdown

krlosMata and others added 12 commits June 12, 2026 15:13
- Add overflow checks for big.Int to uint32/uint64 conversions (safeUint32, safeUint8)
- Add max metadata size validation (1MB) in decodeBridgeEvent to prevent DoS
- Cap batch size to RPCBatchSize in fetchTotalSupplies
- Return error from parseBlockNumber on invalid input instead of silent zero
- Extract globalIndex magic numbers to named constants
- Add progress logging to Step D
- Document native token handling in step_c indexByAddress
- Fix all golangci-lint issues (errcheck, gci, gosec, lll, mnd, prealloc, unparam)

Made-with: Cursor
- Scan L2 bridge for ClaimEvent logs so Step E correctly identifies
  already-claimed deposits instead of treating all as unclaimed (joanestebanr)
- Fail on trace/scan errors instead of warn+continue: traceTransactions,
  fetchL1BridgeEvents now propagate errors (partial scans are unsafe)
- Fix encodeBalanceOf: use zero-padding (LeftPadBytes) instead of
  space-padding (%064s) which produced invalid hex calldata
- Use strconv.ParseUint instead of fmt.Sscanf to reject trailing
  non-numeric input like "123abc"
- Set MaxIdleConnsPerHost=100 instead of 0 (0 defaults to 2 in net/http)
- Preserve OriginNetwork/OriginTokenAddress from LBT for native token
  entries (supports chains with custom gas tokens)
- Add decodeClaimEvent tests

Made-with: Cursor
- Extract magic number 32 to named constant abiWordSize in step_b.go
- Pre-allocate claims slice in fetchClaimEventsInRange

Made-with: Cursor
- Replace ClaimEvent log scanning with isClaimed(depositCount, 0)
  eth_call on L2 bridge contract (authoritative claimed bitmap)
- Extract helper functions across all steps to bring every function
  under diffguard thresholds (complexity ≤ 10, size ≤ 50 lines)

Made-with: Cursor
Add exit_certificate binary to the Makefile build-tools target so it
builds alongside the other tools. Add maskRPCURL helper that strips the
path from RPC URLs before logging, preventing API key exposure in error
messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ndling and logging

- Add L2StartBlock option to config so block scanning starts from a
  configurable block instead of always from block 0
- Add label parameter to concurrentBatchRPC to identify each call site
  in progress logs (e.g. "L2 RPC/blockHeaders", "L2 RPC/balanceOf")
- Improve batchRPC: log individual RPC-level errors via log.Warn and
  return the first error instead of silently dropping failed responses;
  add response-count validation
- Add detailed app.Description to the CLI listing all pipeline steps
  (0, A, B, C, D, E) and how to run individual steps
- Add .PHONY declarations for build-tools targets in Makefile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
joanestebanr and others added 26 commits June 12, 2026 15:13
…king, balance capping, and ignoreUnclaimed

--step flag now supports range notation: "f-i" expands to f,g,h,i and
"f-" expands to f through sign (submit/wait always require explicit opt-in).

Step G reads InitialLocalExitRoot from the bridge contract before
replaying exits (via Anvil fork, or against the real L2 RPC for the
empty-exits case). A 1-minute sleep separates the last bridgeAsset call
from the final getRoot read.

Step H verifies that its agglayer settled LER matches the InitialLocalExitRoot
reported by step G, returning a hard error on mismatch.

Step I (single-step mode) now prefers step-f-capped-certificate.json over
step-e-exit-certificate.json, consistent with step G.

Step F balance capping is rewritten: buildCapMap + capBridgeExits are
replaced by a single capCertificateExits function that processes exits
in order, deducting each exit's amount from a per-token RemainingBalance
(= min(LBT, agglayer)). Exits that exceed the remaining budget are capped
to it; exits that arrive when the budget is exhausted are dropped.
RemainingBalance is carried on TokenBalanceCheck (json:"-").

Step E gains options.ignoreUnclaimed: when true, unclaimed deposits are
detected and logged as warnings but not added to the certificate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ging

Apply crypto.Keccak256 to raw BridgeEvent metadata before assigning it to
BridgeExit.Metadata in Step I, matching aggsender's convertBridgeMetadata
behaviour. Without this, BridgeExit.Hash() produced a different value than
aggsender, causing CertificateID and therefore the certificate hash to
diverge — making the signature unverifiable by the agglayer.

Also log signer address, certificate fields (networkID, height, LERs),
CertificateID, and hash-to-sign in Step SIGN to aid future debugging.

Add a pipeline step summary table to the README.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…filtering

Add optional bridge service cross-check to Step E and split unclaimed
L1→L2 deposits by leaf type.

Changes:
- config: add `bridgeServiceURL` and `bridgeServiceType` options
  ("aggkit" or "zkevm") for configuring the bridge service endpoint
- step_e: separate unclaimed deposits into assets (leaf_type=0) and
  messages (leaf_type=1); only assets are added to the certificate
- step_e: log a single info line with the message count instead of
  per-deposit warnings
- step_e: log a single token-grouped summary for ignored unclaimed
  assets (name and decimals fetched from contract; ETH shown in ETH)
- step_e: when bridgeServiceURL is set, compare the bridge service's
  pending-bridges set against the L1 scan unclaimed set and error only
  on discrepancies; supports both aggkit (/bridge/v1/bridges) and
  zkevm-bridge-service (/pending-bridges) APIs
- run: always save `step-e-unclaimed-messages.json` (even when empty)
- rpc: add `httpGetJSON` helper for REST GET calls
- kurtosis script: auto-detect and configure zkevm-bridge-service-001
  as the bridge service when available

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rors

Per the function contract ("individual RPC errors are logged and become
nil entries"), per-item RPC errors must not bubble up as a Go error.
Remove the early-exit check on responses[0].Error and the firstRPCErr
accumulator — errors are already logged as warnings and the result slot
is left nil.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…al Step E behavior

Remove unused functions depositsToImportedExits and depositsToExits from step_e.go;
these were never called and represented unimplemented logic (Merkle proof support for
adding unclaimed L1→L2 deposits to the certificate).

Remove unused ABI selector constants gasTokenAddressSelector and gasTokenNetworkSelector
from step_0.go.

Fix README.md and CLAUDE.md to reflect what Step E actually does:
- When unclaimed asset deposits are found and ignoreUnclaimed=false → pipeline errors
  (Merkle proof support not yet implemented)
- When ignoreUnclaimed=true → deposits are detected and logged, certificate unchanged
- imported_bridge_exits is not populated by Step E today
- Step I prefers step-f-capped-certificate.json over step-e-exit-certificate.json when it exists
- Add missing abortOnGenesisBalance option to the options table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…factor fetch logic

- Fix zkevmDeposit JSON tags and TotalCnt type to match actual API response
- Fix dest_net query param and restrict cross-check to leaf_type=0 (assets)
- Refactor bridge service fetchers to accept leafType and share comparison logic
- Fix formatTokenAmount precision loss for small sub-unit values
- Always persist unclaimed files even when Step E fails

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ross-check

The bridge service check only covers leaf_type=0 (assets), so the comparison
must use the asset subset rather than all unclaimed deposits. Also split
by leaf type before the check so the log line can report asset/message counts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Explain why l1RpcUrl matters in practice, warn that exitAddress must
be a key you control, document the signerConfig format, and add a table
describing when to use continueOnTraceError, abortOnGenesisBalance and
ignoreUnclaimed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
It is needed by Step E and Step I; without it the certificate is incomplete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…EADME

List l1RpcUrl, exitAddress and signerConfig as the fields that must be
filled in before running the tool, and link to the main README for the
full field reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… API

Introduce options.agglayerAdminToken to pass an Authorization: Bearer
header when calling admin_getTokenBalance in Step F. Required when
agglayerAdminURL is protected by Google Cloud IAP.

Also replaces the flat agglayerGrpcUrl string option with a structured
agglayerClient config object (agglayer.ClientConfig), enabling TLS,
timeout and retry customization for gRPC steps H, SUBMIT, and WAIT.

Add ready-to-use config examples for zkevm-cardona and zkevm-mainnet
in config-examples/.

Documentation updated with IAP token instructions and environment-specific
service account / audience values for spec, bali, cardona, and mainnet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- tools/exit_certificate: fix lll, mnd, gci, whitespace, goconst,
  gocritic, makezero, unparam, prealloc, and errorlint issues; add
  named constants (abiWordBytes, ethDecimals, hexBase, etc.) to hex.go;
  remove unused params from fetchGasTokenInfo and checkNativeGasToken;
  expand unit test coverage with hex_test.go, step_g_test.go and
  additional cases in rpc_test.go, config_test.go, step_f_test.go
- aggsender, bridgeservice, multidownloader, scripts, backward_forward_let:
  suppress gosec false-positives with nolint directives
- db/migrations/testutils: add gosec nolint alongside existing mnd nolint
- l1infotreesync/migrations: preallocate migrations slice
- sync/evmdownloader_test: preallocate testCases slice

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds G204, G602, G703, G118 to the global gosec excludes in .golangci.yml
so local and CI golangci-lint produce identical results regardless of version.
Removes now-redundant //nolint:gosec directives from 6 files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
G703 and G118 are not valid rule IDs in gosec as bundled with golangci-lint
v2.4.0 (used by CI). Move them from gosec.excludes (schema-validated) to
exclusions.rules with text matching, which is version-agnostic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… for SC-locked exits (#1622)

## 🔄 Changes Summary

### Fix F-01: `ensureERC20Balance` storage patching for SC-locked ERC-20
exits
- Implements `ensureERC20Balance` in Step G, which was a stub that
always returned an error without actually patching Anvil storage.
- The function now patches `_balances[account]` via
`hardhat_setStorageAt` using a two-layout detection strategy, verifying
`balanceOf` after each attempt:
  1. **OZ v4 non-upgradeable**: `_balances` mapping at storage slot 0
2. **OZ v5 upgradeable**: `_balances` inside the namespaced
`ERC20Storage` struct at `ERC20StorageLocation =
0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00`
- Adds a package-level `erc20NamespacedStorageLocation` constant
documenting the OZ v5 storage namespace derivation.

### Refactor: remove `lbtFile` config option
- `lbtFile` was an escape hatch to skip Step 0 by providing a
pre-generated LBT. Step 0 now always runs, so the field is removed.
- Merges `RunStepCWithEntries` back into `RunStepC` (simpler API, no
config dependency).
- Updates `resolveOrGenerateLBT`, `loadWrappedTokensFromLBT`,
`runSingleC`, `runSingleB`, `runSingleF`, `runSingleG` accordingly.

### Kurtosis script improvements
- Adds two helper scripts for the Kurtosis test environment.
- Embeds a pre-generated exit address keypair
(`0xe25f5B65E4976025f670e52b790a9746F27A3DB6`) in
`configuration_based_on_kurtosis.sh` so the exit address is stable
across runs without requiring Foundry at script runtime. The private key
and an encrypted keystore (password: `test`) are written to `tmp/` on
first execution.

## ⚠️ Breaking Changes
- `lbtFile` config field removed. Any existing config files using it
will have the field silently ignored (unknown JSON fields are not
errors, but Step 0 will now always run).

## 📋 Config Updates
- `lbtFile` removed from `Config` and `rawConfig`. Step 0 is no longer
skippable via config.

## ✅ Testing
- 🤖 **Automatic**: Existing unit tests pass (`ok
github.com/agglayer/aggkit/tools/exit_certificate`)
- 🖱️ **Manual**:
1. Run `tools/exit_certificate/scripts/reproduce_sc_locked.sh` against a
live Kurtosis `aggkit` enclave
2. The script deploys a `TokenHolder` smart contract on L2, transfers
wrapped ERC-20 tokens to it, then drives the exit-certificate tool from
steps 0→G
3. Before fix: Step G failed with `ERC20InsufficientBalance`
(`0xe450d38c`) when replaying the SC-locked `BridgeExit`
  4. After fix: Step G completes and emits a valid `NewLocalExitRoot`

## 🐞 Issues
- Fixes: #1624 

## 🔗 Related PRs
- N/A

## 📝 Notes
- `TokenWrapped` (wTTK) deployed by `AgglayerBridge` uses OZ v5
upgradeable storage, so the slot-0 attempt is a no-op. The second
candidate (namespaced storage) is the one that matches and patches the
balance correctly.
- The two-layout approach is safe: a failed slot write leaves the token
balance unchanged and the loop moves to the next candidate. If neither
layout works the function returns a descriptive error.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…runs (#1627)

## 🔄 Changes Summary

- Changed `cfg.TargetBlock` type from `string` to
`aggkittypes.BlockNumberFinality`, enabling finality keywords
(`LatestBlock`, `FinalizedBlock`, `SafeBlock`, `PendingBlock`),
decimal/hex block numbers, and offset notation (e.g. `LatestBlock/-10`).
- Added `Step0Result.TargetBlock uint64` — the concrete resolved block
number is now part of the Step 0 output and persisted to
`step-0-l2_target_block.json` (new file).
- Removed `ResolvedTargetBlock` from `Config`; the resolved block number
is passed as an explicit `targetBlock uint64` parameter to `RunStepA`,
`RunStepB`, and `RunStepG`.
- Single-step runners (`runSingleA/B/G`) load the block from
`step-0-l2_target_block.json` via a new `loadTargetBlock` helper.
- Updated README with target-block resolution table and updated Step 0
output list.
- Added output-directory cleanup hint at the end of the kurtosis
configuration script.

## ⚠️ Breaking Changes

- 🛠️ **Config — `targetBlock` values renamed**: existing
`parameters.json` files must update their `targetBlock` field to use the
new PascalCase keywords:
  - `"latest"` → `"LatestBlock"`
  - `"finalized"` → `"FinalizedBlock"`
  - `"safe"` → `"SafeBlock"`
  - `"pending"` → `"PendingBlock"`

The default (empty string) still resolves to `LatestBlock`. Decimal and
hex block numbers are unchanged.

- 🗑️ **New output file**: `step-0-l2_target_block.json` is a new file
produced by Step 0. Note: `step-0-result.json` (from a previous PR) was
already renamed to `step-0-lbt.json`; any tooling still reading
`step-0-result.json` must be updated to `step-0-lbt.json`.

## 📋 Config Updates

```json
// Before
"targetBlock": "latest"

// After
"targetBlock": "LatestBlock"       // or "FinalizedBlock", "SafeBlock", "PendingBlock"
"targetBlock": "LatestBlock/-10"   // latest minus 10 blocks
"targetBlock": "12345678"          // decimal (unchanged)
"targetBlock": "0xBCDE34"          // hex (unchanged)
```

## ✅ Testing

- 🖱️ **Manual**: Run full pipeline with `targetBlock: "LatestBlock"` and
verify `step-0-l2_target_block.json` is written with a valid block
number; subsequent single-step runs for A, B, and G should pick it up
automatically.

## 🐞 Issues

- Closes #1625

## 🔗 Related PRs

- Base branch: `feat/exit_certificate_f01_token_sclocked`

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## 🔄 Changes Summary
- Propagate trace errors through the worker pool and abort all in-flight
workers on first failure when `ContinueOnTraceError=false` (F-09)
- Add `StepAWindowSize` config option to tune Step A chunk size
independently from `blockRange`
- Replace `[]common.Hash failedTraces` with `[]FailedTrace{Hash, Error}`
so callers get the RPC error alongside the hash
- Promote `fetchSetSovereignTokenEvents` /
`applySovereignTokenOverrides` errors from silent `log.Warn` to returned
errors in `RunStep0`
- Fix variable shadowing bug for `nativeEntry` in `RunStep0` (`:=` →
`=`)
- Remove dead `lbtFile` config field and merge `RunStepCWithEntries`
back into `RunStepC`

## ⚠️ Breaking Changes
- 🛠️ **Config**: `lbtFile` option removed from `Options` — it was an
escape hatch to skip Step 0 that is no longer needed since Step 0 always
runs
- 🛠️ **Config**: `blockRange` no longer controls the block window size
in Step A — `stepAWindowSize` is now used exclusively for that (defaults
to 5000). Existing configs that relied on `blockRange` to tune Step A
throughput should add an explicit `stepAWindowSize`
- 🔌 **API/CLI**: `runWorkerPool`, `startWorkers`, `collectResults` now
require a `context.Context` as first argument

## 📋 Config Updates
- 🧾 New optional field `stepAWindowSize` (default: 5000):
```json
"options": {
    "blockRange": 10000,
    "stepAWindowSize": 10000
}
```

## ✅ Testing
- 🤖 **Automatic**:
- Unit tests for `traceOneTransaction` (success, dedup, RPC error, bad
JSON, null+error)
- Unit tests for `traceTransactions` (continueOnError path,
abort-on-error path)
- End-to-end `TestRunStepA_AbortOnTraceError` against a fake HTTP server
verifying context cancellation stops the pool
- 🖱️ **Manual**: Run Step A with a node that returns trace errors with
`continueOnTraceError=false` and verify the tool exits immediately with
the offending hash and error message

## 🐞 Issues
- Closes agglayer/pm#346
- Partially fixes agglayer/pm#349 (`Problem 2 — Variable shadowing makes
nil-check dead code`)

## 🔗 Related PRs
- Base: feat/exit_certificate_f05_target_block (block finality
resolution for Step 0)

## 📝 Notes
- `StepAWindowSize` exists because `debug_traceTransaction` RPC calls
are more expensive than `eth_getLogs`; operators may want a smaller
window for Step A without changing the log-query range used by Steps 0,
B, and E
- Context cancellation in `collectResults` drains `resultCh` in a
background goroutine to let workers release resources cleanly instead of
blocking

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… rollup diagnostics (#1630)

## 🔄 Changes Summary

- **Step A → A1 + A2 split**: Step A is now two sub-steps. A1 runs
`debug_traceTransaction` (prestateTracer + diffMode) as before and
records failed hashes. A2 recovers addresses from
`eth_getTransactionReceipt` for each A1 failure, extracting
from/to/contractAddress/log emitters. The combined
`step-a-addresses.json` is the union of both.
- **Step aliases**: `--step a` expands to `a1,a2`; both sub-steps are
individually addressable (`--step a1`, `--step a2`). Range syntax works
too (`a-b` → `a1,a2,b`).
- **Migration**: on startup, legacy `step-a-*` files are renamed to
`step-a1-*` so existing output dirs remain usable without re-running A1.
- **Legacy rollup diagnostics**: when `AGGCHAINTYPE()` fails
(pre-aggchainbase contract), `logLegacyRollupInfo` queries the rollup
manager to surface `rollupID`, `rollupTypeID`, `chainID`, `forkID`,
`rollupVerifierType`, and full `rollupTypeMap` info (consensusImpl,
verifier, obsolete, genesis, programVKey) as diagnostic log lines.

## ⚠️ Breaking Changes

- 🔌 **CLI**: `--step a` still works as before (runs both sub-steps).
`step-a1-addresses.json` and `step-a1-failed-traces.json` are the new
canonical A1 outputs; `step-a-failed-traces.json` is no longer written
directly (migrated from legacy on startup).

## ✅ Testing

- 🤖 **Automatic**: `TestParseStepList` extended with `a`, `a-b`, `a2-b`
cases.
- 🖱️ **Manual**: run `--step a` on a chain with trace failures to verify
A2 recovers addresses from receipts.

## 📝 Notes

- A2 never aborts on receipt failures — it logs a warning and skips, so
the pipeline always produces a result even when receipts are also
unavailable.
- The legacy rollup diagnostics path does not modify check failures or
results; it is purely informational output to help diagnose
pre-aggchainbase deployments.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…act holder decomposition (#1632)

## 🔄 Changes Summary
- **Step B2**: probes each contract address collected in Step A for the
ERC-20 interface (`totalSupply`/`balanceOf`). Classifies contracts as
`DetectedERC20` (holds ≥1 tracked wrapped token) or `DiscardedERC20`.
Outputs `step-b2-detected-erc20s.json` and
`step-b2-discarded-erc20s.json`.
- **Step B3**: iterates over `options.extraErc20Contracts`. Reuses B2
holder data when available; otherwise calls `balanceOf` for every EOA
from Step A. Outputs `step-b3-erc20-holders.json`.
- **Step C**: extended to incorporate SC-locked values from B2 detected
ERC-20s.
- **Step D**: generates `BridgeExit` entries for ERC-20-locked balances
from B2/B3.
- **config**: added `extraErc20Contracts` option; increased
`defaultStepAWindowSize` from 5 000 to 150 000.
- Pipeline (`run.go`) updated to execute B2 and B3 between B and C.
- New types, RPC helpers, unit tests, docs and example config for the
new steps.

## ⚠️ Breaking Changes
- 🛠️ **Config**: new optional field `options.extraErc20Contracts` (array
of addresses). No breaking change — defaults to empty.

## 📋 Config Updates
```json
"options": {
  "stepAWindowSize": 150000,
  "extraErc20Contracts": ["0xTokenAddress1", "0xTokenAddress2"]
}
```

## ✅ Testing
- 🤖 **Automatic**: unit tests added for Step B2 (`step_b2_test.go`) and
Step B3 (`step_b3_test.go`); Step C tests extended.
- 🖱️ **Manual**: run full pipeline with `extraErc20Contracts` populated
and verify `step-b2-detected-erc20s.json`, `step-b3-erc20-holders.json`
outputs.

## 🐞 Issues
- Closes agglayer/pm#341

## 📝 Notes
- Step B3 short-circuits when `extraErc20Contracts` is empty — no RPC
calls made.
- `defaultStepAWindowSize` raised to 150 000 to reduce RPC round-trips
on chains with large block ranges.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…shadow-fork verification (#1633)

## 🔄 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 `BridgeEvent`s (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 **mandatory** — `LoadConfig`
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) —
`abortOnGenesisBalance` → `ignoreGenesisBalance` *(polarity inverted:
default `false` = abort)*, `continueOnTraceError` →
`ignoreOnTraceError`, `continueIfBalanceMismatch` →
`ignoreBalanceMismatch`.
- 🗑️ **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.verifyNewLocalExitRootUsingShadowFork` — `true` (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.ignoreUnsupportedL2Events` — `false` (default). Downgrades
the lite syncer's abort on unsupported events to a warning.

**Renamed options** (to the `ignore*` convention)
- `abortOnGenesisBalance` → `ignoreGenesisBalance` *(polarity inverted:
default `false` = abort)*
- `continueOnTraceError` → `ignoreOnTraceError`
- `continueIfBalanceMismatch` → `ignoreBalanceMismatch`

**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
- Closes agglayer/pm#352 (agglayer/pm#352)
- Closes agglayer/pm#348 (agglayer/pm#348)

## 📝 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](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… data (#1650)

## 🔄 Changes Summary

- **New `tools/exit_certificate_claimer` service**: a read-only HTTP
service that, given a destination address, lists the bridge exits
available for that address and assembles the full set of parameters
needed to call `AgglayerBridge.claimAsset` on L1. It builds the
`claimAsset` arguments from three sources: the signed certificate
(`exit-certificate-signed.json`), the L2 local exit tree
(`step-g-l2bridgesyncerlite.sqlite`) and the L1 Info Tree DB. API base
path `/claimer/v1`, endpoints: `GET /health`, `GET /bridges`, `GET
/claim-params`. See `SPEC.md` / `README.md`.
- Proofs are anchored to the L1 settlement leaf; the certificate's
`new_local_exit_root` must already be settled on L1 (`/claim-params`
returns `409` if not).
- Config can be provided directly (JSON/TOML, see
`service/config.toml.example`) or **derived from an `exit_certificate`
config** via `--exit-certificate-config`.
- On startup the claimer derives the settlement GER from the WAIT step
result and either serves from the already-synced L1 Info Tree DB or
syncs L1 only until that GER is indexed.
- **Helper scripts** (`tools/exit_certificate_claimer/scripts/`):
`list-bridges.sh`, `claim-asset.sh`, and `claim-all.sh` (claims every
pending deposit for all addresses of an exit run).
- **`exit_certificate` tool changes** required for the claimer flow:
- **WAIT step**: confirm L1 settlement and persist
`step-wait-result.json`; handle `verifyBatches` events.
  - **Step F**: admin toggle.
- **Step G2**: generate metadata when the shadow fork is off; use raw
metadata.
- **Step H**: refuse to proceed when the agglayer still has a
non-settled (open) certificate for the network; clearer LocalExitRoot
mismatch error.
- **Build**: `make build-tools` now also builds
`exit_certificate_claimer`, and individual `build-<tool>` targets were
added (`build-exit_certificate`, `build-exit_certificate_claimer`,
etc.).

## ⚠️ Breaking Changes

- 🛠️ **Config**: None to existing components — the new config is scoped
to the new tool.
- 🔌 **API/CLI**: Adds a new standalone `exit_certificate_claimer`
binary; no changes to existing interfaces. The claimer's default HTTP
port is `7080`.
- 🗑️ **Deprecated Features**: None.

## 📋 Config Updates

- 🧾 New tool config only:
`tools/exit_certificate_claimer/service/config.toml.example` (or derive
it from an existing `exit_certificate` config via
`--exit-certificate-config`).

## ✅ Testing

- 🤖 **Automatic**: `service/certificate_test.go`,
`service/claimer_test.go`, and
`exit_certificate/step_wait_verifybatches_test.go` (plus updated step_f
/ step_g2 / config tests). Verified locally: `make build`, `go test
./...` (pass), and `golangci-lint run` (0 issues).
- 🖱️ **Manual**: Build with `make build-exit_certificate_claimer`, run
the service against an exit-certificate output dir
(`--exit-certificate-config`), then exercise the scripts
(`list-bridges.sh`, `claim-asset.sh`, `claim-all.sh`).

## 🐞 Issues

- Related: agglayer/pm#364

## 🔗 Related PRs

- Builds on #1633 (Step G1/G2 split + `bridgesyncerlite`), already
merged into `feature/exit-certificate-tool`.

## 📝 Notes

- Backend design and API documented in
`tools/exit_certificate_claimer/{SPEC,README}.md`; scripts documented in
`tools/exit_certificate_claimer/scripts/README.md`.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr joanestebanr force-pushed the feature/exit-certificate-tool branch from 7eb9e06 to 427ae79 Compare June 12, 2026 13:19
…aim flow

- Build via `make build-exit_certificate`; run with `./target/exit_certificate`
- Add Requirements section (PP, threshold=1, prior settled certificate, stopped sequencer)
- Add "no unclaimed L1→L2 bridges" limitation
- Document zkEVM config-examples and the missing options
  (ignoreUnsupportedL2Events, verifyNewLocalExitRootUsingShadowFork)
- Fix step descriptions to match code: Step A (A1/A2), SUBMIT (pending-cert
  rejection + L1 block capture), WAIT (L1 settlement confirmation)
- Document `--step` ranges (a-c, g-, 0-wait)
- Note SUBMIT/WAIT must be run explicitly after the pipeline
- Replace Output section with a Result section covering the claim files
  consumed by exit_certificate_claimer
- Drop the obsolete external getLBT reference

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr joanestebanr added the exit_certificate_tool Tool to create a final exit certificate label Jun 12, 2026
@sonarqubecloud

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.

implement exit cert tool

4 participants