Skip to content

getReadyToClaimCount omits the injected-GER gate that toTransaction applies, over-counting the ready-to-claim badge #31

Description

@arnaubennassar

Summary

AggkitBridgeAggregator.getReadyToClaimCount and AggkitBridgeAggregator.toTransaction derive claimability from the same two source signals (an L1-info-tree-index probe on the recording network, then a targeted claim check) but disagree on a third: whether the destination network has actually injected the corresponding GER. toTransaction checks it via resolveInjectedLeafIndex; getReadyToClaimCount does not call that method at all. The result is that the "ready to claim" count can include a deposit that the activity list (built by toTransaction) correctly reports as LEAF_INCLUDED — i.e. not yet claimable — and a user who acts on the badge builds a claim proof against an index the destination hasn't ingested yet, which reverts on-chain with GlobalExitRootInvalid.

This is pre-existing behavior from #28, not a regression from the recent review-response work.

Where (verified against fix/aggkit-pr28-review, commit 2959a83, working tree as of this filing — a concurrent edit to unrelated helpers in the same file was in progress, so exact line numbers may drift further; anchor on the function/method names if so)

  • src/aggkit/aggregator.ts, getReadyToClaimCount (method starts at line 515). Its per-row predicate, in the readyFlags block (lines 585-622), does exactly two things per unclaimed row: call getL1InfoTreeIndex on the recording network, then (if the source probe is ready) call confirmClaimed. A row counts as ready iff confirmedClaim === null. It never calls resolveInjectedLeafIndex.
  • src/aggkit/aggregator.ts, toTransaction (method starts at line 1083). For the same two prior checks (source probe ready, confirmClaimed returns null) and a non-zero destination_network, it additionally calls this.resolveInjectedLeafIndex({ destinationNetworkId, sourceL1InfoTreeIndex }). If that resolves to kind === 'not-ready' (the destination hasn't injected the leaf yet), status is set to LEAF_INCLUDED, explicitly not READY_TO_CLAIM.
  • resolveInjectedLeafIndex (same file) is the single place that queries the destination's /injected-l1-info-leaf endpoint; getReadyToClaimCount has no equivalent call anywhere in its predicate.
  • src/aggkit/types.ts:389-398 documents the state machine: LEAF_INCLUDED is explicitly the case where "the source leaf exists but the destination's GER injection lags," and READY_TO_CLAIM for an L2 destination requires "both source included AND destination injected." getReadyToClaimCount only ever checks the first half of that conjunction.

Reproduction shape

Two configured networks {1, 2}. One unclaimed bridge recorded on network 1, destination_network: 2, deposit_count: 5:

  • GET /l1-info-tree-index?network_id=1&deposit_count=5 → 200, ready, some index N (source settled on the L1 info tree).
  • GET /injected-l1-info-leaf?network_id=2&leaf_index=N → 404 "not injected yet" (destination hasn't ingested the GER).

With this data:

  • getActivity (via toTransaction) returns this row with status: 'LEAF_INCLUDED' and no claim proof — correct, matches the documented state machine.
  • getReadyToClaimCount counts this same row as ready (its predicate returns true once the source probe is ready and confirmClaimed returns null, having never queried /injected-l1-info-leaf) — incorrect.

The badge (backed by getReadyToClaimCount, consumed via app/hooks/useReadyToClaimCount.ts in the dev-ui) therefore advertises a claimable deposit that the activity list, on the same data, correctly shows as not yet claimable. Building and submitting a claim proof for that deposit reverts on-chain with GlobalExitRootInvalid.

Relationship to #30

#30 tracks a redesign of this same code path — the readyFlags/fan-out logic in getReadyToClaimCount is explicitly in scope there (its own Background section names getReadyToClaimCount directly, and its Scope item 3 already covers the ready-probe concurrency/error-surfacing behavior in this exact predicate). This issue does not propose an independent fix; it's the correctness gap in the same predicate that #30's rework should close, so whoever implements #30 should fold in the missing resolveInjectedLeafIndex (or equivalent injected-GER) check when the per-row readiness logic is rewritten, rather than carrying it forward unaddressed.

Suggested direction (for whoever picks this up)

Mirror toTransaction: after the source probe succeeds and before/alongside confirmClaimed, call resolveInjectedLeafIndex({ destinationNetworkId: row.bridge.destination_network, sourceL1InfoTreeIndex: probe.value }) and treat kind === 'not-ready' as not-ready-to-claim, the same way toTransaction derives LEAF_INCLUDED. This adds one probe per remaining candidate row; if that per-request cost is unacceptable for the badge, the alternative is to explicitly document the count as an upper bound rather than leave the two paths silently disagreeing.

Activity

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions