Skip to content

fix: aggkit-proxy: wait for L1 info tree leaf availability before claiming L2->L1 bridges 1823 - #1829

Merged
joanestebanr merged 7 commits into
developfrom
fix/1823-l2-to-l1-l1-info-leaf-available
Sep 7, 2026
Merged

joanestebanr merged 7 commits into
developfrom
fix/1823-l2-to-l1-l1-info-leaf-available

Conversation

@joanestebanr

@joanestebanr joanestebanr commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

🔄 Changes Summary

  • For L2->L1 bridges, the tracker jumped straight from WaitL1SettledGER to WaitingClaim once the certificate's settlement tx was confirmed on L1 — but that doesn't guarantee the bridge-service instance that will actually build the claim proof has caught up yet, since its own L1 info tree sync can lag behind the finality this tracker uses for WaitL1SettledGER. A bridge could look claimable per this tracker's own settlement check yet still fail to claim because that instance couldn't produce a proof for it.
  • Adds a new step, WaitingL1InfoLeafAvailable, always right before WaitingClaim, on every route (L1->L2, L2->L1, L2->L2) — never skipped, never inferred from a sibling step. It calls GET /bridge/v1/l1-info-tree-index against the bridge-service instance that will build the claim proof for the deposit — the origin network's own instance, or, when the origin is mainnet (which has no bridge-service deployment of its own), the destination's instance instead (network_id=0 sent as a query parameter there, never as the URL-resolution key — mainnet has no resolvable bridge-service URL of its own). StepWaitingGERInjection only confirms an L2-side fact (the GER's injection tx landing on the destination's own contract); it says nothing about whether the proof-building instance's own sync has caught up, so this step always runs its own check regardless of what preceded it.
  • Adds a claim_status field to TrackingData (both the REST tracker response and every WebSocket status message): a simplified claim-readiness summary — "error" whenever tracking_status is "error" (takes priority over everything else), "claimed" once the current step is Claimed, "readyToClaim" while it is WaitingClaim, "pending" otherwise — for clients that only care about "is this claimable" without walking all_steps themselves.
  • Adds an api_revision field to GET /tracker/v1/health's response: an integer, bumped whenever a change to the tracker's wire API contract could break an existing client, so a client can tell which contract shape a given instance speaks without diffing full response bodies. Purely informational.

⚠️ Breaking Changes

  • 🔌 API/CLI: the tracker's /tracker/v1/... responses now include one extra step, WaitingL1InfoLeafAvailable, in all_steps, right before WaitingClaim — on every route (L1->L2, L2->L1, L2->L2). Its result is {"l1_info_tree_index": <uint32>}. Existing steps' step_names and result shapes are unchanged. TrackingData also gains a new claim_status field, and the health response a new api_revision field (both additive, existing fields unchanged).

📋 Config Updates

  • None.

✅ Testing

  • 🤖 Automatic:
    • WaitingL1InfoLeafAvailable: unit tests in bridgetracker/domain/path_test.go, bridgetracker/domain/resolve_steps_test.go, bridgetracker/types/status_test.go, and TestEngineLifecycleL1ToL2/TestEngineLifecycleL2ToL1/TestEngineLifecycleL2ToL2 in bridgetracker/engine_test.go walking each route's full lifecycle through the new step.
    • GERSource.L1InfoTreeIndexForBridge: real httptest-backed tests in bridgetracker/sources/ger_test.go covering the mainnet-origin (destination queried) and L2-origin (self-queried) branches, plus not-yet-covered and URL-resolution-failure cases.
    • claim_status: domain.TrackingData.ClaimStatus() derivation table in bridgetracker/domain/tracking_data_test.go, plus wire-level tests in bridgetracker/api/tracking_data_test.go and bridgetracker/types/tracking_test.go.
    • api_revision: extended TestHealthHandler in bridgetracker/bridgetracker_test.go.
  • 🖱️ Manual: n/a.

🐞 Issues

🔗 Related PRs

  • None.

📝 Notes

  • Also updates docs/bridgetracker.md, docs/bridgetracker/README.md (+ mermaid diagram) and docs/bridgetracker/API.md, and regenerates the bridgetracker swagger docs (make generate-swagger-docs).

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-04T07:25:05.665855Z 12afa44 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12afa44789

ℹ️ 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".

Comment thread bridgetracker/domain/resolve_step_waiting_l1_info_leaf_available.go Outdated
@joanestebanr

Copy link
Copy Markdown
Collaborator Author

@claude review

joanestebanr added a commit that referenced this pull request Sep 4, 2026
…ridge-service instance

Fix a review finding on #1829 (chatgpt-codex-connector): mainnet
(network 0) has no bridge-service deployment of its own —
bridgeservicefinder.GetURL(0) fails with ErrURLNotFound unless a
network-0 URL was explicitly configured, which the shipped default
does not do. GERSource.L1InfoTreeIndexForBridge was resolving the
queried instance from bridge.NetworkID directly, which is 0 for every
L1->L2 bridge — turning StepWaitingL1InfoLeafAvailable into a
permanent, repeated error for the whole route instead of letting it
reach StepWaitingClaim.

When the origin is mainnet, query the destination's own instance
instead (a real rollup, always resolvable), with network_id=0 as a
query parameter rather than as the resolution key — the same pattern
coveringLeafIndex/OriginGER already rely on for this exact case. Add
bridgetracker/sources/ger_test.go pinning both branches (destination
queried for an L1-originated bridge, origin self-queried for an
L2-originated one) plus the not-yet-covered and URL-resolution-failure
cases, none of which the existing fake-backed domain/engine tests can
catch since they bypass real URL resolution entirely.
@agglayer agglayer deleted a comment from claude Bot Sep 4, 2026
…aiming L2->L1 bridges

For an L2->L1 bridge, the tracker jumped straight from WaitL1SettledGER to
WaitingClaim once the certificate's settlement tx was confirmed on L1. That
does not guarantee the destination network's own bridge-service instance has
indexed the resulting L1 info tree leaf yet: its own L1 info tree sync can lag
behind the finality this tracker uses for WaitL1SettledGER (e.g. it waits for
the block to be finalized), so a bridge could look claimable while the
destination still could not produce a proof for it.

Add a new StepWaitingL1InfoLeafAvailable, only for the L2->L1 path, between
WaitL1SettledGER and WaitingClaim. It reuses the same mechanism
StepWaitingGERInjection already relies on for L2->L2 (GERSource.
InjectedGERAtIndex against the destination's own bridge-service instance),
which for a mainnet destination resolves straight against l1infotreesync's
own indexing state.

Fixes #1823
Add a new claim_status field to TrackingData (both the REST tracker
response and every WebSocket "status" message), a simplified
claim-readiness summary for clients that only care about "is this
claimable" without walking all_steps themselves:

- ERROR whenever tracking_status is error (this takes priority over
  everything else)
- CLAIMED once the current step is StepClaimed
- READY_TO_CLAIM while the current step is StepWaitingClaim
- PENDING otherwise, including before the bridge resolves

Computed via a new domain.TrackingData.ClaimStatus method, mirroring
TrackingStatus's own derivation pattern, backed by a new
types.TrackerClaimStatus enum (distinct from the unrelated
types.ClaimStatus used by the Activity endpoint's own on-chain
isClaimed() check).
…e claiming

StepWaitingL1InfoLeafAvailable was only reached on the L2->L1 path. For
L1->L2 and L2->L2, StepWaitingGERInjection was assumed to already imply
the leaf is available — but it only confirms the GER's injection tx
landed on the destination network's own contract (an L2-side fact), not
that the destination's own bridge-service instance has its L1 info tree
sync caught up, which is what it actually needs to produce a claim
proof. Those are different facts and can legitimately disagree.

Move StepWaitingL1InfoLeafAvailable so it is always the step right
before StepWaitingClaim, on every route, never skipped and never
inferred from StepWaitingGERInjection having just completed. Its
resolver now looks up the settled leaf index by scanning AllSteps for
whichever earlier step resolved it (StepWaitL1SettledGER for an
L2-originated bridge, StepWaitingGERUpdate for an L1-originated one)
instead of assuming it sits immediately before, since
StepWaitingGERInjection now sits in between on the routes that have it.
…fAvailable

TestBridgeTrackerL1ToL2 pinned the exact all_steps shape for the L1->L2
route, which now includes WaitingL1InfoLeafAvailable right before
WaitingClaim. Also mirrors the new claim_status field in
trackerTrackingData and asserts it at the readyToClaim/claimed
transitions this test already exercises.
…st the proof-building instance

StepWaitingL1InfoLeafAvailable previously called the same
InjectedGERAtIndex mechanism StepWaitingGERInjection uses, against the
bridge's destination network. That checks an L2-side fact only (did the
GER's injection tx land on that network's own contract) and, per
docs/bridge_service.md's documented flow and the l1-info-tree-index
endpoint's own network_id validation (network_id must be either mainnet
or the queried instance's own network — confirmed by
bridge_test.go's "Invalid network ID" case), was never actually a valid
combination for an L2-originated bridge's non-mainnet origin anyway.

The bridge-service instance that will actually build the claim proof is
the origin network's own instance (see docs/bridge_service.md's L2->L2
sequence: bridge_getProof is called there, against the same instance
this step now queries). Replace the resolver's mechanism entirely:

- New GERSource.L1InfoTreeIndexForBridge queries GET
  /bridge/v1/l1-info-tree-index on the origin network's own
  bridge-service instance (network_id=origin, deposit_count=origin's
  own deposit count) — the one combination the endpoint always accepts.
- StepWaitingL1InfoLeafAvailable's Resolve no longer needs to look up a
  leaf index from a previous step at all; the endpoint resolves
  directly from the bridge's own origin network/deposit count.
- Its Result is now types.L1InfoLeafAvailableResult{L1InfoTreeIndex},
  replacing the reused types.InjectedGERL1Leaf (GER/block_number/
  block_timestamp are not part of this endpoint's response).

Still always the step right before StepWaitingClaim, on every route,
still never skipped or inferred from StepWaitingGERInjection completing
— that remains an L2-side fact, distinct from whether the proof-building
instance's own L1 info tree sync has caught up.
…ridge-service instance

Fix a review finding on #1829 (chatgpt-codex-connector): mainnet
(network 0) has no bridge-service deployment of its own —
bridgeservicefinder.GetURL(0) fails with ErrURLNotFound unless a
network-0 URL was explicitly configured, which the shipped default
does not do. GERSource.L1InfoTreeIndexForBridge was resolving the
queried instance from bridge.NetworkID directly, which is 0 for every
L1->L2 bridge — turning StepWaitingL1InfoLeafAvailable into a
permanent, repeated error for the whole route instead of letting it
reach StepWaitingClaim.

When the origin is mainnet, query the destination's own instance
instead (a real rollup, always resolvable), with network_id=0 as a
query parameter rather than as the resolution key — the same pattern
coveringLeafIndex/OriginGER already rely on for this exact case. Add
bridgetracker/sources/ger_test.go pinning both branches (destination
queried for an L1-originated bridge, origin self-queried for an
L2-originated one) plus the not-yet-covered and URL-resolution-failure
cases, none of which the existing fake-backed domain/engine tests can
catch since they bypass real URL resolution entirely.
Add a new api_revision field to GET /tracker/v1/health's HealthResponse:
an integer, backed by a new types.CurrentAPIRevision constant, meant to
be bumped by one whenever a change to the tracker's wire API contract
could break an existing client — a field added/removed/renamed, an
enum's value set changed, a new step inserted into a bridge's expected
path, and the like. Purely informational: the tracker never rejects or
alters behavior based on it, it just lets a client tell which contract
shape a given instance speaks without diffing full response bodies.
@joanestebanr
joanestebanr force-pushed the fix/1823-l2-to-l1-l1-info-leaf-available branch from c2460db to 2cc57d9 Compare September 4, 2026 13:09
@joanestebanr
joanestebanr requested a lite review from Copilot September 7, 2026 08:02
@joanestebanr
joanestebanr merged commit 0945562 into develop Sep 7, 2026
32 of 33 checks passed
@joanestebanr
joanestebanr deleted the fix/1823-l2-to-l1-l1-info-leaf-available branch September 7, 2026 08:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The functional change is well-covered by unit/e2e tests and is localized to an additive lifecycle step plus additive API fields, with only minor documentation wording nits remaining.

Pull request overview

This PR updates the bridgetracker (and related docs/tests) to prevent premature “claimable” status by inserting a dedicated wait step that verifies the proof-building bridge-service instance has indexed the relevant L1 info tree leaf before the tracker reports WaitingClaim.

Changes:

  • Adds a new lifecycle step WaitingL1InfoLeafAvailable (always executed immediately before WaitingClaim) that polls /bridge/v1/l1-info-tree-index on the proof-building bridge-service instance.
  • Adds claim_status to tracking responses/messages as a simplified claim-readiness indicator derived from tracking_status + current step.
  • Adds api_revision to /tracker/v1/health to identify wire-contract revisions.
File summaries
File Description
test/e2e/proxy_tracker_test.go Updates e2e expectations for the new step and asserts claim_status.
docs/bridgetracker/README.md Documents the inserted step and updates the lifecycle mermaid diagram.
docs/bridgetracker/bridge-tracker-infographic.html Updates infographic to include the new step and claim_status in examples/simulator.
docs/bridgetracker/API.md Extends API docs with claim_status, api_revision, and the new step result shape.
docs/bridgetracker.md Updates high-level bridgetracker docs to include the new step in each route.
docs/assets/swagger/bridge_tracker/swagger.json Regenerated swagger artifact for the tracker docs.
bridgetracker/types/tracking.go Introduces TrackerClaimStatus enum and string mapping.
bridgetracker/types/tracking_test.go Adds unit test coverage for TrackerClaimStatus.String().
bridgetracker/types/status.go Adds StepWaitingL1InfoLeafAvailable and its result type.
bridgetracker/types/status_test.go Extends step string tests for the new step.
bridgetracker/types/health.go Adds CurrentAPIRevision and exposes it via HealthResponse.
bridgetracker/sources/ger.go Adds L1InfoTreeIndexForBridge to query the proof-building instance for leaf availability.
bridgetracker/sources/ger_test.go Adds httptest-based unit tests covering mainnet-origin vs L2-origin querying behavior.
bridgetracker/ports.go Extends GERSource port with L1InfoTreeIndexForBridge.
bridgetracker/engine.go Registers the resolver for StepWaitingL1InfoLeafAvailable.
bridgetracker/engine_test.go Adds lifecycle tests ensuring the new step is present and never skipped across routes.
bridgetracker/domain/tracking_data.go Adds TrackingData.ClaimStatus() derivation logic.
bridgetracker/domain/tracking_data_test.go Adds derivation table tests for ClaimStatus().
bridgetracker/domain/resolve_steps_test.go Updates step resolution tests to include the new step and fact queries.
bridgetracker/domain/resolve_step_waiting_l1_info_leaf_available.go Adds the new step resolver that waits until the proof-building instance reports the leaf index.
bridgetracker/domain/path.go Updates expected paths to include WaitingL1InfoLeafAvailable before WaitingClaim.
bridgetracker/domain/path_test.go Updates expected path tests for all bridge routes.
bridgetracker/domain/bridge_step_path.go Extends step result plumbing to carry L1InfoLeafAvailableResult.
bridgetracker/bridgetracker_test.go Extends health handler test to assert api_revision.
bridgetracker/api/tracking_data.go Adds claim_status to wire TrackingData.
bridgetracker/api/tracking_data_test.go Adds JSON and mapping tests for claim_status.
bridgetracker/api/health_command.go Populates api_revision in the health response.
bridgetracker/api/docs/swagger.yaml Updates tracker swagger yaml for new fields and result type.
bridgetracker/api/docs/swagger.json Updates tracker swagger json for new fields and result type.
bridgetracker/api/docs/docs.go Updates embedded swagger template for new fields and result type.
bridgetracker/api/bridge_step_path.go Updates API step-path documentation comment to include the new result type.
Review details

Files not reviewed (1)

  • bridgetracker/api/docs/docs.go: Generated file
  • Files reviewed: 30/31 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +26 to +34
// WaitingL1InfoLeafAvailableResolver resolves StepWaitingL1InfoLeafAvailable: whether the
// bridge-service instance that will actually build the claim proof — the origin network's own
// instance — has its own L1 info tree sync caught up to this bridge's deposit yet, per GET
// /bridge/v1/l1-info-tree-index. Always the current step right before StepWaitingClaim, on
// every route: that sync can lag behind whatever this tracker itself uses elsewhere
// (StepWaitL1SettledGER's own L1 RPC scan, StepWaitingGERInjection's destination-side injection
// check), so a bridge can look claimable per those checks yet still fail to claim because the
// proof-building instance cannot produce a proof for it yet — this is why it is never skipped
// or inferred from a sibling step, on any route (#1823)
<span class="routes"><span class="route-tag tag-l1l2">L1→L2</span><span class="route-tag tag-l2l1">L2→L1</span><span class="route-tag tag-l2l2">L2→L2</span></span>
</div>
<p class="step-desc">The GER that includes the bridge is available on the destination network. The bridge is ready to be claimed with <code>claimAsset()</code> / <code>claimMessage()</code>.</p>
<p class="step-desc">The destination network's own bridge-service instance has the covering L1 info tree leaf indexed. The bridge is ready to be claimed with <code>claimAsset()</code> / <code>claimMessage()</code>.</p>
Comment on lines 495 to +496
These values are the <span class="mono">BridgeStep</span> enum, carried in every
<span class="mono">all_steps[].step</span> entry: <span class="mono">0→WaitingGERUpdate … 7→Claimed</span>.
<span class="mono">all_steps[].step</span> entry: <span class="mono">0→WaitingGERUpdate … 8→Claimed</span>.
joanestebanr added a commit that referenced this pull request Sep 7, 2026
…info tree index lookup

ActivitySource.IsReadyToClaim asked the destination network's own bridge-service
instance for GET /bridge/v1/l1-info-tree-index, passing the origin network as the
network_id query param. The real endpoint only accepts network_id == mainnet or
network_id == the queried instance's own network, so every non-mainnet-originated
bridge got rejected there and stayed reported as pending/errors.readiness even once
claimable.

Route it the same way GERSource.L1InfoTreeIndexForBridge already does (the #1829
fix): query the origin's own instance, falling back to the destination's instance
only when the origin is mainnet (which has no bridge-service deployment of its
own). Extracted the routing rule into a shared bridgeServiceClients.l1InfoTreeIndexClientFor
helper used by both.

Addresses #1831 (comment)
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.

bug: proxy: After step WaitL1SettledGER we must wait until the L1 block is ready to generate the proof (usually finalized depending on network)

3 participants