feat: new tool - exit_certificate#1582
Conversation
|
Claude finished @krlosMata's task in 4m 37s —— View job PR Review: exit_certificate Tool ✅🚀 What's NewA new standalone CLI tool ( 🐛 Bug FixesNone. 📋 Config UpdatesNew config file: Required fields:
Optional fields:
|
6d18df4 to
85a1fa9
Compare
There was a problem hiding this comment.
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). |
4936ffa to
63a43be
Compare
|
ac0936d to
2122813
Compare
|
7eb9e06 to
427ae79
Compare
|
- 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>
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>
…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>
…and CI workflow (#1655) ## 🔄 Changes Summary - Add the **exit-certificate E2E test harness** under `test/e2e-exit_certificate/`: spins up a Kurtosis CDK network (`run_network.sh`), prepares a settled certificate (`prepare_network.sh`), and runs the `exit_certificate` tool (`run_exit_tool.sh`), orchestrated by `run_e2e_test.sh` (+ `helper.sh`, `single_op_pessimistic_args.json`, `README.md`). - Add supporting scripts under `tools/exit_certificate/scripts/`: - `bridge_l2_to_l1.sh` — L2→L1 bridge to produce a certificate. - `agglayer_certificate_status.sh` — poll/wait for certificate settlement. - `export_kurtosis_env.sh` — export Kurtosis env vars for the scripts. - `agglayer_status/main.go` — agglayer status helper. - Update `bridge_l1_to_l2.sh`. - Add CI workflow `.github/workflows/test-e2e-exit_cenrtificate_tool.yml` that runs the E2E test on PRs touching `tools/exit_certificate/**` (also `workflow_dispatch`). ##⚠️ Breaking Changes - None. ## 📋 Config Updates - None. ## ✅ Testing - 🤖 **Automatic**: New GitHub Actions workflow runs `test/e2e-exit_certificate/run_e2e_test.sh` on PRs that modify `tools/exit_certificate/**` (Kurtosis CDK network → settled certificate → exit_certificate tool). - 🖱️ **Manual**: `./test/e2e-exit_certificate/run_e2e_test.sh` locally (requires Docker, Kurtosis CLI, Foundry `cast`, Go). ## 🐞 Issues - N/A ## 🔗 Related PRs - #1654 (feat: subtract genesis preload from EOA balances in Step B) ## 📝 Notes - The E2E harness clones a pinned Kurtosis CDK commit and manages its own `aggkit` enclave; the workflow stops the enclave on completion. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… pending-bridges `dest_net` (#1683) ## 🔄 Changes Summary - `step_e.fetchZkevmPendingBridges()` hardcoded `dest_net=1` in the `GET /pending-bridges` query string. For any L2 chain whose network ID is not `1`, Step E's bridge service cross-check queried deposits targeting a different chain, causing the check to silently pass or spuriously fail against the wrong deposit set. - The function now takes a `destNet` parameter and the caller passes `cfg.L2NetworkID`. ##⚠️ Breaking Changes - None. ## 📋 Config Updates - None. ## ✅ Testing - 🤖 **Automatic**: `TestFetchZkevmPendingBridges` now asserts the request's `dest_net` query param matches the configured network ID (locking the fix). All `tools/exit_certificate` unit tests pass. ## 🐞 Issues - Closes #1681 ## 📝 Notes - The aggkit bridge service path already filtered by `cfg.L2NetworkID` (`destination_network`); only the zkevm path was affected. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…in Step B (#1675) ## 🔄 Changes Summary - **Fix (High):** `fetchAllTokenBalances` (Step B1) silently discarded per-token RPC errors (`log.Warnf` + `return`), leaving the failed token absent from the balance map. Step C then treated its entire LBT supply as SC-locked and Step D routed the whole supply to `exitAddress`, excluding the real EOA holders from the certificate. - Made it **fail-fast**, consistent with its sibling helpers (`classifyAddresses`, `fetchETHBalances`): `fetchAllTokenBalances` now returns an `error` and uses `errgroup.WithContext` (limited to `tokenConcurrency`) to cancel the remaining scans on the first failure. `RunStepB1` propagates it and aborts the pipeline. - Added a doc comment explaining why the error must not be dropped. ##⚠️ Breaking Changes - None. No config, API, or CLI changes. Behaviour change only: a persistent per-token RPC failure now aborts the pipeline (after the existing RPC retries) instead of producing a misrouted certificate. ## 📋 Config Updates - None. ## ✅ Testing - 🤖 **Automatic:** Added `TestFetchAllTokenBalancesFailFast` (a failing `balanceOf` batch must return an error, never omit the token). Updated `TestFetchAllTokenBalances` to the new signature. Full package suite passes (`go test ./tools/exit_certificate/`) and `golangci-lint` reports 0 issues. ## 🐞 Issues - Closes #1679 ## 🔗 Related PRs - N/A ## 📝 Notes - Chose fail-fast as the default without adding an opt-out flag (e.g. `ignoreTokenBalanceFetchError`) to keep the config surface minimal; can add one if a knowingly-degraded test mode is desired. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…deABIString (#1684) ## 🔄 Changes Summary - Fix a uint64 integer overflow in `step_e.decodeABIString()` (SP002). A maliciously crafted L1 ERC-20 whose `name()`/`symbol()` returns an oversized ABI length field could make `64+strLen` wrap past `MaxUint64` to a small value, bypassing the bounds guard and triggering a slice out-of-range **panic** that aborts the exit certificate tool for any chain with an unclaimed L1→L2 deposit of such a token. - Reject length fields that don't fit in `uint64` (`big.Int.IsUint64()`) and compare `strLen` against the remaining byte count without the overflowing addition (`strLen > uint64(len(data))-twoABIWords`, safe since `len(data) >= 64`). ##⚠️ Breaking Changes - None. ## 📋 Config Updates - None. ## ✅ Testing - 🤖 **Automatic**: Added `TestDecodeABIString` with regression cases — `MaxUint64` length field (the overflow case) and a length field larger than `uint64` — asserting no panic and `""` return, plus valid/empty/too-short/length-exceeds-data cases. Full `go test ./tools/exit_certificate/...` passes; `go vet` and `gofmt` clean. - 🖱️ **Manual**: n/a (deterministic decode logic covered by unit test). ## 🐞 Issues - Closes #1680 ## 📝 Notes - The guard now also rejects length fields exceeding `uint64` range, which `big.Int.Uint64()` would have silently truncated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…uild version (#1688) ## 🔄 Changes Summary **Goal:** make every `exit_certificate` run **traceable** — at a glance you can tell which build, which config and which exact command produced a given output/log. To achieve that: - **run.go**: logs an *execution traceability* banner line-by-line (same format as the rest of the logs) at the start of `Run`, reporting: - build version, git revision, git branch and build date, - Go version and OS/Arch, - the **sha256 of the config file** (so the exact parameters used can be verified), - the **command-line arguments** the tool was invoked with. - **Makefile**: the `exit_certificate` build target now passes `-ldflags "all=$(LDFLAGS)"`, so `Version`, `GitRev`, `GitBranch` and `BuildDate` are injected into the binary (previously left at their defaults — without this the banner/`--version` would report placeholders). - **cmd/main.go**: `--version` prints the full build info (version, git revision, branch, build date, Go version, OS/Arch) via a custom `cli.VersionPrinter` calling `aggkit.PrintVersion`. ### Output example: ``` 2026-06-30T15:23:42.172+0200 INFO exit_certificate/run.go:31 ╔═══════════════════════════════════════════╗ {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:32 ║ Exit Certificate Tool — Traceability ║ {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:33 ╚═══════════════════════════════════════════╝ {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:34 Version: v0.10.0-rc1-72-gfc332f25 {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:35 Git revision: fc332f2 {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:36 Git branch: feat/exit-certificate-tool-version {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:37 Built: Tue, 30 Jun 2026 15:16:17 +0200 {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:38 Go version: go1.25.7 {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:39 OS/Arch: linux/amd64 {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:40 Config file: tmp/exit_certificate-kurtosis.json (sha256: e2f0a892dcb070f888037f0f88bf47232e7667f61e6449419fca9c92ba77ce01) {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} 2026-06-30T15:23:42.173+0200 INFO exit_certificate/run.go:41 Command line: ./target/exit_certificate -c tmp/exit_certificate-kurtosis.json {"pid": 560596, "version": "v0.10.0-rc1-72-gfc332f25"} ``` ##⚠️ Breaking Changes - None. ## 📋 Config Updates - None. ## ✅ Testing - 🤖 **Automatic**: `make build-exit_certificate` builds with injected ldflags. - 🖱️ **Manual**: `./target/exit_certificate --version` prints full build info; running any step logs the traceability banner (verified config sha256 + command line render correctly). ## 📝 Notes - `log/log.go` is shared infrastructure and is intentionally left untouched; the per-trace `version` field stays as-is for all binaries. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
b546dad to
2433abe
Compare
…is premints (genesisPrefundETHWei, capMode, nativeSCLockedFromContracts) (#1694) ## 🎯 Motivation / Context This PR adapts the exit-certificate tool to the particularities of **testnets**, where native funds are pre-loaded at genesis (e.g. 110,000 ETH on Kurtosis enclaves, 100,000 ETH on Cardona). That premint breaks several steps of the pipeline, because it inflates account balances without any matching agglayer deposit: - The **Step F** balance check fails: the certificate's native exits include the premint, but the agglayer balance and the Step 0 LBT only count genuinely bridged funds. - The **Step C** native SC-locked formula (`LBT − EOA_accumulated`) underflows, clamps to 0 and silently drops contract-held ETH from the certificate. - The premint itself has **no collateral on the agglayer**, so it can never be bridged out — the certificate must always be capped to the bridged amount. ## 🔄 Changes Summary - **`options.genesisPrefundETHWei`** — declares the native amount (Wei, decimal string) preminted at genesis: - **Step B** verifies the declared value against the detected genesis ETH preload total (sum of balances at block 0); a mismatch is fatal even with `ignoreGenesisBalance=true`, since Step F would otherwise discount the wrong amount. The genesis-preload error message now explains that pre-funding accounts at genesis is not allowed and points at `ignoreGenesisBalance=true`. - **Step F** subtracts the value from the native-token **certificate sum** before both the agglayer three-way and offline comparisons (via `discountGenesisPrefund`, floored at zero), logging the certificate total, the pre-fund and the difference — so the check balances against the genuinely bridged amount. - The pre-fund has **no agglayer collateral**, so even when every check matches Step F emits `step-f-capped-certificate.json` trimming the native exits to `min(agglayer, lbt)` — no `ignoreBalanceMismatch` needed. - Validated by `LoadConfig` (non-negative base-10 integer). - **`options.capMode`** — `"amount"` (default) or `"appearance"`. Controls how Step F allocates each token's cap budget when trimming exits. `"amount"` serves the smallest-amount exits first so the largest holders (the premint whales) are capped/dropped first; `"appearance"` serves exits in appearance order. Surviving exits keep their original order in both modes. `capCertificateExits` reworked into a greedy per-token allocator. - **`options.nativeSCLockedFromContracts`** — when `true` (the default), Step C measures the native token's SC-locked value directly by summing the ETH held by contract accounts at `targetBlock` (excluding the L2 bridge reserve), instead of deriving it as `LBT − EOA_accumulated`, which underflows on premint chains. Wrapped tokens keep the formula; set to `false` to fall back to the derivation for the native token. - **Step A no longer excludes the zero address** — `0x000…000` can hold value (e.g. burned funds) that the certificate must account for, so it stays in the collected address set. - **Step F agglayer LBT dump** — whenever `agglayerAdminURL` is set, Step F queries `admin_getTokenBalance` once up front and writes the raw LBT to `step-f-agglayer-lbt.json`, regardless of comparison mode (skipped when `OutputDir` is unset, i.e. hand-built configs). In agglayer mode the response is reused (no second RPC). Added `scripts/get-agglayer-lbt.sh` to fetch the same LBT manually. - **Step F mismatch logs** now include the signed `certificate−agglayer` / `certificate−lbt` differences (collapsed into one when LBT == agglayer). ##⚠️ Breaking Changes - `capMode` defaults to `"amount"`: with `ignoreBalanceMismatch=true`, the default cap outcome changes (previously appearance-order allocation). - `nativeSCLockedFromContracts` defaults to `true`: Step C now measures the native SC-locked value from contract balances by default (set to `false` for the previous `LBT − EOA` derivation). - Step A now includes the zero address in the collected set, so its balances can appear in the certificate. ## 📋 Config Updates Three new fields under `options` (all reflected in `parameters.toml.example` / `parameters.json.example`): - 🧾 **`genesisPrefundETHWei`** — amount of native token preminted at genesis, in **Wei**, as a **decimal string**. - *Purpose:* tells the tool how much native value exists on the chain without agglayer collateral. Step B verifies it against the preload actually detected at block 0 (a mismatch aborts, even with `ignoreGenesisBalance=true`); Step F discounts it from the native certificate sum before the balance comparison and always emits a capped certificate trimming the native exits to `min(agglayer, lbt)`, since the premint can never be bridged out. - *Values:* `""` (default — feature disabled, no discount and no premint capping) or any non-negative base-10 integer, e.g. `"100000000000000000000000"` (100,000 ETH). Negative or non-numeric values are rejected by `LoadConfig`. - 🧾 **`capMode`** — allocation order used when Step F trims a token's bridge exits to its cap budget. - *Purpose:* decides **which holders absorb the cut** when the certificate's exits exceed the collateral (mismatch with `ignoreBalanceMismatch=true`, or the premint capping above). - *Values:* `"amount"` (default) — serves the smallest exits first, so the largest holders (typically the premint whales) are capped/dropped first and small legitimate holders keep their full exits; `"appearance"` — serves exits in the order they appear in the certificate. Any other value is rejected by `LoadConfig`. Surviving exits keep their original order in both modes. - 🧾 **`nativeSCLockedFromContracts`** — how Step C computes the **native** token's SC-locked value (the amount bridged to `exitAddress`). - *Purpose:* the derived formula `LBT − EOA_accumulated` underflows on premint chains (EOA balances include the premint, the LBT does not), clamping to 0 and silently dropping contract-held ETH from the certificate. - *Values:* `true` (default) — measure it directly by summing `eth_getBalance` of every contract account at `targetBlock`, excluding the L2 bridge (its balance is the un-released native reserve); `false` — keep the `LBT − EOA` derivation. Wrapped tokens always use the formula, regardless of this flag. Example configs updated accordingly: - 🧾 `config-examples/zkevm-cardona.toml` sets `genesisPrefundETHWei` to the 100,000 ETH minted at genesis on Cardona. - 🧾 `scripts/configuration_based_on_kurtosis.sh` sets `genesisPrefundETHWei` to the 110,000 ETH preminted by the Kurtosis enclave (overridable via the `GENESIS_PREFUND_ETH_WEI` env var). ## ✅ Testing - 🤖 **Automatic**: `go test ./tools/exit_certificate/...` — added tests for the pre-fund discount and always-cap behaviour (`TestDiscountGenesisPrefund`, `TestCompareTokenBalances_GenesisPrefundDiscount`, `TestRunStepF_PrefundMatchedStillCapsToLBT`, `TestRunStepF_NoPrefundNoCapOnAllMatch`), the Step B preload verification (`TestCheckDeclaredGenesisPrefund`, `TestRunStepB1GenesisPrefundDeclared`), the Step C contract measurement (`TestRunStepC_NativeSCLockedFromContracts`, `TestSumContractNativeBalances`, `TestApplyNativeContractLocked`), cap allocation (`TestLoadConfig_CapMode`, `TestCapCertificateExits_ByAmount*`), config loading (`TestLoadConfig_GenesisPrefundETHWei`) and the LBT dump guard (`TestRunStepF_EmptyOutputDir_SkipsLBTDump`). - 🧪 **Manual**: full pipeline run against a Kurtosis enclave (`scripts/configuration_based_on_kurtosis.sh`). ## 📝 Notes - An `ignoreAddresses` option (exclude specific accounts from the certificate exits) was initially part of this PR but was removed before merging: it is not in use and complicated other in-flight PRs. The branch name still reflects it; the branch will be deleted after merge. - README/CLAUDE.md docs were audited against the code and corrected (e.g. `stepAWindowSize` default, `rollupManagerAddress`, Step D holder-bridge exits, conditional Step C/E outputs). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in Step G (#1698) ## 🔄 Changes Summary - Step G misclassified bridge exits for **external native tokens** (another network's native asset, represented on this L2 as a wrapped ERC-20 with `OriginTokenAddress=0x0` but a non-local `OriginNetwork`) as the **local gas token**, computing `NewLocalExitRoot` from the wrong leaves. The internal consistency check still passed because both the shadow-fork replay and the off-chain tree builder made the same misclassification. - `isNativeBridgeExit` (`step_g2.go`): removed the standalone `OriginTokenAddress == 0x0` condition. An exit is now native only when `TokenInfo == nil` or its origin identity (network **and** address) matches the gas token's. - `buildLiteTreeFromCertificate` (`step_g_events.go`): the off-chain tree builder now selects the leaf origin via `isNativeBridgeExit` instead of an address-only check, keeping it in lock step with the shadow-fork replay's native/ERC-20 decision. - `resolveTokenAddresses` already delegated to `isNativeBridgeExit`, so external native tokens now correctly resolve their L2 wrapped address instead of being skipped. ##⚠️ Breaking Changes - None. ## 📋 Config Updates - None. ## ✅ Testing - 🤖 **Automatic**: Added AET-01 regression case to `TestIsNativeBridgeExit` (`(OriginNetwork=5, OriginTokenAddress=0x0)` must not be native). Full package suite passes: `go test ./tools/exit_certificate/`. `golangci-lint run ./tools/exit_certificate/...` clean. ## 🐞 Issues - Closes #1693 ## 📝 Notes - The tool only supports ETH-gas-token chains (Step CHECK #7), so in practice `gasToken` is `(network=0, addr=0x0)`; the fix is general regardless of gas token identity. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…st-snapshot deposits cannot block the exit pipeline (#1704) ## 🔄 Changes Summary - New **optional** config field `options.l1EndBlock` (`uint64`, next to `options.l1StartBlock`; a plain L1 block number, `0` = unset). - **Step E**: the L1 `BridgeEvent` scan for unclaimed L1→L2 deposits now ends at the cutoff instead of the current latest L1 block (`resolveL1EndBlock`). The bridge service cross-check (both `aggkit` and `zkevm` flavours) filters out pending entries whose `block_num` is past the cutoff, logging how many were ignored. - **Step I**: the backward `UpdateL1InfoTreeV2` scan for `L1InfoTreeLeafCount` starts at the cutoff when set (via the same shared `resolveL1EndBlock`), making the leaf count deterministic and immune to post-snapshot L1 activity. - **Validation**: `LoadConfig` rejects an `l1EndBlock` below `options.l1StartBlock`; at run time `resolveL1EndBlock` rejects a cutoff beyond the current L1 head with an actionable error — it is almost surely a misconfiguration (e.g. an L2 block number), and some L1 clients otherwise fail the scan with a cryptic `invalid block range params`. - Docs updated (`README.md`, tool `CLAUDE.md`, `parameters.json.example`, `parameters.toml.example`). ## Why this fixes AET-03 (#1703) The attack in AET-03 works because the pipeline freezes the **L2** state at `targetBlock` (resolved once in Step 0), but Step E scanned **L1** up to whatever `eth_blockNumber` returned at run time. After the sequencer is stopped, an unprivileged user can submit a dust L1→L2 asset deposit; Step E picks it up, `isClaimed` on the frozen L2 is necessarily false, and with the default `ignoreUnclaimed=false` the pipeline aborts. Since the sequencer is down, the deposit can never be claimed — the block is **permanent**, and re-running never helps. `l1EndBlock` gives L1 the same frozen-cutoff semantics the L2 already has: the operator pins the scan to a block at/after the moment the sequencer was stopped, so any deposit submitted after that point is simply outside the scanned range and cannot abort the run. Two complementary holes are closed at the same time: 1. **Bridge service cross-check** — without the filter, the post-cutoff deposit would still appear in the service's pending set, trip the "service reports deposits not found by L1 scan" mismatch, and block the pipeline through that path instead. 2. **Step I `L1InfoTreeLeafCount`** — a post-snapshot deposit with `forceUpdateGlobalExitRoot` emits a new `UpdateL1InfoTreeV2`, so scanning from `latest` would reference attacker-influenced state; anchoring the scan at the cutoff keeps the referenced leaf pre-snapshot and already settled. When the field is unset (the default, `0`), behaviour is exactly as before (latest L1 block), so existing configs are unaffected. ##⚠️ Breaking Changes - None. The new field is optional and defaults to the previous behaviour. ## 📋 Config Updates - 🧾 **Diff/Config snippet** (JSON and TOML, same key): ```toml [options] # OPTIONAL — L1 cutoff block for the L1 reads (Step E unclaimed scan + bridge service # cross-check, Step I UpdateL1InfoTreeV2 scan). Pick a block at or after the sequencer # stop. Must be >= l1StartBlock and must not exceed the current L1 head. # Default: 0 (no cutoff: use the latest L1 block). l1EndBlock = 0 ``` ## ✅ Testing - 🤖 **Automatic**: `go test ./tools/exit_certificate/...` (all green) and `golangci-lint run ./tools/exit_certificate/...` (0 issues). New unit tests: - `TestResolveL1EndBlock` — no cutoff → latest; cutoff at/below the head → returned as-is; cutoff beyond the head → clear config error. - `TestFetchAggkitPendingBridgesCutoff`, `TestFetchZkevmPendingBridgesCutoff`, `TestFetchZkevmPendingBridgesCutoffBadBlockNum` — bridge service entries past the cutoff are dropped; unparseable `block_num` errors instead of being silently dropped. - `TestFetchL1InfoTreeLeafCount` (new `step_i_test.go`) — Step I scans from the cutoff when set, from latest otherwise. - `TestLoadConfig_L1EndBlock` — parse and the `l1StartBlock` validation. - 🖱️ **Manual** (Kurtosis, dust asset deposit at L1 block 41944): with `l1EndBlock = 41943` (cutoff before the deposit) Step E reports `Unclaimed L1→L2 deposits: 0` and completes; with a cutoff after the deposit it is detected as unclaimed and the step aborts as designed; with `l1EndBlock` past the L1 head the run fails fast with `options.l1EndBlock N is beyond the current L1 latest block M; ...` instead of the client's cryptic `invalid block range params`. ## 🐞 Issues - Closes #1703 ## 📝 Notes - Step CHECK and SUBMIT/WAIT intentionally keep using the latest L1 block: those uses are operational (connectivity check, capturing the block right before submission), not part of the frozen snapshot. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…T-02) and zero adress issue (AET-08) (#1701) ## 🔄 Changes Summary - **Step A replaced**: addresses are discovered via a `debug_accountRange` state dump + `Transfer` event logs per wrapped token, instead of `debug_traceTransaction` over the whole chain history — `O(#accounts)` vs `O(#txs)`, and it finds passive token-only holders that tracing structurally misses. Both sources always run and merge (the dump covers native-ETH holders and contracts; the logs cover token holders): they have complementary blind spots, so there is no strategy option and no fallback — an unusable `debug_accountRange` fails Step A instead of degrading silently. The trace-based A1/A2 implementation is removed. - **Zero address kept in Step A** — `0x00…00` can hold value like any other account (a plain `transfer(0x0, amount)` is not a burn and native ETH can be sent there); dropping it left that value uncovered and the certificate unbalanced against the LBT. - **Extra ERC-20 holders discovered in Step A** — the `Transfer`-log scan also covers `options.extraErc20Contracts` (deduplicated against the wrapped tokens, and it runs even with no wrapped tokens). Step B3 only probes `balanceOf` against the Step A address set, so a passive holder of an extra token (no ETH/nonce/code, never touched a wrapped token) was invisible to both sources and their collateral share flowed to `exitAddress`; discovery stays in Step A (B3 never discovers addresses), so `step-a-addresses.json` remains the complete address universe in single-step mode. - **`options.capMode` gains `"none"`, the new default** — capping the certificate is a lossy operation and must now be opted into explicitly. With `"none"`, Step F fails (`errCapForbidden`) as soon as any bridge exit would have to be trimmed or dropped, on every capping path: the mismatch cap (`ignoreBalanceMismatch=true`), the genesis pre-fund trim (which caps even on allMatch, so `genesisPrefundETHWei` now requires a trimming mode) and the final-certificate re-cap in the pipeline. A no-op cap (everything fits the budget) still passes. `"amount"`/`"appearance"` keep their previous trimming behavior. - The Kurtosis config generator (`configuration_based_on_kurtosis.sh`) now emits `capMode: "amount"` (its config declares `genesisPrefundETHWei`, whose premint trim needs a trimming mode) and `ignoreBalanceMismatch: false` (real mismatches must abort). - **E2E harness extended with the AET-02 scenario and split into numbered stages** (`test/e2e-exit_certificate/`): `10-run_network` → `20-prepare_network` → `30-stop_sequencer` → `40-generate_exit_certificate` → `45-check_exit_certificate` → `50-submit_exit_certificate` → `60-claim_exit_certificate_funds` (the old `run_exit_tool.sh` is gone). The sequencer stop now halts only the block producers (`op-batcher`/`op-cl`), keeping the `op-el` RPC alive so the tool can still read the frozen L2 state. ##⚠️ Breaking Changes - 🛠️ **Config**: `options.capMode` now defaults to `"none"`: runs that relied on the previous `"amount"` default to trim the certificate (any `genesisPrefundETHWei` run, or `ignoreBalanceMismatch=true` with real mismatches) now fail in Step F unless `capMode` is set explicitly to `"amount"` or `"appearance"`. - 🛠️ **Config**: `options.ignoreOnTraceError` and `options.stepAWindowSize` removed (the trace-based Step A and the receipt-harvesting fallback no longer exist). `options.addressDiscovery` (present in intermediate revisions of this PR, never released) is also gone — Step A always runs both sources. - 🔌 **API/CLI**: `--step a1` / `--step a2` removed (`a` has no sub-steps anymore); `step-a1-*`, `step-a2-*` and `step-a-failed-traces.json` are no longer produced. ## 📋 Config Updates - 🧾 `capMode` accepts `"none"` (new default) besides `"amount"` and `"appearance"`; `zkevm-cardona.toml` sets `capMode = "amount"` explicitly because its `genesisPrefundETHWei` always requires trimming. ## ✅ Testing - 🤖 **Automatic**: `go test ./tools/exit_certificate/...` and `golangci-lint` clean. New unit tests for Step A discovery (pagination, dialect detection, Transfer extraction, source merging, extra-ERC-20 scanning — including token dedup and the no-wrapped-tokens case —, truncation/empty-dump/dump-unavailable guards), zero-address handling, and capMode `"none"` (no-op pass-through, fail-on-trim, fail-on-drop, plus RunStepF-level prefund and mismatch failures). - 🧪 **E2E (covers the AET-02 fix end-to-end)**: `test/e2e-exit_certificate/run_e2e_test.sh` reproduces the exact bug scenario on a Kurtosis enclave — `20-prepare_network.sh` deploys a `MintableERC20` on L1, bridges it to L2 and transfers part of the wrapped balance to a **passive account** that never sends any L2 transaction (asserted: nonce 0 and zero native balance, so it is invisible to the state dump and only discoverable through `Transfer` logs). After generating the certificate, `45-check_exit_certificate.sh` asserts it contains exactly two bridge exits for that ERC-20 — the active holder and the passive recipient, each with the exact expected amount — before the certificate is submitted, settled on L1 and the funds claimed. With the old trace-based Step A this check fails (the passive recipient is missing); with the new Step A it passes. - 🖱️ **Manual**: the state-dump Step A approach was validated in #1687 against Bali (block 24,000,064): ~2m45s vs ~7h46m for the trace-based scan, 321,581 addresses (99.95% coverage of the trace set plus 5,987 extra real holders). ## 🐞 Issues - Closes #1699 — AET-02: passive ERC-20 recipients (accounts that only ever *received* tokens) are structurally invisible to `debug_traceTransaction`, so they were omitted from the certificate and their balances wrongly absorbed into the SC-locked residual. The `Transfer`-log discovery in the new Step A covers them. - #1700 — the zero-address fix included here (second commit) is what that issue asked for; the issue was already closed manually. - Closes #1707 — AET-08: `dialBridge()` binds the bridge contract without rejecting a zero `BridgeAddr`, so the lite syncer would silently scan the wrong address and `BuildTree()` would return the zero hash as the LER without any error. Not addressed here (still open, tracked elsewhere): #1680 is already fixed by #1684 (merged into the feature branch; it will auto-close when the feature branch reaches `develop`), and #1693 (AET-01) is being handled in `fix/exit-certificate-tool_aet01-native_token`. ## 🔗 Related PRs - Imports and supersedes #1687 (its Step A lands here as the default, not as the opt-in `aalt` step) - Rebased on top of #1694, which already landed `genesisPrefundETHWei`, `capMode`, `nativeSCLockedFromContracts` and the Step F agglayer LBT dump — those changes are no longer part of this PR. The `ignoreAddresses` option was dropped intentionally (no longer wanted). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
…deployment (#1690) ## 🔄 Changes Summary Packaging and deployment preparation for the exit_certificate_claimer service. - 🐳 **Dockerfile**: ship the `exit_certificate` (generator) and `exit_certificate_claimer` (HTTP service) binaries in the aggkit image under `/usr/local/bin`, so the tools travel alongside `aggkit` and the same image can be used to deploy the exit_certificate_claimer. - 📦 **docker-compose**: add `tools/exit_certificate_claimer/docker/docker-compose.yml` to run the exit_certificate_claimer as a read-only HTTP service. It builds from the repo-root Dockerfile, overrides the entrypoint with the exit_certificate_claimer binary, and derives its whole config from the exit_certificate `parameters.toml` via `--exit-certificate-config`. Mounts the exit_certificate working dir at `/data` (read-write, since L1 sync keeps its SQLite DBs updated) and wires `host.docker.internal` so a host-pointing `l1RpcUrl` is reachable on Linux. Defines a `healthcheck:` and documents the volume-permission expectations (the container runs as `appuser`, uid 1000). - 🩺 **healthcheck subcommand**: `exit_certificate_claimer healthcheck` probes `GET /claimer/v1/health` of a running claimer and exits 0 (healthy) or 1. The production image ships without a shell or curl, so the binary itself performs the HTTP probe; the docker-compose healthcheck is backed by it. - 📜 **JSON logs**: new `--log-json` flag switches the claimer logs to JSON format (aggkit's `production` log environment, zap JSON encoder); the default remains the human-readable console format. Fatal startup errors are emitted through the configured logger too (JSON or console), instead of urfave/cli's plain `Error: ...` line. Exposed in docker-compose via `CLAIMER_LOG_JSON` (default `false`). - 🧪 **e2e docker mode**: `60-claim_exit_certificate_funds.sh` gains `CLAIMER_MODE=docker` to run the claimer through the docker-compose deployment instead of a host binary (a sibling config copy rewrites `127.0.0.1`/`localhost` to `host.docker.internal` so the container reaches the Kurtosis-published L1 RPC). The CI workflow sets it, so the e2e exercises the same artifact users deploy — image build from the repo-root Dockerfile included — and now also triggers on `tools/exit_certificate_claimer/**` and `Dockerfile` changes. The compose service user is parameterizable (`CLAIMER_USER`, default `appuser`) so L1 sync can write its SQLite DBs whatever the host uid. - ⚙️ **.env.example**: documented env vars (`EXIT_TOOL_DIR`, `EXIT_CERT_CONFIG`, `CLAIMER_ADDRESS`, `CLAIMER_PORT`, `CLAIMER_LOG_JSON`, `AGGKIT_IMAGE`) consumed by compose. - 🩹 **e2e fix**: `agglayer_status` (`agglayer_certificate_status.sh --wait`) now retries on the transient agglayer error raised right after the network starts, before the node has classified the network: ``` ERROR: get network info: failed to get network info: Code: NotFound, Message: Network type could not be determined, Details: [Reason: GET_NETWORK_INFO_ERROR_KIND_UNKNOWN_NETWORK_TYPE, Domain: agglayer-node.grpc-api.v1.node-state-service.get_network_info. ] ``` `waitForSettled` previously aborted on any `GetNetworkInfo` error, making the e2e `prepare_network.sh` step fail intermittently. This specific error is now treated as transient and polling continues (respecting the configured interval and timeout); other errors still abort. ##⚠️ Breaking Changes - None. ## 📋 Config Updates - New env-var driven setup for the exit_certificate_claimer (see `.env.example`). No changes to the existing TOML config. - New optional `--log-json` CLI flag (`CLAIMER_LOG_JSON` in compose) to emit JSON-formatted logs; defaults to the previous console format. ## ✅ Testing - 🖱️ **Manual**: - `make build-tools` / `go build ./tools/exit_certificate/... ./tools/exit_certificate_claimer/...` build cleanly. - From `tools/exit_certificate_claimer/docker/`: `cp .env.example .env` then `docker compose up --build` starts the exit_certificate_claimer serving on `CLAIMER_PORT`. - `exit_certificate_claimer healthcheck` verified against a live health endpoint (exit 0) and a closed port (exit 1). - Ran the claimer with and without `--log-json` and verified the log output switches between the zap JSON encoder and the console format, including fatal startup errors (logged as an error line in the active format, exit code 1). - 🧪 **Unit**: `go test ./tools/exit_certificate/scripts/agglayer_status/` covers the new retry-on-undetermined-network-type path; `go test ./tools/exit_certificate_claimer/...` covers the healthcheck probe (URL building, 200/non-200/unreachable, end-to-end against the real router). - 🚀 **E2E**: the exit-certificate tool E2E (CI) runs the claim step with `CLAIMER_MODE=docker`, validating the full docker deployment end to end: compose build of the aggkit image, container startup deriving the config, in-container L1 sync, `/claimer/v1/health` readiness and claiming every bridge exit against the containerized service. ## 📝 Notes - The exit_certificate_claimer runs from the same working directory the exit_certificate tool used (config + `output/`), so no separate config file is needed. - PR base: `feature/exit-certificate-tool` (packaging/deployment work only). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>



🔄 Changes Summary
New
exit-certificateCLI tool undertools/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 agglayerCertificateready for submission.Full pipeline: CHECK → 0 → A → B → C → D → E → F → G → H → I → SIGN → SUBMIT → WAIT
NewWrappedToken/SetSovereignTokenAddressevents, fetchestotalSupplyper tokendebug_traceTransactionwithprestateTracer+diffModeCertificatewithBridgeExitentriesadmin_getTokenBalance; proportional capping on mismatchNewLocalExitRootcomputation via Anvil shadow-fork of the L2 chainPreviousLocalExitRootfrom agglayer via gRPCL1InfoTreeLeafCountfrom L1)go_signerSettledorInErrorSteps can be run individually or as a full pipeline via
--stepflag. Intermediate results are persisted to an output directory, enabling step-by-step execution and resumability.Additional changes in this PR:
options.agglayerAdminToken)hex,rpc,config,step_f, andstep_gutilities📋 Config Updates
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
tools/exit_certificate/(hex, rpc, config, step_c, step_f, step_g)🐞 Issues
📝 Notes
anvil(Foundry) in$PATHfor Step Gdebug_traceTransaction)