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
|
- 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>
ac0936d to
2122813
Compare
Revert the step A split (A1 + A2) back to a single RunStepA to avoid OOM caused by json.MarshalIndent serialising all tx hashes while they were still live in memory. Add a new SIGN step that signs the exit certificate with a local keystore and exposes signerKeyPath / signerKeyPassword in config and as CLI flags. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ContinueOnTraceError config option so Step A skips transactions whose debug_traceTransaction call fails instead of aborting. Failed tx hashes are collected in StepAResult.FailedTraces and always saved to step-a-failed-traces.json alongside the other Step A outputs. Also fix README and cmd usage strings to reflect the reverted A1/A2 split and document the new sign step and signer config fields. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the second eth_getBlockByNumber round-trip (with full tx objects). The headers-only call (false) already returns transaction hashes in the transactions array, so extractTxHashes and parseTxHashesFromResults are no longer needed. Update README to reflect the simplified two-phase flow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Step G: computes NewLocalExitRoot by replaying all bridge exits against an Anvil shadow-fork of the L2 chain. Uses debug_traceCall to discover LBT storage slots and hardhat_setStorageAt to unlock them before each bridgeAsset replay; falls back to EmptyLER when there are no bridge exits. Step H: fetches PreviousLocalExitRoot from the agglayer via interop_getNetworkInfo (requires agglayerRpcUrl in options). Step I: assembles the final certificate by applying NewLocalExitRoot (from G) and PreviousLocalExitRoot (from H) into exit-certificate-final.json. Step SUBMIT: sends the signed certificate to the agglayer over gRPC (requires agglayerGrpcUrl in options). Not part of the default pipeline; run with --step submit. Guards against submission when a pending certificate already exists for the network. The default pipeline is now 0 → A → B → C → D → E → F → G → H → I → SIGN. Config gains agglayerRpcUrl and agglayerGrpcUrl options. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ript Replace signerKeyPath/signerKeyPassword in Config with signertypes.SignerConfig, matching the same type aggsender uses for AggsenderPrivateKey. The JSON config now uses a flat signerConfig object (Method, Path, Password at top level). Update configuration_based_on_kurtosis.sh to download the sequencer keystore from the aggkit-sequencer-keystore artifact and extract the password from config.toml, producing a fully configured signerConfig block. Also add agglayerRpcUrl and agglayerGrpcUrl (needed for steps H and SUBMIT). Remove obsolete --signer-key-path/--signer-key-password CLI flags. Align README.md and CLAUDE.md with the current pipeline (steps H, I, SUBMIT, corrected step order, updated config fields and output file names). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion When a chain uses a custom gas token (non-zero origin address, e.g. a token from network 0), resolveTokenAddresses was falling through to the wrapped-token lookup path and failing because getTokenWrappedAddress returns zero — the token is native, not a wrapped ERC-20. Fix: fetch gasTokenInfo from the real L2 RPC in RunStepG (same pattern as step 0) and pass it to resolveTokenAddresses. Bridge exits whose TokenInfo matches the gas token are now treated as isNative=true, so bridgeAsset is called with token=address(0) as the contract expects. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, and align docs - Add step_check.go: prerequisite verification (Anvil, L1 RPC reachable, L2 network ID match, sovereignRollupAddr, PP type, threshold=1, gas token) - Add step_wait.go: polls agglayer every 5s until certificate is Settled or InError; handles pre-existing pending certs before polling submitted one - Add l1GlobalExitRootAddress config field; step I now fetches L1InfoTreeLeafCount by scanning L1 backwards for UpdateL1InfoTreeV2 events - Fix step I: use resolveLatestBlock(L1) instead of ResolvedTargetBlock (L2) to determine the scan starting point for L1 events - kurtosis script: extract polygonZkEVMGlobalExitRootAddress from config.toml and write it as l1GlobalExitRootAddress in the generated config - Align CLAUDE.md and README.md with actual implementation: pipeline order, Step CHECK full check list, Step I L1InfoTreeLeafCount, Step WAIT section, config fields sovereignRollupAddr/l1GlobalExitRootAddress, Step G reads Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…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>
|





🔄 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)