feat(exit-certificate): adapt certificate generation to testnet genesis premints (genesisPrefundETHWei, capMode, nativeSCLockedFromContracts)#1694
Conversation
Add an optional options.ignoreAddresses list of accounts whose balances must not be returned to them in the exit certificate. Step B1 still fetches their ETH and token balances (recorded in step-b-ignored-balances.json for traceability) but excludes them from both EOABalances and Accumulated via extractIgnoredBalances. Because their value no longer counts as EOA-held, it rolls into the per-token SC-locked total and Step D bridges it to exitAddress, so the certificate still balances against the LBT total (Step F stays green) and no exit is ever created back to the ignored address. LoadConfig validates each entry as a non-zero hex address. Documented in the tool CLAUDE.md and both parameters examples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When Step F caps a certificate (ignoreBalanceMismatch=true), the new options.capMode selects how each token's budget is allocated to its exits: "appearance" (default, the previous behavior) serves exits in the order they appear; "amount" serves the largest-amount exits first, so big holders are kept intact and small ones are capped/dropped once the budget runs out. In both modes the surviving exits are emitted in their original order, so downstream ordering (Step G) is unaffected. Refactor capCertificateExits to compute the per-exit outcome over a priority order (capAllocationOrder) and emit in original order; extract capExitCopy. LoadConfig validates capMode. Docs and both parameters examples updated; stale "proportional scaling" wording corrected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flip the CapModeByAmount allocation order from largest-first to smallest-first, so the budget is consumed by the small exits and the largest-amount exits are the first to be capped/dropped once it runs out. Surviving exits are still emitted in their original order. Update tests and docs accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make "amount" the default cap allocation mode (largest exits capped first) instead of "appearance". Update the default, tests, docs and both parameters examples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step F now queries admin_getTokenBalance once at the very start whenever agglayerAdminURL is configured and writes the full response (the agglayer's local balance tree for l2NetworkId) to step-f-agglayer-lbt.json before any comparison runs. The dump therefore persists even when Step F later fails on a balance mismatch. In agglayer mode the same response is reused for the comparison to avoid a second RPC round-trip. Also adds scripts/get-agglayer-lbt.sh, a standalone curl helper that issues the same admin_getTokenBalance request from the command line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-agglayer-lbt filename Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fbd239ec2b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…r LBT dump in README Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ken is optional Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- stepAWindowSize default is 150000, not 5000 (README + stale struct comment) - document rollupManagerAddress config field (used by Step WAIT) - Step D also builds ERC-20 holder-bridge exits (3rd category) - Step C also writes step-c-holder-bridges.json - step-e-exit-certificate.json is conditional (not written on unclaimed-abort path) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds new exit-certificate configuration knobs to handle testnet “genesis-preload” balances (via address exclusion and configurable capping strategy), and improves Step F traceability by always persisting the agglayer-side LBT when an admin endpoint is configured.
Changes:
- Introduces
options.ignoreAddresses(validated) and wires Step B outputs to record+exclude those balances (step-b-ignored-balances.json). - Adds
options.capMode(amountdefault /appearance) and updates Step F capping logic to allocate per-token budget according to the selected mode. - Updates Step F to dump agglayer LBT to
step-f-agglayer-lbt.jsonwhenagglayerAdminURLis set, plus adds a helper script to fetch it manually.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tools/exit_certificate/types.go | Extends Step B/F result structs and clarifies capped-certificate semantics. |
| tools/exit_certificate/step_f.go | Adds agglayer LBT dump + capMode-aware capping allocator implementation. |
| tools/exit_certificate/step_f_test.go | Updates existing cap tests and adds new coverage for CapModeByAmount. |
| tools/exit_certificate/step_b.go | Extracts and removes ignored-address balances before building EOA/accumulated outputs. |
| tools/exit_certificate/step_b_helpers_test.go | Adds unit test for extractIgnoredBalances. |
| tools/exit_certificate/scripts/get-agglayer-lbt.sh | New helper script to fetch agglayer LBT via admin_getTokenBalance. |
| tools/exit_certificate/run.go | Persists ignored-balances output and threads capMode into final-certificate capping. |
| tools/exit_certificate/README.md | Documents new options and updated Step F behavior/output. |
| tools/exit_certificate/parameters.toml.example | Adds capMode and ignoreAddresses examples and updates Step F description. |
| tools/exit_certificate/parameters.json.example | Adds capMode and ignoreAddresses examples. |
| tools/exit_certificate/filenames.go | Adds new output filenames for ignored balances and agglayer LBT dump. |
| tools/exit_certificate/config.go | Adds options, defaults, validation, and merging for ignoreAddresses and capMode. |
| tools/exit_certificate/config_test.go | Adds tests for ignoreAddresses and capMode validation/defaulting. |
| tools/exit_certificate/CLAUDE.md | Updates internal tool documentation to reflect new options and Step F LBT dump. |
…genesis-minted native in Step F Genesis pre-funded native tokens inflate the native-token LBT total without a matching agglayer deposit, forcing the Step F comparison to mismatch (and the certificate to be capped). The new options.genesisPrefundETHWei lets the operator declare that amount (in Wei); Step F subtracts it from the native LBT entry (the gas token, identified by a zero WrappedTokenAddress) before both the agglayer and offline comparisons — flooring at zero — so the check balances against the genuinely bridged amount. Affects the Step F comparison and cap budget only; the Step 0 LBT and Step C SC-locked totals are untouched. Validated by LoadConfig as a non-negative base-10 integer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The option is not in use and complicates other in-flight PRs. Removes the Options field, LoadConfig validation, extractIgnoredBalances in Step B1, the step-b-ignored-balances.json output, and all related tests, examples and docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dona example config Cardona minted 100000 ETH at genesis, which inflates the native LBT total without a matching agglayer deposit; declaring it lets Step F balance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RunStepF wrote step-f-agglayer-lbt.json via cfg.Options.OutputDir, which is empty for programmatically built configs (unit tests), dropping the file in the process's working directory. LoadConfig always sets OutputDir, so an empty value now skips the dump. Tests cover both the dump and the skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted genesis preload The genesis-preload error now explains that pre-funding accounts at genesis is not allowed and points at options.ignoreGenesisBalance=true to accept it. When options.genesisPrefundETHWei is set, Step B additionally checks that the declared amount equals the detected genesis ETH preload total. A mismatch is fatal even with ignoreGenesisBalance=true, since Step F would subtract the wrong amount from the native LBT entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cate sum, not the LBT The Step 0 native LBT only counts funds bridged in through the bridge contract (balance at block 0 minus balance at target), so it already matches the agglayer — the genesis pre-fund never inflates it. The inflated side is the certificate: the pre-funded balances sit in accounts and become bridge exits. Subtracting from the LBT also zeroed the cap budget (min(agglayer, lbt)), which would drop every native exit when capping. Step F now discounts the pre-fund from the native-token certificate sum in both comparison modes, logging the certificate total, the declared pre-fund and the resulting difference. The cap budget is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… logs Each ❌ MISMATCH line now includes the signed certificate−agglayer and/or certificate−lbt difference, so the size of the discrepancy is readable without manual arithmetic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ch log When the LBT and agglayer amounts are equal, certificate−agglayer and certificate−lbt are the same value; show a single difference in that case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The zero address can hold value (e.g. burned funds) that the certificate must account for, so Step A1/A2 and the merge no longer drop it from the collected address set, and receipt parsing no longer filters it out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… native SC-locked from contract balances (from #1687) On chains with a native genesis premint, the Step C formula LBT − EOA_accumulated underflows for the native token (EOA balances include the premint but the LBT only measures bridge outflow), gets clamped to 0, and the ETH actually held by contracts silently disappears from the certificate. With options.nativeSCLockedFromContracts=true, Step C instead measures the native SC-locked value directly: it sums eth_getBalance of every Step B contract at the target block (excluding the L2 bridge, whose balance is the un-released native reserve) and uses that as the native token's SC-locked value. Wrapped tokens keep the LBT − EOA formula. Defaults to false. In this mode --step c needs the L2 RPC, the Step 0 target block and step-b-contract-addresses.json (run Step B first). Usually combined with options.genesisPrefundETHWei so the Step F comparison also accounts for the premint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measuring the native SC-locked value from actual contract balances is the correct behaviour on any chain (and required on premint chains, where LBT − EOA underflows); the LBT − EOA derivation remains available by explicitly setting the option to false. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
|
Claude finished @joanestebanr's task in 1m 57s —— View job PR Review Complete ✅🚀 What's NewThis PR adapts the exit-certificate tool for testnets where funds are pre-loaded at genesis (typically 100,000 ETH). Genesis-injected native funds inflate the native-token LBT total without a matching agglayer deposit, causing Step F balance check failures. The PR introduces:
🐛 Bug FixesNone - this is primarily a feature enhancement for testnet support. 📋 Config Updates
|
…ig generator The Kurtosis enclave premints 110000 ETH at genesis, so the generated config now declares it in options.genesisPrefundETHWei (overridable via the GENESIS_PREFUND_ETH_WEI env var) for the Step F discount and the Step B preload verification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f7ad6bc5c9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ficate capCertificateExits budgeted every token with a RemainingBalance, including matched ones. That was a harmless no-op before (match implied sum == budget), but with the genesis pre-fund discount the native token can match while its raw exits exceed min(agglayer, lbt) — so capping triggered by an unrelated token would silently trim the pre-funded ETH. Matched tokens are now skipped, which is also what the docs already described. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yer, lbt) The genesis pre-fund has no agglayer collateral, so it can never generate bridge exits. Reverts the previous skip-matched-tokens capping: every token is budgeted again (a no-op for matched tokens except the discounted native), and finalizeStepFResult now produces the capped certificate even when every check matches — trimming the native exits to min(agglayer, lbt) without requiring ignoreBalanceMismatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
0aa3e8c
into
feature/exit-certificate-tool
…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>
…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>
…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>



🎯 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:
agglayer balance and the Step 0 LBT only count genuinely bridged funds.
LBT − EOA_accumulated) underflows, clamps to 0 andsilently drops contract-held ETH from the certificate.
certificate must always be capped to the bridged amount.
🔄 Changes Summary
options.genesisPrefundETHWei— declares the native amount (Wei, decimal string) preminted atgenesis:
balances at block 0); a mismatch is fatal even with
ignoreGenesisBalance=true, since Step Fwould 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.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.
step-f-capped-certificate.jsontrimming the native exits tomin(agglayer, lbt)— noignoreBalanceMismatchneeded.LoadConfig(non-negative base-10 integer).options.capMode—"amount"(default) or"appearance". Controls how Step F allocates eachtoken's cap budget when trimming exits.
"amount"serves the smallest-amount exits first so thelargest holders (the premint whales) are capped/dropped first;
"appearance"serves exits inappearance order. Surviving exits keep their original order in both modes.
capCertificateExitsreworked into a greedy per-token allocator.
options.nativeSCLockedFromContracts— whentrue(the default), Step C measures the nativetoken'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, whichunderflows on premint chains. Wrapped tokens keep the formula; set to
falseto fall back to thederivation for the native token.
0x000…000can hold value (e.g. burned funds)that the certificate must account for, so it stays in the collected address set.
agglayerAdminURLis set, Step F queriesadmin_getTokenBalanceonce up front and writes the raw LBT tostep-f-agglayer-lbt.json,regardless of comparison mode (skipped when
OutputDiris unset, i.e. hand-built configs). Inagglayer mode the response is reused (no second RPC). Added
scripts/get-agglayer-lbt.shto fetchthe same LBT manually.
certificate−agglayer/certificate−lbtdifferences (collapsed into one when LBT == agglayer).
capModedefaults to"amount": withignoreBalanceMismatch=true, the default cap outcomechanges (previously appearance-order allocation).
nativeSCLockedFromContractsdefaults totrue: Step C now measures the native SC-locked valuefrom contract balances by default (set to
falsefor the previousLBT − EOAderivation).certificate.
📋 Config Updates
Three new fields under
options(all reflected inparameters.toml.example/parameters.json.example):genesisPrefundETHWei— amount of native token preminted at genesis, in Wei, as adecimal string.
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 beforethe balance comparison and always emits a capped certificate trimming the native exits to
min(agglayer, lbt), since the premint can never be bridged out.""(default — feature disabled, no discount and no premint capping) or anynon-negative base-10 integer, e.g.
"100000000000000000000000"(100,000 ETH). Negative ornon-numeric values are rejected by
LoadConfig.capMode— allocation order used when Step F trims a token's bridge exits to its capbudget.
collateral (mismatch with
ignoreBalanceMismatch=true, or the premint capping above)."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 othervalue 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).LBT − EOA_accumulatedunderflows on premint chains (EOAbalances include the premint, the LBT does not), clamping to 0 and silently dropping
contract-held ETH from the certificate.
true(default) — measure it directly by summingeth_getBalanceof every contractaccount at
targetBlock, excluding the L2 bridge (its balance is the un-released nativereserve);
false— keep theLBT − EOAderivation. Wrapped tokens always use the formula,regardless of this flag.
Example configs updated accordingly:
config-examples/zkevm-cardona.tomlsetsgenesisPrefundETHWeito the 100,000 ETH minted atgenesis on Cardona.
scripts/configuration_based_on_kurtosis.shsetsgenesisPrefundETHWeito the 110,000 ETHpreminted by the Kurtosis enclave (overridable via the
GENESIS_PREFUND_ETH_WEIenv var).✅ Testing
go test ./tools/exit_certificate/...— added tests for the pre-fund discountand always-cap behaviour (
TestDiscountGenesisPrefund,TestCompareTokenBalances_GenesisPrefundDiscount,TestRunStepF_PrefundMatchedStillCapsToLBT,TestRunStepF_NoPrefundNoCapOnAllMatch), the Step B preload verification(
TestCheckDeclaredGenesisPrefund,TestRunStepB1GenesisPrefundDeclared), the Step C contractmeasurement (
TestRunStepC_NativeSCLockedFromContracts,TestSumContractNativeBalances,TestApplyNativeContractLocked), cap allocation (TestLoadConfig_CapMode,TestCapCertificateExits_ByAmount*), config loading (TestLoadConfig_GenesisPrefundETHWei) andthe LBT dump guard (
TestRunStepF_EmptyOutputDir_SkipsLBTDump).(
scripts/configuration_based_on_kurtosis.sh).📝 Notes
ignoreAddressesoption (exclude specific accounts from the certificate exits) was initiallypart 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.
stepAWindowSizedefault,rollupManagerAddress, Step D holder-bridge exits, conditional Step C/E outputs).🤖 Generated with Claude Code