Skip to content

fix(l2gersync): adapt isGERRemovedFromL2 scan to RPC eth_getLogs block-range cap - #1813

Merged
joanestebanr merged 2 commits into
developfrom
fix/l2gersync-max-block-range-1812
Sep 1, 2026
Merged

fix(l2gersync): adapt isGERRemovedFromL2 scan to RPC eth_getLogs block-range cap#1813
joanestebanr merged 2 commits into
developfrom
fix/l2gersync-max-block-range-1812

Conversation

@joanestebanr

Copy link
Copy Markdown
Collaborator

🔄 Changes Summary

  • isGERRemovedFromL2 scanned for a GER's UpdateRemovalHashChainValue removal event from the insert block (which can be arbitrarily far behind the head) to "latest" in a single open-ended eth_getLogs call.
  • Once the chain advanced past the RPC provider's block-range cap, that call failed with query exceeds max block range N on every single appender retry, logging an ERROR forever and never actually recovering a genuinely-removed GER.
  • scanRemovedGERs now parses that error via aggkitcommon.ParseMaxRangeFromError and retries chunked — no config parameter involved, the same adaptive pattern already used by L2EVMGERReader.GetRemovedGERsForRange and AgglayerBridgeL2Reader.fetchUnsetClaimsWithFallbackChunking/getUnsetClaimsInChunks.
  • The learned cap is cached on removalScanMaxRange so that, once learned, later retries skip straight to the chunked path instead of repeating the doomed unbounded call.
  • fetchRemovedGERsChunk recurses per-chunk (mirroring those siblings) so a chunk that is itself still too large keeps adapting instead of failing the whole scan outright.

⚠️ Breaking Changes

  • 🛠️ Config: none.
  • 🔌 API/CLI: none — internal only (downloaderSovereign.scanRemovedGERs/fetchRemovedGERsChunk).
  • 🗑️ Deprecated Features: none.

📋 Config Updates

  • None.

✅ Testing

  • 🤖 Automatic: added TestDownloaderSovereign_IsGERRemovedFromL2_RecoversFromMaxBlockRangeError (reproduces the max-range error and verifies the chunked recovery combines results across chunks) and TestDownloaderSovereign_IsGERRemovedFromL2_CachesLearnedMaxRangeAcrossCalls (proves a second call skips the doomed unbounded call once the cap is learned). Full l2gersync suite green, go vet clean, golangci-lint 0 issues.
  • 🖱️ Manual: reproduced against a live bali-82-op bridge-service node whose L1InfoTreeSync was still far behind head — the unbounded scan was failing every retry with this exact error; confirmed the chunked fallback resolves it.

🐞 Issues

🔗 Related PRs

📝 Notes

  • None.

@joanestebanr joanestebanr self-assigned this Aug 26, 2026
@joanestebanr joanestebanr added bug Something isn't working bridge labels Aug 26, 2026
@joanestebanr joanestebanr changed the title fix(l2gersync): adapt removal-event scan to RPC eth_getLogs block-range cap fix(l2gersync): adapt isGERRemovedFromL2 scan to RPC eth_getLogs block-range cap Aug 26, 2026
…ge cap

isGERRemovedFromL2 scanned for the GER removal event from the insert
block (which can be arbitrarily far behind the head) to "latest" in one
open-ended eth_getLogs call. Once the chain advanced past the RPC
provider's block-range cap, that call failed with "query exceeds max
block range N" on every single appender retry, logging an ERROR forever
and never actually recovering a genuinely-removed GER.

scanRemovedGERs now parses that error via ParseMaxRangeFromError and
retries chunked - the same adaptive, config-free pattern already used by
L2EVMGERReader.GetRemovedGERsForRange and AgglayerBridgeL2Reader's
fetchUnsetClaimsWithFallbackChunking/getUnsetClaimsInChunks - caching the
learned cap (removalScanMaxRange) so later retries skip the doomed
unbounded call, and recursing per-chunk (fetchRemovedGERsChunk) so a
chunk that is itself still too large keeps adapting.

Closes #1812

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@joanestebanr
joanestebanr force-pushed the fix/l2gersync-max-block-range-1812 branch from 7f3e35d to 3583743 Compare September 1, 2026 08:23
@joanestebanr
joanestebanr enabled auto-merge (squash) September 1, 2026 08:25
@joanestebanr
joanestebanr enabled auto-merge (squash) September 1, 2026 10:27
@joanestebanr
joanestebanr merged commit fe65832 into develop Sep 1, 2026
31 checks passed
@joanestebanr
joanestebanr deleted the fix/l2gersync-max-block-range-1812 branch September 1, 2026 10:49
joanestebanr added a commit that referenced this pull request Sep 2, 2026
…, GER settlement/injection fixes (#1815)

## 🔄 Changes Summary
- **`feat(bridgetracker)`**: new `GET
/tracker/v1/activity/from/{from_address}` endpoint that scans every
bridge service known to the `bridgeservicefinder` (via new
`Finder.NetworkIDs()`) for bridges sent by an address and resolves each
one's claim state (`ActivityCache`, `ActivitySource`,
`ActivityQuerier`/`ActivityBridgeScanner`/`ActivityClaimChecker` ports).
Opt-in via `includeTracking=true` to also register still-unclaimed
bridges with the tracker. Endpoint is only registered when wired
(`proxy/cmd/run.go`), and swagger docs are regenerated.
- **`feat(bridgetracker)`**: `StepClaimed` is now its own tracked step
with a dedicated `ClaimedResolver` that fetches the claim tx/block from
the destination network's bridge service, decoupled from
`StepWaitingClaim`, which now checks `isClaimed()` on-chain directly
through a new `ClaimChecker` port instead of waiting on the indexed
claim record — faster and authoritative, at the cost of not carrying
claim tx details itself (that's what the new `Claimed` step result is
for). The on-chain binding/cache logic is factored out of
`ActivitySource` into `sources/claim_checker.go`
(`contractClaimCheckers`) and shared by both the tracker engine and the
activity endpoint.
- **`fix(bridgetracker)`**: introduce `domain.ScannedBridge` to track
which network's bridge service actually reported a scanned bridge
(`NetworkID`), distinct from `Bridge.OriginNetwork` (the bridged
*asset*'s origin network) — the two diverge when an asset is re-bridged
across more than one hop, which was feeding the wrong
`sourceBridgeNetwork` into `isClaimed()` and the wrong network into
`TrackingID`/the activity endpoint's `bridge_network_id` for such
bridges.
- **`feat(bridgetracker)`**: new `GET /bridge-address[/{network_id}]`
endpoint, resolving the bridge contract address for one network or every
network currently known — opt-in via `Config.BridgeAddressResolver`,
wired in `proxy/cmd/run.go` off `bridgeservicefinder.Finder` directly.
Backed by a new `Config.BridgeAddress` override map on
`bridgeservicefinder.Finder` itself (priority: per-network entry, then
`[0]` as the default, then the rollup manager's own on-chain
`BridgeAddress()`).
- **`feat(bridgetracker)`**: `ClaimResult` now also carries
`BlockTimestamp` alongside `BlockNumber`. `InjectedGERResult` is
restructured into `L1InfoTreeLeaf` (the L1
`UpdateL1InfoTree`/`UpdateL1InfoTreeV2` event that produced the covering
leaf) and an optional `L2InjectedGER` (the actual L2 block/timestamp the
GER was injected at on the destination network) — fixing #1818, where
the L1 block was returned in the L2 field's place. When the
destination's bridge-service instance doesn't report the L2 injection
block itself (`injected_l2_block_num`/`injected_l2_block_timestamp` on
`GET /bridge/v1/injected-l1-info-leaf`), `GERSource` falls back to
scanning that network's own `GlobalExitRootManagerL2` contract for the
`UpdateHashChainValue` event backwards in chunks, via the new
`Tracker.L2GlobalExitRootAddress` per-network contract address map (a
workaround-only fallback, see its doc) — bounded by a new
`Tracker.L2InjectionLookbackBlocks` (default 1,000 blocks) instead of
always walking back to genesis.
- **`fix(bridgetracker)`**: resolve the settled GER correctly when the
settlement tx has no `UpdateL1InfoTree` event: walk backwards on L1 for
the most recent earlier one instead of surfacing "not ready" forever,
excluding same-block logs from a later transaction, and querying
`eth_getLogs` in chunks that respect providers' 10,000-block range cap.
- **`fix(bridgetracker)`**: gate certificate settlement on the
settlement tx actually being visible on L1 (not just the agglayer client
reporting `Settled`), avoiding a premature `TransactionReceipt` lookup
that would otherwise fail.

## ⚠️ Breaking Changes
- None. `Config.BridgeAddressResolver` and the new `bridgetracker`
activity/bridge-address ports are additive/opt-in.

## 📋 Config Updates
- 🧾 New optional `BridgeServiceFinder.BridgeAddress` map (networkID →
address override), default empty — consulted by the new `GET
/bridge-address[/{network_id}]` endpoint before falling back to the
rollup manager's on-chain `BridgeAddress()`. That endpoint itself is
gated by `Config.BridgeAddressResolver` (Go-level wiring, not a TOML
key); unset leaves both routes unregistered.
- 🧾 New optional `Tracker.ActivityIdleTimeout` (defaults to
`IdleTimeout` if unset) — idle timeout for the activity endpoint's own
cache.
- 🧾 New optional `Tracker.L2GlobalExitRootAddress` (networkID →
`GlobalExitRootManagerL2` address map, default empty) and
`Tracker.L2InjectionLookbackBlocks` (default `1000`) — workaround-only
fallback for a destination network whose bridge-service instance
predates L2 injection block reporting (#1819); defaults leave behavior
unchanged for everyone else.

  ```toml
  [BridgeServiceFinder.BridgeAddress]
# optional per-network override, consulted in this priority order by
Finder.BridgeAddress:
# BridgeAddress[networkID], then BridgeAddress[0] (default for every
other network), then the
  # rollup manager's own on-chain BridgeAddress()
  0 = "0xShared_L1_bridge_address_used_as_the_default"
  5 = "0xNetwork5s_own_bridge_address"

  [Tracker]
  ActivityIdleTimeout = "30m"
  L2InjectionLookbackBlocks = 1000

# Workaround only: uncomment for a destination network whose
bridge-service instance does not
  # report the L2 block a covering GER was injected at.
  # [Tracker.L2GlobalExitRootAddress]
  # 1 = "0x..."
  ```

## ✅ Testing
- 🤖 **Automatic**: `go build ./...` and `go test ./bridgetracker/...
./bridgeservicefinder/... ./autoclaim/... ./proxy/...` pass, including
new/updated regression tests for the activity endpoint
(`activity_test.go`, `sources/activity_test.go`, `cache_test.go`), the
claimed-step split (`resolve_steps_test.go`, `engine_test.go`), the new
bridge-address endpoint (`bridge_address_test.go`), the settlement GER
backwards-search fix (`settlement_test.go`), and the L2 injection
fallback/lookback cap (`sources_test.go`'s
`TestGERSourceInjectedGER_FallsBackToL2Scan` and
`TestFindL2InjectionBlockBackwards`, `proxy/config/config_test.go`).

## 🐞 Issues
- Fixes #1818.

## 🔗 Related PRs
- None.

## 📝 Notes
- This branch's history still contains commits for
`bridgeservicefinder`'s `IgnoreNetworkIDs` and the `l2gersync`
`eth_getLogs` chunk-cap fix (originally developed alongside this work) —
both landed directly on `develop` in the meantime (#1810, #1813), so
neither shows up in this PR's diff anymore despite still being present
in the branch's git history.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bridge bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: l2gersync: isGERRemovedFromL2 fails, error: failed to scan for GER removal events from block n: query exceeds max block range 100000

2 participants