Skip to content

feat(aggoracle): add UseUpdateExitRoot mode to forward both exit roots#1597

Open
revitteth wants to merge 1 commit intoagglayer:developfrom
revitteth:rd-862/update-exit-root-mode
Open

feat(aggoracle): add UseUpdateExitRoot mode to forward both exit roots#1597
revitteth wants to merge 1 commit intoagglayer:developfrom
revitteth:rd-862/update-exit-root-mode

Conversation

@revitteth
Copy link
Copy Markdown
Collaborator

Summary

Adds an opt-in UseUpdateExitRoot mode to AggOracle.EVMSender so the chain sender forwards the uncombined (mainnet, rollup) exit root pair via updateExitRoot(bytes32 newRollupExitRoot, bytes32 newMainnetExitRoot) instead of the combined keccak(mainnet||rollup) hash via insertGlobalExitRoot(bytes32 combined).

Why

On sovereign chains that can't reverse keccak(mainnet||rollup) (e.g. Miden), the combined-hash path forces the receiving service to fetch live L1 state and verify keccak(latest_mainnet, latest_rollup) == combinedGER. Under rapid L1 deposit load this races: by the time a given GER lands at the destination, L1 has typically advanced and the verification fails — the GER is stored without resolved roots, and any deposit covered by it can be stranded ready_for_claim=false indefinitely.

We measured 85% orphan rate at N=30 back-to-back deposits on the receiving service (gateway-fm/miden-agglayer): 18 of 21 GERs failed to decompose. Forwarding the pair from the L1InfoTreeLeaf aggoracle already has eliminates the race entirely — orphan rate 0%, deposit→ready_for_claim conversion 30/30.

Design

  • Additive, no breakage of existing modes:
    • DirectInjectionMode (default) — unchanged, calls insertGlobalExitRoot(combined).
    • AggOracleCommitteeMode — unchanged, calls proposeGlobalExitRoot(combined).
    • DirectUpdateExitRootMode (new) — calls updateExitRoot(rollup, mainnet).
  • Selected via new config field EVMConfig.UseUpdateExitRoot bool.
  • L1InfoTreeSyncer interface gains GetLatestL1InfoLeaf() (the impl already exposes it).
  • ChainSender interface gains additive UsesTwoRootMode() + ProcessGERWithRoots() methods. Existing ProcessGER(ctx, ger) left in place.
  • AggOracle.processLatestGER() branches on UsesTwoRootMode() and dispatches accordingly.

Manual calldata packing

EVMChainGERSender.InjectExitRoots() packs updateExitRoot(bytes32,bytes32) calldata by hand using the known selector 0x736ca7f4. The reason: cdk-contracts-tooling@v0.0.12's agglayergerl2 Go binding only exposes the 1-arg updateExitRoot(bytes32) overload, so abi.Pack(funcName, rollup, mainnet) would fail. The 2-arg selector is what the Miden sovereign GER manager (and any contract that supports recomputing the combined GER on-chain) actually expects.

If/when cdk-contracts-tooling ships a binding with both overloads, the manual packing can swap back to abi.Pack.

Test plan

  • go test ./aggoracle/... — green (oracle + chaingersender)
  • End-to-end on gateway-fm/miden-agglayer's docker-compose stack: orphan rate 85% → 0%, conversion 25/30 → 30/30
  • Reviewer: smoke-test against an existing direct-injection deployment (no config change → no behavior change)

Reference

🤖 Generated with Claude Code

revitteth added a commit to mandrigin/aggkit-proxy that referenced this pull request Apr 27, 2026
…faucet_registry migration

End-to-end validation of the RD-862 fix on the kurtosis stack required
three small alignments:

1. `params.yaml`: set `aggkit_image: "miden-agglayer/aggkit:rd-862"` so
   the kurtosis-cdk parser threads our locally-built patched aggkit
   through to `bridge_service.star::_deploy_aggkit`. Without this the
   default `ghcr.io/agglayer/aggkit:0.9.0-rc2` was picked up and the
   `UseUpdateExitRoot` config was silently ignored.

2. `bridge_service.star`: add `UseUpdateExitRoot = true` to the
   `[AggOracle.EVMSender]` section of the inlined aggkit config
   template. This is the flag the patched aggkit reads to switch
   `EVMChainGERSender` from `direct_injection` to
   `direct_update_exit_root` mode.

3. `miden_services.star`: append `migrations/002_faucet_registry.sql`
   (CREATE TABLE faucet_registry + idx_faucet_origin) to the inlined
   migration SQL. Previously only `001_initial.sql` was inlined, so
   miden-proxy startup failed with
   `db error ERROR: relation "faucet_registry" does not exist`. Added
   a comment marking the inline as a snapshot and pointing at the
   long-term fix (upload the migrations dir as a files-artifact).

Validation: kurtosis enclave deployed cleanly with all services
RUNNING. aggkit logs show
`EVMChainGERSender initialized in direct_update_exit_root mode`
followed by `update-exit-root GER transaction submitted` — i.e. the
two-root calldata path. miden-proxy logs show 0
`L1 exit roots don't match injected GER` warnings (orphan count = 0).
The L1→Miden test deposit completes; both injected GERs committed
without decomposition.

Refs:
- gateway-fm/miden-agglayer#37
- agglayer/aggkit#1597
- Linear RD-862

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@revitteth
Copy link
Copy Markdown
Collaborator Author

@mandrigin — could you take a look when you have a moment? GitHub won't let me request you formally as a reviewer (not a collaborator on agglayer/aggkit) so cc-ing here.

This closes RD-862 on the gateway-fm/miden-agglayer side: under rapid L1 deposit load, ~85% of GERs orphan because the receiving service can't reverse keccak(mainnet || rollup) once L1 has advanced. Patch adds an opt-in UseUpdateExitRoot mode where the chain sender forwards both roots from the L1InfoTreeLeaf it already has, calling updateExitRoot(bytes32 newRollupExitRoot, bytes32 newMainnetExitRoot) instead of insertGlobalExitRoot(bytes32 combined).

Backward-compatible (additive interface methods, new DirectUpdateExitRootMode constant, default behaviour unchanged), go test ./aggoracle/... green, and verified end-to-end against a Miden sovereign chain (orphan rate 85% → 0%, 30/30 deposits ready_for_claim under N=30 0ms-gap burst).

The receiving-side PR is at gateway-fm/miden-agglayer#37 if you want the full picture.

Closes the GER decomposition race (RD-862) on sovereign chains that can't
reverse keccak(mainnetExitRoot || rollupExitRoot). When configured with
`UseUpdateExitRoot: true`, the EVM ChainSender forwards the uncombined
(mainnet, rollup) pair via updateExitRoot(bytes32,bytes32) instead of the
combined hash via insertGlobalExitRoot(bytes32). The downstream service
no longer needs to fetch live L1 state to decompose, eliminating the
race window that strands deposits as ready_for_claim=false under
rapid-burst L1 traffic.

Why a new mode (vs. flipping DirectInjectionMode):
- Existing DirectInjectionMode is the universal default and other rollups
  may rely on the combined-hash semantics. Gating behind a config flag
  preserves backward compat.
- AggOracleCommitteeMode is unaffected; the committee path still uses
  proposeGlobalExitRoot(combined).

How it threads through:
- L1InfoTreeSyncer interface gains GetLatestL1InfoLeaf() so the oracle
  can fetch the full leaf instead of just the GER hash. The implementer
  (l1infotreesync.L1InfoTreeSync) already exposes this method.
- ChainSender interface gains UsesTwoRootMode() + ProcessGERWithRoots()
  for additive opt-in. Existing ProcessGER() unchanged.
- AggOracle.processLatestGER() branches on the sender's mode flag and
  fetches/dispatches accordingly.
- EVMChainGERSender.InjectExitRoots() packs updateExitRoot calldata
  manually because cdk-contracts-tooling@v0.0.12's agglayergerl2 binding
  only exposes the 1-arg `updateExitRoot(bytes32)` overload. The 2-arg
  `updateExitRoot(bytes32 newRollupExitRoot, bytes32 newMainnetExitRoot)`
  is the variant that actually carries the pair (selector 0x736ca7f4).

Validation:
- `go test ./aggoracle/...` — green
- e2e on miden-agglayer's docker-compose stack: orphan GER rate at
  N=30 back-to-back deposits dropped from 85% (18/21) to 0% (0/8),
  deposit→ready_for_claim conversion 30/30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@revitteth revitteth force-pushed the rd-862/update-exit-root-mode branch from d46f980 to 490ec81 Compare April 29, 2026 06:49
@revitteth
Copy link
Copy Markdown
Collaborator Author

@arnaubennassar @joanestebanr — apologies for the misdirected cc to mandrigin in my earlier comment (had a wrong mental model of who maintains aggkit). Could either of you take a look when you have a moment?

CI is fully green on the substance side:

  • lint
  • Run Go E2E Tests ✓ (16m45s)
  • Analyze (go) / CodeQL ✓
  • Build Docker Image

The one red check (test-unit) is the SonarCloud step inside that job failing because SONAR_TOKEN isn't exposed to fork PRs (Project not found ... please check the SONAR_TOKEN environment variable — see job log). The actual go test step inside the same job passes — you can see Test ✓ in the job's step list.

Recap of the change: opt-in UseUpdateExitRoot mode for AggOracle.EVMSender so sovereign chains that can't reverse keccak(mainnet||rollup) (e.g. Miden) get the uncombined (rollup, mainnet) pair via updateExitRoot(bytes32,bytes32) and don't race L1. Fully additive — three modes (DirectInjectionMode default unchanged, AggOracleCommitteeMode unchanged, new DirectUpdateExitRootMode) selected via EVMConfig.UseUpdateExitRoot bool. Blocks gateway-fm/miden-agglayer#37 (RD-862, validated end-to-end: 85% → 0% orphan rate, 30/30 deposits ready_for_claim under N=30 0ms-gap burst).

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant