Skip to content

fix: aggkit-proxy: bali integration: bridge tracker activity endpoint, GER settlement/removal fixes, dead-network handling - #1815

Open
joanestebanr wants to merge 14 commits into
developfrom
feat/fix_bali_integration
Open

fix: aggkit-proxy: bali integration: bridge tracker activity endpoint, GER settlement/removal fixes, dead-network handling#1815
joanestebanr wants to merge 14 commits into
developfrom
feat/fix_bali_integration

Conversation

@joanestebanr

@joanestebanr joanestebanr commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

🔄 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.
  • feat(bridgetracker): ClaimResult and InjectedGERResult now also carry BlockTimestamp alongside BlockNumber.
  • fix(l2gersync): isGERRemovedFromL2's open-ended removal scan now recovers from RPC providers that cap eth_getLogs block ranges ("query exceeds max block range ..."), falling back to a chunked scan and caching the learned cap so retries don't repeat the doomed unbounded call (and its ERROR log) forever.
  • fix(bridgetracker): resolve the settled GER correctly when the settlement tx has no UpdateL1InfoTree event.
  • feat(bridgeservicefinder): IgnoreNetworkIDs config to skip known-dead networks entirely during on-chain enumeration and the live listener, exempting them from the startup health probe; default config updated in config/default.go and proxy/config/default.go.
  • e2e op-pessimistic-proof genesis-timestamp/sequencer-drift changes were tried and then reverted in this branch — no net effect.

⚠️ Breaking Changes

  • None. BridgeServiceFinder.IgnoreNetworkIDs, Config.BridgeAddressResolver and the new bridgetracker activity/bridge-address ports are additive/opt-in.

📋 Config Updates

  • 🧾 New optional IgnoreNetworkIDs = [] under [BridgeServiceFinder] / [AutoClaim.BridgeServiceFinder] (defaults to empty, no behavior change unless set).
  • 🧾 New optional Config.BridgeAddressResolver (Go-level wiring, not a TOML key) gating the GET /bridge-address[/{network_id}] endpoint; unset leaves both routes unregistered.

✅ Testing

  • 🤖 Automatic: go build ./... and go test ./bridgetracker/... ./bridgeservicefinder/... ./l2gersync/... ./autoclaim/... pass, including new/updated regression tests for the chunked GER-removal scan (evm_downloader_sovereign_test.go), the activity endpoint (activity_test.go, sources/activity_test.go, cache_test.go), the claimed-step split (resolve_steps_test.go, engine_test.go), and the new bridge-address endpoint (bridge_address_test.go).

🐞 Issues

  • No associated issue.

🔗 Related PRs

  • None.

📝 Notes

  • This branch also carries prior commits already on top of develop (bridgeservicefinder IgnoreNetworkIDs, bridgetracker GER settlement fix) that had not yet been merged.

joanestebanr and others added 11 commits August 21, 2026 10:49
Config.IgnoreNetworkIDs lists networkIDs (rollupIDs) that are excluded
entirely from on-chain resolution: buildInitialCache skips them during
enumeration (no RollupIDToRollupData call, no contract reads, no health
probe) and the live listener skips them when a rollup-manager lifecycle
event announces them (CreateNewRollup/CreateNewAggchain/AddExistingRollup).

This avoids known-dead networks (decommissioned or unreachable test
rollups) slowing down proxy startup and event processing with on-chain
reads and health-check timeouts that can never succeed. A static
Config.BridgeURLs override for a networkID is still served even if that
networkID is also listed in IgnoreNetworkIDs.

Fixes #1809
Documents the new bridgeservicefinder.Config.IgnoreNetworkIDs field in the
proxy binary's default TOML template. No other doc location lists
BridgeServiceFinder fields individually for the proxy binary (only
docs/autoclaim.md has a per-field table, already updated for AutoClaim).
…alth probe

probeAll iterated every cache entry unconditionally, including one installed
by the config-seeding step for a networkID that is both config-overridden
and listed in IgnoreNetworkIDs. That defeated the point of ignoring a
known-dead network: its /health probe still incurred the configured
timeout, and under RequireAllHealthyOnStart=true an unreachable ignored
override could still fail Start with ErrServicesUnhealthyOnStart.

probeAll now skips any networkID in Config.IgnoreNetworkIDs; its entry is
still served by GetURL, with healthy defaulting to false (never probed).

Found by review on #1810.
Add the missing BridgeServiceFinder fields (BlockFinality,
BlockChunkSize, HealthCheckPath, HealthCheckTimeout,
RequireAllHealthyOnStart, IgnoreNetworkIDs) and the empty
BridgeURLs/RPCURLs map sections to config/default.go, matching the
values already used in proxy/config/default.go and the Default*
constants defined in bridgeservicefinder/config.go.

Also add the missing BridgeURLs/RPCURLs sections to
proxy/config/default.go for the same reason.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…l-clock

op-pp's L1 image (arnaubennassar/geth:op-pp) has its chain data baked in at
build time and never advances past that snapshot. The op-geth entrypoint was
patching the L2 genesis timestamp to date +%s (real wall-clock) on every
start, so the gap between the L2 genesis and its L1 origin block grows by one
day for every day that passes since the L1 image was built. Once that drift
exceeded rollup.json's max_sequencer_drift (600s), op-node's sequencer could
never find a valid L1 origin for the first post-genesis block and the L2
chain stalled forever at block 0 -- surfacing as
"wait for MintableERC20 deployment: context deadline exceeded" during
LoadEnv, since op-pp's L1 snapshot is from Feb 2026 (~6 months of drift by
now).

Fix: read L1's actual head timestamp and use it to patch the L2 genesis
instead of wall-clock time, keeping L2 genesis anchored to L1's frozen origin
regardless of what day the test actually runs.

Verified locally: op-geth-001/op-node-001 went from stuck at block 0 to
actively sequencing new L2 blocks.

Note: since L1 never advances, the chain still stalls again once L2's
virtual time drifts past max_sequencer_drift from the anchored origin
(~1800s of L2 time in local testing) -- well past LoadEnv/MintableERC20
deployment, but a longer-running test could still hit it. Left as a known
follow-up rather than widening scope here.
L1's chain data is baked into its image at build time and never advances
past block 384. Anchoring L2 genesis to L1's head (previous commit) fixes
LoadEnv, but once the sequencer has produced ~600s (max_sequencer_drift)
worth of L2 blocks since genesis, op-node's origin-selector needs a newer
L1 origin than block 384 to keep going and never finds one, stalling the
chain forever mid-test-run.

Raise max_sequencer_drift to a week so the sequencer never needs to look
for a newer L1 origin within the lifetime of a test run.

Found while investigating CI failures on #1810.
…ateL1InfoTree event

Fixes #1811.

A cert's settlement tx on L1 doesn't always emit UpdateL1InfoTree itself — when
the settlement doesn't move the GER, it just propagates whatever GER an earlier
update already established. StepWaitL1SettledGER treated the missing event as
"not ready yet" and stalled forever instead of recognizing this case.

SettlementSource now:
- Fails fast (domain.ErrBadSettlementTx, permanent) when the receipt is missing
  VerifyBatchesTrustedAggregator, instead of silently returning "not ready".
- When UpdateL1InfoTree is missing, walks L1 backwards in bounded chunks
  (findEventUpdateL1InfoTreeBackwards) to find the closest earlier
  UpdateL1InfoTree event and uses its GER.
- Requires the L1 GlobalExitRoot contract address (NewSettlementSource) to
  scope that backwards search.

resolve_steps.UpdateStep now distinguishes permanent step failures
(IsPermanent) from transient ones: a permanent error marks the step
StepErrorPermanent immediately instead of accumulating a retry history that
will never be retried.

L1SettledGERResult now carries where each piece of evidence was found
(SettlementBlockNumber/SettlementLogIndex, GERBlockNumber/GERLogIndex)
instead of a single BlockNumber, since the GER-producing event can now live
in a different block than the settlement tx itself.

Also logs the set of resolved network entries once bridgeservicefinder
finishes building its initial cache, to aid diagnosing network-resolution
issues like the one reported in bali.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
isGERRemovedFromL2 scans for the (S-log) removal event from the insert
block, which can be arbitrarily far behind the head, up to "latest"
(open-ended eth_getLogs). Some RPC providers reject that open-ended
query once the insert block is more than their max-block-range cap
behind the head, with e.g. "query exceeds max block range 100000".
Before this fix, that error was only logged and treated as
"not removed" forever, so a stale insert could never be unstuck once
the chain advanced past the provider's cap.

scanRemovedGERs now detects that specific error via
aggkitcommon.ParseMaxRangeFromError, resolves the current head, and
retries the scan chunked via aggkitcommon.ChunkedRangeQuery - the same
pattern already used by L2EVMGERReader.GetRemovedGERsForRange and
AgglayerBridgeL2Reader's unset-claims fallback chunking. The learned
range cap is cached on the downloader (removalScanMaxRange) so that,
once learned, subsequent retries (this runs on every appender retry
while a GER stays unresolved) go straight to the chunked path instead
of repeating the doomed unbounded call - and its ERROR log - forever.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a new activity endpoint to the bridge tracker that answers "what
bridges has this address sent, and what is their claim state", across
every bridge service the bridgeservicefinder currently knows about
rather than one network at a time:

- bridgeservicefinder.Finder gains NetworkIDs(), enumerating every
  network currently resolved (i.e. every network GetURL would presently
  succeed for), backed by a new cache.networkIDs() read.
- bridgetracker/domain/activity.go defines the ActivityEntry model and
  the driven ports (ActivityBridgeScanner, ActivityClaimChecker,
  ActivityQuerier) the endpoint depends on; bridgetracker/activity.go
  implements ActivityCache, composing a scan across networks with claim
  resolution and (optionally, via includeTracking=true) registering
  still-unclaimed bridges with the tracker.
- bridgetracker/sources/activity.go implements ActivitySource, the
  adapter over the per-network bridge-service/JSON-RPC clients used
  elsewhere in the tracker.
- bridgetracker/api/activity_command.go + api.go wire
  GET /tracker/v1/activity/from/{from_address}; the route is only
  registered when both Config.ActivityScanner and Config.ActivityClaims
  are set, so the endpoint is entirely opt-in.
- proxy/cmd/run.go wires the new sources.ActivitySource into the
  tracker config using the existing finder/rpcClients/BridgeAddrs.
- bridgetracker/types/claim_status.go adds the claim-status vocabulary
  shared between the activity endpoint and its sources.
- Regenerated swagger docs (bridgetracker/api/docs,
  docs/assets/swagger/bridge_tracker) for the new route.
- Mocks for the new ports generated under bridgetracker/mocks; unrelated
  autoclaim call sites updated for the new
  bridgeservicefinder.Finder.NetworkIDs() method on the interface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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: 3712ed8ae8

ℹ️ 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/sources/activity.go Outdated
Comment on lines +89 to +93
items, err := fetchAllBridgesFrom(ctx, svc, networkID, addr, activityPageSize)
if err != nil {
return nil, fmt.Errorf("fetching bridges from %s on network %d: %w", fromAddress, networkID, err)
}
all = append(all, items...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the bridge transaction's source network

Preserve networkID with each scanned bridge instead of discarding it here. For a return bridge made on L2 with an L1-origin token, BridgeResponse.OriginNetwork is 0 while the transaction's source network encoded in GlobalIndex is the L2; downstream code consequently calls IsClaimed(..., 0), registers the transaction for tracking on L1, and reports the wrong bridge_network_id. Carry the scanned network ID through the activity entry or decode it from GlobalIndex.

Useful? React with 👍 / 👎.

Comment on lines +181 to +183
if toBlock > l1InfoTreeBackwardsSearchChunkSize {
fromBlockChunk = toBlock - l1InfoTreeBackwardsSearchChunkSize
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Limit each inclusive log query to 10,000 blocks

Because FromBlock and ToBlock are inclusive, subtracting 10,000 produces a 10,001-block query (for example, [15000,25000]). On providers enforcing the stated 10,000-block eth_getLogs limit, every backward lookup fails immediately, so settlements without an UpdateL1InfoTree in their own receipt remain in error instead of resolving. Compute the lower bound with toBlock - chunkSize + 1.

Useful? React with 👍 / 👎.

Comment on lines +196 to +199
if len(logs) > 0 {
// FilterLogs returns logs in ascending block/log-index order, so the last one is the
// most recent event in this chunk — the closest one at or before fromBlock
last := logs[len(logs)-1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude GER updates after the settlement log

When another transaction emits UpdateL1InfoTree later in the same block as a settlement whose own receipt has no such event, selecting the last log in the block returns a GER that did not exist when the settlement executed. The tracker then associates the certificate with the wrong GER and leaf index. Filter same-block logs to those before SettlementLogIndex (analogous to the position filtering already used by GERSource) before choosing the latest event.

Useful? React with 👍 / 👎.

@joanestebanr joanestebanr changed the title fix(bali): bridge tracker activity endpoint, GER settlement/removal fixes, dead-network handling fix: aggkit-proxy: bali integration: bridge tracker activity endpoint, GER settlement/removal fixes, dead-network handling Aug 27, 2026
joanestebanr and others added 3 commits August 27, 2026 18:40
…k IDs, auto bridge address, incremental cache

Builds on the GET /activity/from/{from_address} endpoint (3712ed8) with:

- filterBridges query param (all|claimed|pending|error, default all): lets
  a caller ask for only claimed, only pending, or only errored bridges.
  Requesting pending/error skips fetching a claimed bridge's claim record
  (it would be filtered out anyway) — the entry simply stays unsettled and
  is fetched normally once a filter that needs it is used.
- claimed becomes a tri-state string ("false"/"true"/"error") instead of a
  bool, via types.ClaimStatus, so a failed isClaimed() check (e.g. no
  bridge contract address configured) is never confused with "not
  claimed"; the failure message is reported under errors["claim"].
- bridge_network_id / claim_network_id sit alongside the raw bridge/claim
  payloads (kept byte-for-byte as the bridge service returned them)
  instead of wrapping them, so callers know which bridge service produced
  each one without altering the response shape.
- bridgeservicefinder.Finder gains BridgeAddress(ctx, networkID): defaults
  to the rollup manager's own on-chain BridgeAddress() (an immutable
  constructor parameter, resolved once and cached forever), overridable
  per network via the new BridgeServiceFinder.BridgeAddress config map —
  and a BridgeAddress[0] override doubles as the default for every network
  without its own entry. ActivitySource now resolves destination bridge
  contracts through this instead of a manually maintained address map.
- ActivityCache no longer re-scans every page of every network on every
  call: ActivityBridgeScanner.BridgesFrom takes the caller's already-known
  global indexes and each network's scan stops at the first already-known
  bridge, relying on the bridge service's own newest-first order. Once a
  bridge is confirmed claimed, isClaimed() is never asked again for it
  (only its claim record may still need fetching); once a claim record is
  fetched, it is cached for good. A from_address idle for
  Config.ActivityIdleTimeout (default 30m, mirroring IdleTimeout) is
  forgotten entirely on the next request, freeing everything cached for
  it — swept lazily on access rather than a dedicated ticker/goroutine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… endpoint, document it

- ActivityEntry gains CreatedAt/UpdatedAt: CreatedAt is stamped once (or
  carried forward from the previous cache entry) and never changes;
  UpdatedAt is stamped on every refresh, so it freezes once a bridge
  settles (claimed with its claim record fetched) since it is never
  refreshed again from that point on.
- ActivityItem exposes them as creation_timestamp/last_updated_timestamp
  (unix seconds, matching the rest of the API's timestamp fields).
- docs/bridgetracker/API.md: documents the whole activity endpoint end to
  end (it had none before) — request params (includeTracking,
  filterBridges), response shape (ActivityResponse/ActivityItem), the
  pass-through BridgeResponse/ClaimResponse shapes, an example, and the
  caching/eviction behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…block timestamps

- StepClaimed now gets its own resolver (ClaimedResolver) fetching the claim tx/block
  from the destination bridge service, decoupled from StepWaitingClaim, which now checks
  isClaimed() on-chain directly via the new ClaimChecker port instead of waiting on the
  claim record.
- Factor the on-chain isClaimed() binding/cache logic out of ActivitySource into
  sources/claim_checker.go (contractClaimCheckers), shared by both the tracker engine's
  ClaimChecker and the activity endpoint's ActivitySource.
- 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) — they diverge for a re-bridged asset across more than one hop, which
  was feeding the wrong sourceBridgeNetwork into isClaimed() and the wrong network into
  TrackingID for such bridges.
- Add GET /bridge-address[/{network_id}], 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).
- ClaimResult and InjectedGERResult now also carry BlockTimestamp alongside BlockNumber.
- Regenerate swagger docs and update API.md accordingly.
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