fix(l2gersync): adapt isGERRemovedFromL2 scan to RPC eth_getLogs block-range cap - #1813
Merged
Merged
Conversation
isGERRemovedFromL2 scan to RPC eth_getLogs block-range cap
…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
force-pushed
the
fix/l2gersync-max-block-range-1812
branch
from
September 1, 2026 08:23
7f3e35d to
3583743
Compare
joanestebanr
enabled auto-merge (squash)
September 1, 2026 08:25
arnaubennassar
approved these changes
Sep 1, 2026
joanestebanr
disabled auto-merge
September 1, 2026 10:27
joanestebanr
enabled auto-merge (squash)
September 1, 2026 10:27
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔄 Changes Summary
isGERRemovedFromL2scanned for a GER'sUpdateRemovalHashChainValueremoval event from the insert block (which can be arbitrarily far behind the head) to "latest" in a single open-endedeth_getLogscall.query exceeds max block range Non every single appender retry, logging an ERROR forever and never actually recovering a genuinely-removed GER.scanRemovedGERsnow parses that error viaaggkitcommon.ParseMaxRangeFromErrorand retries chunked — no config parameter involved, the same adaptive pattern already used byL2EVMGERReader.GetRemovedGERsForRangeandAgglayerBridgeL2Reader.fetchUnsetClaimsWithFallbackChunking/getUnsetClaimsInChunks.removalScanMaxRangeso that, once learned, later retries skip straight to the chunked path instead of repeating the doomed unbounded call.fetchRemovedGERsChunkrecurses per-chunk (mirroring those siblings) so a chunk that is itself still too large keeps adapting instead of failing the whole scan outright.downloaderSovereign.scanRemovedGERs/fetchRemovedGERsChunk).📋 Config Updates
✅ Testing
TestDownloaderSovereign_IsGERRemovedFromL2_RecoversFromMaxBlockRangeError(reproduces the max-range error and verifies the chunked recovery combines results across chunks) andTestDownloaderSovereign_IsGERRemovedFromL2_CachesLearnedMaxRangeAcrossCalls(proves a second call skips the doomed unbounded call once the cap is learned). Fulll2gersyncsuite green,go vetclean,golangci-lint0 issues.bali-82-opbridge-service node whoseL1InfoTreeSyncwas still far behind head — the unbounded scan was failing every retry with this exact error; confirmed the chunked fallback resolves it.🐞 Issues
isGERRemovedFromL2fails, error:failed to scan for GER removal events from block n: query exceeds max block range 100000#1812🔗 Related PRs
isGERRemovedFromL2.📝 Notes