feat(bridge_ui): opt-in aggkit bridge-service backend via aggkit-proxy, 2-L2 enclave + bridge tracker (rc5) - #929
feat(bridge_ui): opt-in aggkit bridge-service backend via aggkit-proxy, 2-L2 enclave + bridge tracker (rc5)#929arnaubennassar wants to merge 45 commits into
Conversation
- Add `bridge_ui_backend` arg (aggkit | bridge_hub, default bridge_hub so
existing behavior is preserved). In aggkit mode, bridge_ui deploys only
a CORS-injecting haproxy proxy exposing `/aggkitapi/*` -> aggkit bridge
REST, skipping the bridge-hub mongo/consumers/api/autoclaimer stack.
- haproxy: multi-L2-shaped aggkit backend list + direct OPTIONS->204 CORS
preflight.
- Fix `_extract_aggkit_version` to fall back gracefully on non-numeric
image tags (was crashing on float("feat")).
- Update aggkit config template `[REST]` -> `[PublicREST]`.
- Add params-aggkit-ui.yml example (op-reth + target aggkit image +
aggkit bridge_ui backend).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4tXyxxSecCKHA4uqsiN81
…ettle The feat-autoclaim-l2-lx aggkit build routes every certificate through an on-chain multisig validator committee. With `pessimistic` consensus the committee interface isn't deployed, so aggsender's committee query reverts, no certificate is submitted to agglayer, and L2 exit roots never settle to L1 — making every L2->L1 exit permanently unclaimable (l1-info-tree-index for the L2 stays "not found"). Switch params-aggkit-ui.yml to `ecdsa-multisig` (deploys AggchainECDSAMultisig) with use_agg_sender_validator + a single-signer committee (threshold 1), per the tested op-reth ecdsa-multisig reference. Verified on the enclave: aggsender committee errors clear, certificates settle, l1-info-tree-index resolves for L2 deposits, and L2->L1 becomes claimable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4tXyxxSecCKHA4uqsiN81
Adds params-aggkit-l2l2-run1.yml / run2.yml: two kurtosis run invocations into the same enclave (rollup 1 + rollup 2, network_id 1/2, aggkit-00X + aggkit-00X-bridge each) on ghcr.io/agglayer/aggkit:develop_2026_08_03_13_20_56c849c. - src/chain/shared/aggkit.star: _extract_aggkit_version chokes on underscore-only CI tags like develop_2026_08_03_13_20_56c849c (no "-" delimiter, digit-scan lands on the date but the remainder isn't a clean float) -> invalid float literal. Add _is_simple_version() gate, fall back to 999.9 (latest/grpc) like the existing local/no-digit tolerances. - src/agglayer.star + static_files/agglayer/config.toml + src/package_io/input_parser.star: agglayer's config.toml hardcoded rollup 1 in [full-node-rpcs]/[proof-signers], so a second rollup's certificates would never settle. Add agglayer_extra_rollups (default []) so a run that deploys agglayer can pre-declare later rollups (deterministic op_el_rpc_url + shared default sequencer key), rendered via a template range loop. No agglayer image change and no post-hoc patch+restart needed. Verified end-to-end: L1->L2-1, L1->L2-2, L2-1->L1, L2-1->L2-2 and L2-2->L1 bridge txs all indexed correctly by both aggkit-00X-bridge instances, and both rollup-1-origin and rollup-2-origin exits settle to an l1-info-tree-index via the shared agglayer. See plans/aggkit-proxy-l2l2/enclave-notes.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…ns 1 & 2, never L1) Adds an "autoclaim" component to each per-chain aggkit service (aggkit-001, aggkit-002), gated by new aggkit_autoclaim_destinations / aggkit_autoclaim_bridge_urls args, so L1->L2 and L2->L2 deposits auto-claim while L2->L1 deposits stay claimable for the bridge UI's manual path. Each instance runs exactly one claimer targeting its own network_id (never 0/L1), reusing the already-mounted aggoracle keystore as the claimer's signer. Verified in a fresh 2-L2 enclave: L1->L2-1 auto-claims in ~67s with zero manual action; L2-1->L1 never auto-claims over a 5m41s window while claim-proof succeeds and a manual claim lands. L2-1->L2-2 discovery/tracking is confirmed working end-to-end, but claim submission is blocked by an upstream aggkit bug (autoclaim/claimer.go's proofReadyForRequest compares an L1 block number against the source rollup's own L2 block number for L2ToLx requests) -- root-caused and documented in enclave-notes.md, not a kurtosis-cdk defect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
Deploys aggkit-proxy-001 (--components=proxy) fronting the per-chain aggkit bridge REST services with static BridgeServiceFinder overrides for networks 0/1/2, since this package registers no on-chain BRIDGE_SERVICE_URL aggchain metadata and network 0 is never enumerated on-chain at all. Wired as a new additional service (aggkit_proxy) launched from run2, with devnet-tuned BridgeServiceFinder settings (LatestBlock, 10s poll, HealthCheckPath "/") matching aggkit's own configuration_based_on_kurtosis.sh recipe.
…utes - haproxy's single /aggkitapi backend now points at the standalone aggkit-proxy service (aggkit_proxy_url arg) instead of one chain's own bridge REST instance, so it fronts networks 0/1/2 through one backend. - Generalize the L2 RPC route to an args-driven per-chain list (bridge_ui_l2_rpc_urls): /l2rpc-001, /l2rpc-002, with the bare /l2rpc route kept as a back-compat alias to chain 1. - Wire bridge_ui + the new args into params-aggkit-l2l2-run2.yml (runs alongside aggkit_proxy, once both bridge services exist). - Switch aggkit_image on both run1/run2 params to the locally patched aggkit:fix-autoclaim-l2tolx-local image (fixes the S3 L2->L2 autoclaim block-domain bug). Verified via a full fresh enclave bring-up (enclave rm -f cdk -> run1 -> run2): CORS-safe /aggkitapi routing with correct headers, OPTIONS preflight 204, per-chain /l2rpc-00X + /l1rpc eth_chainId, default bridge_hub mode still dry-run-renders, and the proxy still routes networks 0/1/2 from a clean template render (no hand-patching needed, unlike S4's first bring-up).
Add comprehensive documentation for the 2-L2 devnet configuration with AggKit proxy and automated claim routing: - New advanced guide: `docs/docs/advanced/aggkit-2l2-with-bridge-ui.md` covering: - 2-L2 deployment recipe (two-run params files + bring-up commands) - AggKit proxy configuration (network_id-based multiplexing) - Autoclaim setup (per-chain destination lists + static bridge URL maps) - HAProxy route map (/l1rpc, /l2rpc, /l2rpc-001, /l2rpc-002, /aggkitapi) - E2E testing across all bridging routes (L1↔L2-1, L1↔L2-2, L2-1↔L2-2) - Certificate cadence and timeout considerations - Image version note (locally-patched pending upstream PR #1761) - Troubleshooting guide - Updated `docs/docs/configuration/examples/bridge-ui.md` to add cross-reference to the new 2-L2 guide These docs enable a new developer to bring up and test the complete 2-L2 + proxy setup using only the documented parameter files and commands, following the established bridge UI documentation patterns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…ce claim, drop dangling refs Findings from the S14 adversarial review, all documentation/comment-level (no behavior change; both params files re-validated with `kurtosis run --dry-run`, plus params-aggkit-ui.yml for default bridge_hub-mode preservation). 1. Local image was an undocumented hard prerequisite. Both params files pin `aggkit:fix-autoclaim-l2tolx-local`, a tag that exists on no registry, but nothing in this repo said how to build it or what failure to expect without it -- a colleague cloning this branch hit an opaque image-not-found error at service creation. The "Image Version" section now carries the git checkout + `docker build` command, the two-binary verification commands, the exact failure mode, and why the patch is needed (unpatched develop never fires L2->L2 autoclaim). Prerequisites now points at it explicitly. 2. The "Certificate Cadence" note was wrong about the mechanism. It described `MinimumNewCertificateInterval: 5m0s` as a minimum spacing between certificate sends and concluded the E2E L2->L2 budget had "zero margin". `fulfillMinimumInterval` (aggkit aggsender/trigger/trigger_asap.go) only fires when no other trigger was already programmed in that window, so it is a maximum-idle heartbeat, not a rate limit; the ASAP trigger sends the next certificate as soon as the previous reaches a final state (`DelayBetweenCertificates: 1s`). Verified against a live 2-L2 enclave: the aggsender logs a certificate attempt roughly every 2s. The section now states the real mechanism, cites aggkit's `config/default.go` as the source of the 5m default (not the previously cited file), and re-frames the E2E budget around measured latencies plus the one observed 5-minute timeout. 3. Six in-repo comments referenced `enclave-notes.md`, a planning file that never ships with this repository -- dangling references in a branch destined for an external PR. Repointed to the in-repo guide's own sections (or dropped, where the detail was not load-bearing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…ersion gate - Bump aggkit_image in both 2-L2 params files from the locally-built autoclaim-fix tag to ghcr.io/agglayer/aggkit:0.11.0-rc4 -- the upstream fix (agglayer/aggkit#1761) is in rc4, so the local image/build recipe is no longer needed. - aggkit-proxy now runs --components=proxy,tracker (was proxy-only) and its config template gains [Tracker] + [Tracker.AgglayerClient.GRPC], wired to the enclave's agglayer gRPC endpoint. REST.MaxRequestsPerIPAndSecond raised 10->50 since all browser traffic funnels through haproxy's single source IP. - Fix the config.toml/cdk-config.toml version gate that hard-fails aggkit v0.11 startup: `{{- if lt .aggkit_version "0.8" }}` does a lexicographic STRING comparison in Go's text/template, and "0.11" < "0.8" digit-by-digit, wrongly rendering the deprecated (non-WarnOnly) polygonBridgeAddr key. Moving to a plain Starlark `aggkit_version < 0.8` float comparison reproduces the identical bug one level down, since _extract_aggkit_version collapses "<major>.<minor>" into a single float and 0.11 is numerically less than 0.8. Added _render_legacy_bridge_addr, which parses major/minor as integers and compares them as a tuple, and gated both templates on it. Added a regression test covering both failure modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
_get_agglayer_endpoint compared _extract_aggkit_version's collapsed float via `version >= 0.3`, which suffers the same bug fixed for _render_legacy_bridge_addr in c124850: "0.11.0-rc4" collapses to the float 0.11, and 0.11 < 0.3 even though minor version 11 is ordinally newer than minor version 3. This silently pointed aggkit rc4 nodes at the readrpc agglayer endpoint instead of grpc, breaking certificate settlement. Was dormant against the previous local/CI image tag (non-numeric -> 999.9 fallback -> grpc) and only activated by the rc4 image bump. Factor both _render_legacy_bridge_addr and _get_agglayer_endpoint onto a shared _parse_aggkit_major_minor(aggkit_image) helper that parses major/minor as integers and compares them as a tuple, ordering correctly regardless of minor-version digit count. Add the 0.11.0-rc4 -> grpc regression case to test_get_agglayer_endpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
L1GlobalExitRootAddress has no on-chain discovery fallback (unlike BridgeAddrs) -- left unset it defaults to the zero address and permanently stalls StepWaitingGERUpdate for every L1->L2 bridge, since the tracker's GERSource filters L1 logs by that address. Wire it through contract_setup_addresses (l1_ger_address, same mechanism already used for rollup_manager_address) into aggkit_proxy.star's template data and set it under [Tracker] in the proxy config template. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…e tracker - Replace the locally-built aggkit:fix-autoclaim-l2tolx-local image section entirely -- PR #1761 is upstream in rc4, so the params files' registry image ghcr.io/agglayer/aggkit:0.11.0-rc4 needs no local docker build. Rewrites the "Image Version" section and prerequisites accordingly; zero repo-wide references to the dead local tag remain. - Document the aggkit-proxy's new tracker component (--components=proxy,tracker): what it serves (/tracker/v1), its config template's [Tracker] block, and why L1GlobalExitRootAddress must be set explicitly (no on-chain discovery fallback -- left unset it zero-stalls every L1->L2 StepWaitingGERUpdate). - Add curl smoke-test examples for /aggkitapi/tracker/v1/health and for registering/querying a bridge tx's tracking status. - Note the aggkit_image field in both params-file yaml snippets and cross-reference the version-gate fix (_parse_aggkit_major_minor) now that rc4 exercises it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Bumps aggkit_image from 0.11.0-rc4 to 0.11.0-rc5 in both params-aggkit-l2l2 files. rc5 (agglayer/aggkit#1784, closes #1781/#1782/#1783) adds fail-fast startup validation on [Tracker].L1GlobalExitRootAddress and changes MaxRequestsPerIPAndSecond's default from 10 to 0 in RESTConfig-backed sections (unenforced there; upstream now documents it as infra-layer-only). Per plans/bridge-tracker-rc5/discovery.md: L1GlobalExitRootAddress templating needs no code change (this package already threads a real, non-zero address via contract_setup_addresses["l1_ger_address"]) -- only extended the existing config.toml comment to note the new fail-fast behavior. Dropped MaxRequestsPerIPAndSecond to the new upstream default of 0 and rewrote the comment to stop implying in-process enforcement it never had. Updated the 2-L2 bridge UI guide's rc4 references and invalidated prose (zero-address stall -> fail-fast, rate-limit "raised from 10" framing) accordingly. Left rc4 string literals in src/chain/shared/aggkit.star/aggkit_test.star untouched -- they're regression-test fixtures documenting a past version-gate bug using that exact tag as the historical reproduction case, unrelated to the deployed image version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ling refs - Add "After an enclave reset: recovering your wallet and UI" under Troubleshooting: symptom table, MetaMask/injected-wallet nonce-cache clearing, and the UI re-sync checklist (re-run kurtosisDevnetEnv.mjs, restart pnpm dev, hard-reload). - Add "Tracker troubleshooting" under Troubleshooting: the #1786 WaitingClaim-vs-claimable gap (expected, do not "fix" the UI copy while it's open), the three causes of a row with no progress bar, per-step stall diagnosis (which component each step depends on), and the unenforced rate-limit note (#1783). - Replace the local-only plans/bridge-tracker-rc5/discovery.md citations (guide + config.toml) with the actual upstream issue/PR links (#1781-#1783, PR #1784) they were standing in for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Two review findings against the live enclave: - `kurtosis port print cdk agglayer-dev-ui-proxy-002 http` prints a full URL (http://127.0.0.1:<port>), not a bare port, so the documented `http://127.0.0.1:${HAPROXY_PORT}/...` composed an invalid URL (curl exit 3). Renamed the variable to HAPROXY_URL and use it directly; the troubleshooting snippet now derives it inline too. - `tracking_status` has no `tracking` value: the wire enum is `registered`/`running`/`finished`/`error` (aggkit rc5 API.md, verified against live responses and the SDK's AggkitTrackingStatus union). Also replace the params files' references to a local-only plans/bridge-tracker-rc5/discovery.md path with the durable upstream reference (agglayer/aggkit PR #1784), matching the same cleanup already applied to the guide and the proxy config template. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Resolves a semantic merge conflict in the aggkit version-gate helpers: both branches independently fixed the "0.11" lexicographic/float comparison bug (main's e18da57, ours c124850/8e1d41af). Adopts main's _aggkit_version_gte(image, major, minor) implementation and its (0, 0) "assume oldest" fallback for non-numeric tags, dropping our duplicate _parse_aggkit_major_minor/_render_legacy_bridge_addr helpers and rewiring our call sites (_get_agglayer_endpoint, _build_config_data) onto main's function. main's fallback differs from ours (we assumed "latest" for non-numeric CI tags); this is a no-op for this package today since every params file pins a numeric aggkit tag (ghcr.io/agglayer/aggkit:0.11.0-rc5). Ports the rc4-tag regression cases from our aggkit_test.star onto main's API and documents the fallback difference in a new test case. Also reconciles the two config.toml/cdk-config.toml conflicts (both sides touched the same polygonBridgeAddr gate line): keeps main's `aggkit_legacy_bridge_addr` template key, preserving our AutoClaim block, [PublicREST] rename, and bridge-tracker templating alongside main's unrelated cdk-erigon FinalizedBlock fix and dual-wallet agglayer signer support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
The "421ba23" example short-sha in _extract_aggkit_version's CI-tag comment tokenizes to "ba" at a digit/letter boundary, which the typos checker flags as a misspelling of "by"/"be". Pre-existing on this branch (introduced in 8e1d41a, before this branch had a PR open to run CI); tripped by src/chain/shared/aggkit.star:753 on PR #929's first CI run. Reword the example hex to sidestep it -- no functional change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…flow Adds a third sequencer_type (anvil) so agglayer sovereign contracts are predeployed through L2 genesis allocs, a new snapshot/ flavor (anvil-aggkit) that captures the anvil L1 + two anvil L2s + agglayer + aggkit x2 + aggkit-proxy + haproxy + dev-ui stack into self-contained docker images (state/config baked in, no bind mounts), and a snapshot-devui.yml workflow that builds, gates (settlement_free + per-chain historical_states), verifies, and publishes that bundle to GHCR for agglayer-dev-ui's hermetic e2e CI to consume. The default geth/lighthouse snapshot flavor and its existing tests are byte-identical and untouched (proved across S7-S10, see plans/dev-ui-ci-snapshot/*-evidence/). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
The first workflow_dispatch run (31615246384) failed at the "Pre kurtosis run" step: kurtosis-pre-run's GCR pre-pull authenticates via google-github-actions/auth (Workload Identity Federation), which needs an OIDC token that only exists when id-token: write is granted. Match test.yml/nightly.yml's existing permissions for this same composite action (contents: read, actions: write, id-token: write), plus packages: write for the GHCR push this workflow adds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
CI run 31615686075 (the first real workflow_dispatch of snapshot-devui.yml) failed TEST 11: the structural fallback check in verify-anvil-aggkit.sh assumed "chains" is an array and "aggkitBridgeApis" is a top-level key. The real schema emitted by static_files/additional_services/bridge-ui/aggkit-dev-ui-config.json.tmpl has "chains" as an object keyed by chain name, and "aggkitBridgeApis" nested under appModes.configs.<mode>. This mismatch was masked on every prior local run because this dev machine has an agglayer-dev-ui checkout, which takes the validateConfig.mjs branch instead of the structural fallback -- a GH runner has no such checkout, so it's the first exerciser of this code path. Verified the fix against the actual baked config.json extracted from an S8 snapshot image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
…ents The published GHCR naming was changed to kurtosis-cdk-snapshot-<service> (both package and tag must carry the literal word "snapshot") after generate-compose.sh's SNAPSHOT_IMAGE_PREFIX override comment was originally written. Comment-only fix -- image_ref() itself already used env-var indirection correctly, so no functional bug, but the stale comment (still baked into every generated docker-compose.yml) would have pointed S13/S14 at the wrong registry prefix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
…efault The first real publish (run 31616965584) proved all 11 kurtosis-cdk-snapshot-<service> packages anonymously pullable immediately, with no manual visibility toggle -- update the header note to state this as observed fact (with the run URL and evidence path) rather than a hypothetical one-time step, while keeping the toggle procedure documented as a fallback in case org policy changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
…matrix job
Starlark unit tests (src/package_io/input_parser_test.star) for the anvil
input-parser branches added in S3/S4/S4b/S5: consensus-type coupling (V1),
deployment-stage coupling (V2), optimism-package rejection (V4),
block-time/slots validation (V5), mnemonic validation (V6), and the
op_el_rpc_url/op_cl_rpc_url aliasing (incl. a regression check that op-reth's
own aliasing is unaffected). All call input_parser.parse_args() end to end
against realistic user_args, not a hand-rolled partial dict.
A hermetic template-rendering regression check (scripts/anvil-template-check/)
renders aggkit/config.toml, agglayer/config.toml and
zkevm-bridge-service/config.toml with real Go text/template against JSON
fixtures captured from a live sequencer_type: anvil enclave, then asserts
every address/URL key the anvil branches own is present and non-empty -- the
regression net for the S4 "silent-fallback" class ({{- else }} arms that
render empty strings, or in two cases drop the key entirely). Wired into the
`unit` CI job (needs Go).
A shell unit test for snapshot/scripts/lib/ports.sh (the single source of
truth for snapshot host-port numbering), also wired into `unit`.
Both new tests are proven to fail by temporary revert (restored afterward;
trees verified byte-identical to HEAD before committing) -- see
plans/dev-ui-ci-snapshot/s15-evidence/.
New CI matrix job run-with-anvil-l2 (test.yml) exercises the single-chain
anvil-L2 bring-up (.github/tests/other/anvil/anvil-l2-rollup.yml) in
kurtosis-cdk's own CI: sovereign bridge/GER bytecode presence + aggkit bridge
sync-status, both locally dry-run and validated against a live enclave.
monitor.sh gains an `anvil` sequencer_type case. Kept as its own job rather
than a row in the generic list-ymls matrix: that matrix's bridge-tests action
needs a live zkevm-bridge-service, which the anvil params intentionally skip
(aggkit's own bridge component covers it instead).
Also fixes two pre-existing bugs discovered while getting `lint` green (it
was failing on HEAD before this commit, which skips every job gated on
`needs: [lint, unit]`, including the new one):
- black-formatting violation in src/chain/anvil/anvil_l2.star (S3).
- shellcheck findings (SC1091/SC2016/SC2317/SC2018/SC2019/SC2001/SC2005)
across seven snapshot/ scripts that had never actually been linted, because
the black failure short-circuited the lint job before shellcheck ran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
run-with-anvil-l2's sync-status check raced the L1 syncer: CI observed l1_info.is_synced=false (synchronized_deposit_count 0 vs contract_deposit_count 1) immediately after monitor.sh returned, because monitor.sh only waits for L2 block height, not for aggkit's bridge sync to catch up. Poll up to 120s (24 x 5s), matching monitor.sh's own retry style, instead of failing on the first observation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
New docs/docs/advanced/anvil-devui-snapshot.md covers the anvil-flavor snapshot's topology, the two-run params pair, why anvil_image is pinned to v1.5.1, the snapshot.sh --flavor anvil-aggkit pipeline, the full summary.json/port-table bundle contract, the publish workflow (naming, GHCR visibility), and the restore hazards (tip-state-only --load-state, the settlement_free/historical_states gates, the timestamp seam). Cross-linked from aggkit-2l2-with-bridge-ui.md and snapshot.md; snapshot/README.md now points at both flavors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
Caught by actually running the documented pipeline end-to-end: unlike the default flavor, snapshot.sh --flavor anvil-aggkit exits right after generating summary.json and does NOT auto-run verify.sh (the script's own "not implemented yet" message is stale). Document verify.sh as a separate step, matching what snapshot-devui.yml's CI workflow already does. Re-verified with a fresh 2-L2 anvil enclave: snapshot.sh ran in 68s, gate-snapshot-soundness.sh passed, and verify.sh passed 51/51 in 203s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
Independent review pass over the anvil-L2 + snapshot work. Fixes only CONFIRMED findings; every one is backed by a reproduction. Correctness - input_parser: set_l1_client_args no longer clobbers an anvil L1's URLs back to the ethereum-package el/cl service names. This is the pre-existing bug (introduced by 7fc310e) that every anvil params file carried a three-line workaround for; the workarounds are now dropped from run1 and the CI fixture. run2 keeps its explicit URLs for a different, real reason: suffix -002 must point at run1's anvil-001. - input_parser: reject l1_engine anvil + sequencer_type op-reth with a message naming the cause, instead of dying with a bare "el_cl_genesis_data artifact doesn't exist" from optimism-package. - .github/tests/other/anvil/anvil-rollup.yml: pin sequencer_type cdk-erigon. It defaulted to op-reth and had been broken (never run by CI: list-ymls only globs cdk-erigon/ and op-reth/). - input_parser V7: reject a custom l2_anvil_mnemonic -- contracts.sh's initialize_rollup hard-codes the default dev mnemonic when funding sovereignadmin/aggoracle/claimsponsor on the L2. - 4 new parse_args regression tests covering the above. Checks that could not fail - verify-anvil-aggkit: jq 1.6 exits 0 on EMPTY input, so several `jq -e` probes passed against a dead endpoint. Guard each with a non-empty check. Assert the "L2->L1 does NOT autoclaim" step positively; `!= done` was true for "" as well. - verify-anvil-aggkit: TEST 1/TEST 4 passed when `docker compose config/ps` errored (empty stream, zero iterations). Cross-check row counts; use `ps --all` so a crashed container is visible. - _poll_tracker and derive_anvil_restore_cmd logged to stdout inside command substitution, corrupting captured JSON / swallowing their own "fail loudly" errors. - gate-snapshot-soundness: `.chains: 5` and `.chains: "abc"` passed vacuously (jq length on a scalar). Require an array and assert the iteration count. - build-images: same vacuous-loop class; assert chain count before and after baking. Type-guard the historical_states length check (also in extract-state). - publish-images: refuse to report success having pushed nothing. CI hygiene - test.yml: the sync-status retry loop was defeated by `bash -e` -- an unguarded `var=$(curl -sf ...)` aborts the step on the first transient failure, which is exactly when the endpoint is expected to be down. - test.yml: move the two new snapshot checks out of `unit`. Every integration job is `needs: [lint, unit]`, and GitHub reports a job skipped by a failed `needs` as skipped, not failed. - snapshot-devui: route the workflow_dispatch `tag` input through `env:` and validate it (it was interpolated straight into a `run:` block in a job holding packages: write + a GCP WIF identity); drop the unneeded `actions: write`; make the publish/dry-run choice two explicit invocations rather than an unquoted flag variable; make concurrency ref-independent so two refs cannot race the moving latest tag; add static_files/** and src/package_io/** to the path filter. - seed-devui-fixtures: validate the address/key/port values that land in hand-built JSON-RPC bodies and in the container's `sh -c` string. - Unknown --flavor now fails in build-images/generate-compose/ generate-summary/verify-healthchecks instead of silently running the default flavor; ports.sh rejects out-of-range computed ports. - shellcheck directives accept SC1090 as well as SC1091 (which code is emitted depends on the runner's floating shellcheck version). Docs - anvil-devui-snapshot.md: the L1-URL workaround no longer exists. - snapshot.sh: replace the stale "verification not implemented yet" message with the actual gate/verify commands. - aggkit.star: mark create_bridge_config_artifact as dead code and record that its sequencer_type branch was never updated for anvil. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
Final layer: anvil-flavor devnet snapshot for hermetic dev-ui CIThis branch now carries a third Measured: 2-L2 anvil bring-up 224-254 s (op-reth equivalent: 459 s) · snapshot capture 67-80 s · restored bundle boot-to-ready ~24 s local / ~65 s in CI · Published: The non-obvious constraint. Default paths untouched, and proven so rather than asserted: the geth/lighthouse snapshot flavor's generated compose/USAGE/helper scripts are byte-identical old-vs-new against a mocked enclave; all 18 Review notes
Full write-up and per-step evidence: |
…er (K1)
Collapses aggkit-00X-bridge into aggkit-00X: the bridge is now a component
of the main aggkit process (--components=...,bridge), served from the same
rendered config (config.toml renders byte-identically regardless of which
components run), exposing rpc (5576), pprof (6060) and the bridge REST API
(5577) on one service. This drops the 2-L2 anvil-aggkit topology from 11 to
9 relevant services.
- src/chain/shared/aggkit.star: delete _deploy_bridge_service; the main
service now adopts _get_aggkit_bridge_ports and returns the service handle
so run() can still read .ports["rest"].url.
- Five params files (+the anvil-l2-rollup CI fixture) add "bridge" to
aggkit_components and repoint bridge_url/aggkit_proxy_bridge_urls/
aggkit_autoclaim_bridge_urls at aggkit-00X:5577 instead of the removed
-bridge sibling.
- .github/workflows/test.yml: run-with-anvil-l2's sync-status probe now
targets aggkit-001 directly.
- snapshot/scripts/{build-images,generate-compose,generate-summary,
discover-containers,extract-state,verify-healthchecks}.sh and
snapshot.sh/lib/ports.sh: collapse the per-service "aggkit + aggkit_bridge"
role loops to a single aggkit role; move the bridge sync-status healthcheck
onto the merged image; delete the now-dangling aggkit_bridge_rpc port
offset (kept as a rejected key with a regression test, not left dangling).
- docs: update the anvil-devui-snapshot and aggkit-2l2-with-bridge-ui service
tables/topology descriptions to match (9 services, one bridge_url per
aggkit instance).
Verified on a live 2-L2 anvil enclave (kurtosis run x2): 9 services with no
-bridge suffix, bridge REST answers on aggkit-001/aggkit-002 for network_id
0/1/2 both directly and via aggkit-proxy, and snapshot.sh + verify.sh's full
anvil-aggkit contract (51/51 checks) passes against a freshly captured
bundle, including a real L1->L2 autoclaim and L2->L1 manual-claim round trip.
shellcheck/yamllint/kurtosis-lint (black) all clean on touched files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…-config code, fix bridge URL for merged aggkit (K2) - Delete create_bridge_config_artifact and its DEAD CODE comment block in src/chain/shared/aggkit.star: zero callers repo-wide, and the legacy zkevm-bridge-service config is actually rendered by src/chain/shared/zkevm_bridge_service.star. - Wire .github/tests/other/anvil/anvil-rollup.yml into test.yml's list-ymls matrix as an explicit third path (not a folder glob, to avoid double-running anvil-l2-rollup.yml, which keeps its dedicated run-with-anvil-l2 job). Add an explicit anvil arm to the job_name if/elif for clarity. - Teach _get_bridge_service_url (src/additional_services/test_runner.star) to resolve the merged aggkit service's "rest" port when deploy_cdk_bridge_infra is false, instead of returning "". Fails loudly if "bridge" isn't in aggkit_components, since the rest port is always open on the merged service but only serves bridge routes when the component is enabled. - Investigated switching run-with-anvil-l2 to the generic bridge-tests action now that BRIDGE_SERVICE_URL resolves for it too: proved empirically (K2-evidence/) that polycli's default --legacy bridge client 404s against the aggkit bridge's /bridge/v1/... routes, so kept the job's bespoke checks and documented the specific incompatibility in its comment block. Verified anvil-rollup.yml end-to-end (kurtosis run + bridge-tests bats filters, both pass) and confirmed the matrix-generation shell logic produces an anvil-rollup entry without anvil-l2-rollup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…atency knobs (K3) [outbound.rpc.settle] has had no effect on settlement since agglayer PR #1393 -- confirmations=1 there never applied, so this devnet was silently stuck on the live default of 12. Delete the dead block and port its intent onto [settlement.pessimistic-proof-tx-config]: confirmations (default 1 on this devnet) and settlement-policy (default "safe", matching upstream). max-retries/retry-interval have no 1:1 mapping onto the new schema's two retry-policy tables and settlement-timeout has no equivalent at all upstream -- both are left at upstream defaults with a comment explaining why, rather than inventing config keys that don't exist. Bump agglayer_image 0.6.0-rc.5 -> 0.6.0-rc.8 (note: no "v" prefix on the published GHCR tag, verified against the registry's tag list -- only the git tag has one). rc.7+ warns on startup if stale [outbound.*] keys are present, which is a free regression net for the migration. Expose agglayer_settle_confirmations and agglayer_settlement_policy as input args (validated against latest/safe/finalized), following the trigger_cert_mode validated-enum precedent. Add a comment at [epoch.block-clock] noting it's bookkeeping-only from v0.6.0-rc.2 onward (PR #1615) rather than adding a no-op epoch_duration knob. Measured settlement wall-clock for one L2->L1 bridge before/after on the same anvil enclave shape: ~60.0s in both cases. The confirmations fix (12->1) is a real correctness fix but shows no measurable latency delta here -- the dominant floor is the retry-on-not-included-on-l1 policy's default 60s initial interval, which gates the settlement task's first receipt check regardless of how many confirmations are required on a 1s-block anvil L1. See plans/snapshot-v2-aggkit-e2e/K3-evidence/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
Each publish run now computes one shared unix-ts and, per service, a
sanitised <component-version> resolved from IMAGE_INFO.json's base_image
(new snapshot/scripts/lib/version-tag.sh, with a unit test), yielding
tags like kurtosis-cdk-snapshot-aggkit-001:0.11.0-rc5-1755100800. The
moving snapshot-latest-devui alias is kept for humans; snapshot-<sha> is
retired as the pinning mechanism.
publish-images.sh always tags+pushes this per-service tag alongside any
uniform --tag values, hard-failing (no "unknown" fallback) if a
service's version can't be resolved, and on a real push captures each
image's manifest digest into a new PUBLISHED_TAGS.json. A new script,
apply-digests.sh, is run as a separate post-publish workflow step (K4
context: summary.json/docker-compose.yml are both written before
publish runs) to patch {tag,digest} into summary.json and rewrite the
compose file's image: lines to `<ref>@sha256:<digest> # <tag>`,
validating with `docker compose config` before writing anything.
Also fixes snapshot-devui.yml's header runbook, which still described
the pre-K1 11-service/aggkit-*-bridge topology and the retired tag
scheme, and updates docs/advanced/anvil-devui-snapshot.md's now-stale
tag-scheme description.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…image override seam (K5)
Makes the anvil-aggkit snapshot bundle consumable by any repo:
- Profile-gate agglayer-dev-ui-002 (profiles: [devui]); drop haproxy's
depends_on edge to it and override its compose-level healthcheck to
probe only /aggkitapi (its baked healthcheck.sh also probes bare /,
which has no live backend once dev-ui is off by default).
- Discovered live that this alone is insufficient: the captured
haproxy.cfg's backend_default server line has no resolvers/init-addr,
so haproxy hard-crashes at boot ("could not resolve address
agglayer-dev-ui-002") when dev-ui was never started. Patch the
captured haproxy.cfg (extract-state.sh) to drop default_backend
entirely -- bare / now 503s instead of crash-looping; nothing
CI-relevant ever hit it.
- Emit a second compose file, docker-compose.mounts.yml
(generate-compose.sh), bind-mounting the already-captured config/
tree instead of relying on baked-in COPY. agglayer/aggkit-00X/
aggkit-proxy-001 run bare upstream images (${AGGLAYER_IMAGE:-...} /
${AGGKIT_IMAGE:-...}) with no healthcheck (matches aggkit's own e2e
envs); haproxy/dev-ui keep their derived image and just bind-mount
their config file; anvil stays fully baked (not override-able).
- Teach apply-digests.sh to patch the mounts file's tag-based image
refs (anvil family, haproxy, dev-ui) too; the override-able services
there use bare upstream refs by design, so a miss for those is
expected, not an error.
- Document the config/ tree's kurtosis-cdk-name -> aggkit-env-name
mapping in extract-state.sh (A2 builds its env from this).
- verify.sh gains --compose-file so the same bundle can be verified
against both variants; verify-anvil-aggkit.sh parametrizes every
docker compose -f call, skips the zero-mount-contract audit and the
baked-config-verbatim check for the mounts variant (both test an
invariant that variant intentionally doesn't hold), and skips the
dev-ui /config.json check when the devui profile isn't active.
verify-healthchecks.sh's discovery cross-check now lists services
with --profile devui so it still sees the profile-gated container.
Verified live against a fresh anvil-aggkit enclave (kurtosis run x2 on
this HEAD): default `up -d --wait` reaches healthy with no dev-ui
container; --profile devui additionally starts it and it serves;
docker-compose.mounts.yml reaches the same readiness, proven to
genuinely read the config/ tree by editing aggkit's log level on disk,
restarting the (bare, shell-less) container, and observing the live
process's log output switch from info to DEBUG; AGGKIT_IMAGE pointed
at a different aggkit tag runs that image and still reaches readiness.
verify.sh passes 43-45/45 against both variants -- the only failures
are an unrelated, pre-existing bridge-round-trip certificate-settlement
stall reproduced identically on both variants (same root cause, not
caused by this change), tracked separately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… K5's profile contract (K5b) verify-healthchecks.sh's "every discovered component is present" check used to fetch SERVICE_NAMES with --profile devui unconditionally, which made it structurally unable to fail on dev-ui specifically: it could only see the profile-inclusive view, so it never distinguished "present only under --profile devui, as K5 intends" from "present in the default set, which K5's own change says should no longer be true." Split the check in two: the one profile-gated component named in discovery.json's dev_ui.service_name must be present under --profile devui AND absent from the plain (no-profile) service list; every other discovered component is checked only against the plain default-set list, same as before K5's change. Proven failure-provable in both directions on scratch copies of a freshly generated compose file (never against the tracked tree): passes unmodified; fails when a non-profiled service is genuinely removed; fails when dev-ui is dropped out of the devui profile; fails when dev-ui's profile gating is removed entirely (regressing it to default-always-on). git diff stayed empty throughout each mutation. Also attributes (does not fix) the L2->L1 round-trip failure verify.sh reported on both compose variants at K5's HEAD: a brand-new capture from a brand-new enclave (no restart churn) shows the default variant at a clean 49/49, ruling out K3/K4/K2 as the cause there and confirming K5's own "repeated up/down churn against one long-lived enclave" attribution for that variant. The mounts variant, however, deterministically reproduces its own distinct failure across three independent fresh runs: aggsender permanently deadlocks at status starting_claim_syncer_stage due to a claim-syncer initialization race (SetClaimSyncerNextRequiredBlock requests block 0 since no certificate has ever settled, but the claim syncer's own autostart has already advanced its local DB past block 100, and ClaimSync.SetNextRequiredBlock permanently rejects 0 < firstBlock). This traces to K5's own mounts-variant depends_on/healthcheck design (aggkit-00X waits only on agglayer: condition: service_started there, vs service_healthy in the default variant), not to K3's agglayer rc.8/settlement-config migration -- reported as a structured blocker per plan (see plans/snapshot-v2-aggkit-e2e/K5b-evidence/00-SUMMARY.md) rather than reworking K5's landed mounts-variant design unilaterally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… mounts compose variant (K5c) docker-compose.mounts.yml gated aggkit-00X on agglayer via condition: service_started (K5's own design, since bare agglayer had no healthcheck). This let aggkit-00X's aggsender race agglayer's async gRPC-listener bind against its own local claim-syncer autostart, and lose that race deterministically: aggsender's SetClaimSyncerNextRequiredBlock stalls waiting for agglayer to become reachable while the claim syncer's purely-local autostart advances past block 100 in the meantime, after which ClaimSync.SetNextRequiredBlock permanently rejects the block-0 fallback and aggsender deadlocks at starting_claim_syncer_stage forever -- reproduced 3/3 by K5b, root-caused there, fixed here. agglayer's bare upstream image is not distroless (unlike aggkit's), so it gets a real compose-level healthcheck: a bash `/dev/tcp` TCP-connect probe against its own gRPC port (no curl/wget/nc/busybox dependency). aggkit-00X and aggkit-proxy-001 now depend on it via condition: service_healthy instead of service_started. aggkit-00X/aggkit-proxy-001 themselves stay healthcheck-less (genuinely distroless images), matching aggkit's own op-pp-2chains e2e env precedent, whose agglayer service also carries a compose healthcheck for the same reason -- though its trivial "process exists" probe would not have closed this race, so a real TCP-connect probe is used instead. Verified: mounts variant verify.sh passes 46/46 twice consecutively from a fresh capture (bug reproduced 3/3 pre-fix); default variant unaffected at 49/49; AGGKIT_IMAGE override (0.11.0-rc4 vs captured rc5) still reaches readiness; K5b's three-direction healthcheck-audit mutation proofs still hold. Evidence: plans/snapshot-v2-aggkit-e2e/K5c-evidence/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…g guides (K6) Rename anvil-devui-snapshot.md -> anvil-devnet-snapshot.md (stub left at the old path for agglayer-dev-ui's inbound link) and rewrite it for three consumers instead of one: dev-ui (zero-mount), aggkit (mounts variant + AGGKIT_IMAGE, proven with a re-run of K5c's rc4-vs-rc5 override), and agglayer (same seam via AGGLAYER_IMAGE, marked untried). Document the previously-undocumented docker-compose.mounts.yml variant and the dev-ui compose profile, including why bare `/` now 503s on the default set. Add an Overrides section covering aggkit_image/aggkit_components/ trigger_cert_mode (Auto resolves to EpochBased for a PessimisticProof aggsender, not ASAP -- confirmed live via aggkit's own log line), agglayer_image/agglayer_settle_confirmations/agglayer_settlement_policy (PascalCase wire values, confirmations showing ~0 latency delta per K3, retry-on-not-included-on-l1's 60s initial-interval as the real floor, why [epoch.block-clock] isn't the knob), and l1/l2_anvil_block_time x slots_in_epoch (confirmed the latest/safe/finalized lag formula by direct RPC measurement, and found a real asymmetry: the L2 floor is Starlark- validated, the L1 floor is not and instead crashes anvil itself with a clear CLI error). Every snippet was executed at least once against a live anvil-aggkit enclave; evidence under plans/snapshot-v2-aggkit-e2e/K6-evidence/. Add a Reorg section from T2's findings: anvil_reorg's exact shape, the byte-identical-block trap, and the real ceiling (bridge-contract deployment block, not the load point) -- re-ran the recipe successfully. Reconcile the Restore-constraints section's tip-state-only framing against T2: the published bundle does not hit that hazard (gate-snapshot-soundness.sh already enforces preserve_historical_states), and confirmed summary.json itself never carries the historical_states field either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… (K7) Extends the test surface so a revert of K1-K5 fails something: - src/package_io/input_parser_test.star: end-to-end parse_args() tests for K3's agglayer_settle_confirmations (default/override/must-be->=1) and agglayer_settlement_policy (default/override/invalid-value rejection). - scripts/anvil-template-check/: asserts [settlement.pessimistic-proof-tx-config] renders a present, non-empty confirmations value, that no [outbound.*] section survives, and that settlement-policy renders the upstream PascalCase wire name (not kebab-case); fixes a stale pre-merge "-bridge" sidecar hostname left in the aggkit AutoClaim fixture from before K1's merge. - snapshot/scripts/extract-state_test.sh (+ testdata/extract-state-config-tree/): hermetic test (fake docker/curl, no live enclave) that runs the real extract-state.sh --flavor anvil-aggkit end to end and asserts the emitted config/ tree contains all 11 files on aggkit-env-design.md (d)'s list, at the paths K5 actually emits, and that TriggerCertMode = "ASAP" is present in both emitted aggkit configs. - .github/workflows/test.yml: wires extract-state_test.sh into the snapshot-unit job alongside ports_test.sh/check.sh. K4's version-tag_test.sh already covers the version-resolution/tag-sanitisation surface -- not duplicated here; its failure-provability was re-verified (SNAPSHOT_MAX_VERSION_LEN neutered/restored) as part of this step's evidence without a code change. Every new assertion's failure-provability was proven both ways (neuter the underlying production code/fixture, show the exact test fails, restore, show git diff empty, rerun green); logs archived under plans/snapshot-v2-aggkit-e2e/K7-evidence/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… on the compose-restored stack, add test.yml workflow_dispatch snapshot-devui has failed on this branch since K1, always on the same two L2->L1 round-trip checks with CertificatePending.status_string stuck at "?" for the full 5-minute budget, while the identical suite passes locally. This workflow captures no log artifact for the docker-compose-restored stack, so add a temporary always() step dumping aggkit/agglayer/anvil container logs right after the Verify step (containers are still up at that point; nothing tears the compose stack down). Separately, `test`'s pull_request-triggered runs stopped being created entirely for PR #929 starting at commit 32a7ca6 (K3) -- total_count: 0 via the Actions API for every commit since, despite no paths/branches filter. Add workflow_dispatch so `test` can be triggered manually while the root cause of the missing synchronize-triggered runs is diagnosed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
|
K7b diagnostic: closing/reopening to test whether pull_request:reopened re-creates the missing |
…gests.sh's trap-only cleanup()
K7b: this SC2317 finding ("Command appears to be unreachable") is the
well-known shellcheck false positive for functions invoked only via `trap
... EXIT` (cleanup() here is never called directly, only via `trap cleanup
EXIT`), matching the exact class already annotated elsewhere in this repo
(ports.sh, verify-anvil-aggkit.sh, version-tag.sh). It went undetected
because `test`'s pull_request-triggered runs have not been created since
K3 (see the workflow-level K7b commit) -- CI's lint job was failing on
this the moment a real run finally executed (workflow_dispatch run
31720115255), which in turn skipped every job with `needs: [lint, unit]`
(list-ymls, run-with-anvil-l2, and the whole matrix).
Verified with shellcheck 0.9.0 (matching CI's apt-installed version, not
this sandbox's stale 0.7.1) across every tracked *.sh file: clean. Also
reran yamllint and `kurtosis lint .` locally: clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…yer logs, filter the known-benign claim-syncer retry noise
v1's plain `tail -300` came back 91% dominated by one INFO-level line
("Setting next required block for claim syncer...") that retries forever
and is confirmed present in K5b's healthy LOCAL runs too (see
plans/snapshot-v2-aggkit-e2e/K5b-evidence/3a-aggkit-001-mounts-3rdrun-full.log)
-- so it is not the cause of the CI-only failure, just noise that drowned
out everything else in a 300-line window. Drop the tail entirely and
filter that one line out so whatever the aggsender/certificate/epoch
machinery is actually doing (or not doing) around the Leg-2 send is
visible on the next run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… wedges aggsender's claim-syncer (K7b) Root cause of the CI-only snapshot-devui failure (every push since K1, 5689c40 onward): the default/baked compose variant's agglayer healthcheck probed its own prometheus metrics endpoint (:9092/metrics) via wget, not its gRPC listener (:4443). agglayer's metrics HTTP server can start answering before its async gRPC bind completes -- nothing serializes the two -- so `condition: service_healthy` could fire before gRPC is actually accepting connections. aggkit-00X's aggsender races that gRPC bind against its own claim-syncer autostart on startup; losing the race doesn't error transiently, it wedges claim-syncer initialization permanently ("cannot set next required block to 0, it must be >= the first block in DB (100)", retried once a second forever), so the aggsender's certificate loop for that network never starts and the L2->L1 leg never leaves WaitingClaim. K5c (b7a360b) already diagnosed and fixed this *exact* race for the mounts compose variant with a genuine TCP-connect probe against the gRPC port itself (bash's builtin /dev/tcp) -- the default/baked variant never got the equivalent fix, so the same latent race stayed open there, gated by a weaker/different signal that a fast, lightly-loaded host usually (but not reliably) wins and a loaded CI runner reliably loses. This is why the suite passes locally and fails on CI: it is a genuine timing race, not a CI-vs-local environment difference in the code path itself. Confirmed via full untruncated aggkit-001/002 log dumps from two real CI runs (K7b-evidence/03-*, 04-*): the aggsender module never logs anything past claim-syncer startup for the entire run except the one permanently- failing retry line, while the unrelated l1infotreesync syncer (a different module, unaffected by this race) continues normally in parallel -- ruling out a broader hang and pointing specifically at aggsender's claim-syncer-vs-agglayer-gRPC startup ordering. Reuses K5c's exact probe for the default variant's agglayer service. Verified: shellcheck 0.9.0 clean, yamllint clean, kurtosis lint clean, and the heredoc renders valid YAML (checked with an isolated harness before committing). No test asserts the old healthcheck test string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
Root cause found and fixed in 902a54e (agglayer gRPC-bind race in the default compose variant's healthcheck) and confirmed green on a real CI run (https://github.com/0xPolygon/kurtosis-cdk/actions/runs/31724584073, 49/49, both L2->L1 round-trip checks passing). The diagnostic step served its purpose; no longer needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…sendTx removal (K7c) run-with-anvil-rollup fails in CI at "Monitor rollup progress" -- zkevm_verifiedBatchNumber never advances past 0. Root cause: upstream agglayer PR #1637 (agglayer/agglayer#1637, closing #1632, first shipped in v0.6.0-rc.6) permanently and unconditionally disables the interop_sendTx JSON-RPC method -- no config flag re-enables it. cdk-node's Aggregator (SettlementBackend = "agglayer") relays each verified batch's proof-settlement tx through that exact method for consensus_contract_type "rollup"/"cdk-validium", so with it disabled the aggregator log repeats "failed to send tx to the agglayer: -10009 The interop_sendTx method is disabled" for the whole run and verified batches never progress. K3 bumped agglayer_image from 0.6.0-rc.5 (still has the legacy flow) to 0.6.0-rc.8 (permanently disabled) -- this is what broke it, confirmed via the branch's own pre-K3 CI run (16c4fde) where this exact job shape passed against rc.5 with verified batches progressing normally. rc.5 already understands the [settlement.pessimistic-proof-tx-config] schema K3 introduced (it predates rc.5, from agglayer-settlement-service / PR #1393), so no config.toml template change is needed -- just pin this one fixture's agglayer_image back to rc.5. run-with-cdk-erigon-rollup and run-with-cdk-erigon-validium hit the identical signature in the same CI run and are NOT pre-existing/unrelated as previously assumed (K7b) -- confirmed by direct evidence in plans/snapshot-v2-aggkit-e2e/K7c-evidence/. They are deliberately not touched here per K7c's explicit non-goal; see the evidence pack for the recommended follow-up. See plans/snapshot-v2-aggkit-e2e/K7c-evidence/ for full evidence, including the upstream agglayer PR diff and a secondary finding (a checkout-ref race in test.yml unrelated to this PR) that explains an earlier apparent contradiction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…res (K9) K7c proved run-with-anvil-rollup breaks under agglayer 0.6.0-rc.8 (K3's new default) because upstream PR #1637 permanently disables interop_sendTx, which cdk-node's Aggregator needs to relay verified batches for consensus_contract_type rollup/cdk-validium. .github/tests/cdk-erigon/ rollup.yml and validium.yml use that exact same flow and were verified passing at K2's 16c4fde (pre-K3, on rc.5). Per the user's explicit decision (per-fixture rc.5 pin, not a global revert), apply the same one-line agglayer_image override K7c already proved green on anvil-rollup.yml, with the same evidence comment mirrored so the next reader knows why the pin exists. constants.star's agglayer_image default is untouched and stays 0.6.0-rc.8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…mage (K9) dev-ui's D0c step removed the per-network aggkitBridgeApis map field entirely from its config schema, which is now .strict() and rejects that key outright. kurtosis-cdk still emitted it, so the baked dev-ui config would fail to load. - aggkit-dev-ui-config.json.tmpl now emits a single "aggkitProxy": "/aggkitapi" string instead of a per-network map that repeated the same URL for every L2 -- the exact duplication the user objected to. - ui.star's run_dev_ui() comment updated to reference aggkitProxy and dev-ui's "Relative aggkitProxy URLs" doc section (renamed from "Relative aggkitBridgeApis URLs"). - constants.star's agglayer_dev_ui_aggkit_image bumped to D0c's published tag (dispatch-feat-aggkit-backend-8563dd4ba876-31732860787), which understands the new field; verified pullable. - verify-anvil-aggkit.sh's structural fallback check (used when no dev-ui checkout with validateConfig.mjs is available) now asserts on appModes.configs.<mode>.aggkitProxy instead of .aggkitBridgeApis. - aggkit-2l2-with-bridge-ui.md's example config.json and prose updated to match. Repo-wide grep for aggkitBridgeApis leaves only explanatory comments about the removal (this commit's own comments, and constants.star/verify-anvil- aggkit.sh's rationale) -- zero live references. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
Description
This PR is the Kurtosis-CDK side of the Bridge Hub → aggkit bridge-service migration and the hermetic two-L2 Anvil snapshot consumed by aggkit #1802 and agglayer-dev-ui #24. The earlier opt-in
bridge_ui_backend: "aggkit"mode, standaloneaggkit_proxy, two-L2 parameters, per-chain AutoClaim, tracker configuration, and bridge/UI guides remain part of the branch; default Bridge Hub behavior remains opt-in/backward-compatible.The snapshot implementation has since been revised to the current v2 contract:
aggkit-001-bridge/aggkit-002-bridgesidecars are not part of the bundle.<repository>@sha256:<digest>and the accompanyingsummary.jsonrecords the human-readable immutable tag and its digest. The successful publish was run 31787941750; its tag/digest table is the bundle contract.snapshot-latest-devuiis only a convenience alias, not the consumer pin.0.6.0-rc.8and the current settlement configuration ([settlement.pessimistic-proof-tx-config],confirmations = 1,settlement-policy = "SafeBlock", and a 5s not-included retry). Live validation found no active[outbound.*]configuration, while both Aggkit configs setTriggerCertMode = "ASAP".agglayer-dev-ui-002is behind thedevuiprofile, so ordinarydocker compose up -d --waitstarts eight healthy containers;--profile devuiadds the ninth. The emitted bundle also removes the proxy dependency on that optional service and avoids a bare-/HAProxy health probe. Runtime config remains mountable through the dev-ui image.Verification and R1 fixes
At the current PR head
5b546412366eb5f098eda20930571936f94012ce, R1 fixed two verification defects:discovery.jsonnow fails health-check verification instead of allowing a vacuous success;verify.shoptions and positional arity now exit non-zero, while--helpremains successful.Scratch mutations prove both failures are now observable. Post-fix CI is green: snapshot/dev-ui 31809961060, CodeQL 31809962366, and the explicit 18-job test dispatch 31810302770, whose substantive jobs all ran successfully.
The v2 snapshot publish itself completed successfully in run 31787941750, including capture, gates, verification, publishing, and digest recording. Anonymous pulls of all nine digest-pinned images were verified. The companion dev-ui E2E run is green at 11m23s: 12 bridge tests, 6 built-image container tests, and 4 preflight tests passed.
Deferred §3b / review follow-ups
Three known hardening items are intentionally visible rather than silently claimed fixed:
test.ymlcheckouts still use bareactions/checkout@v4; delayed jobs can therefore test a moving branch revision rather than the triggering SHA. The explicit post-fix dispatch above was made after the branch was pushed and remained unchanged.References and merge order
anvil-2chainsE2E environment and is at0a6481cc.07e25e9.