fix: aggkit-proxy: bali integration: bridge tracker activity endpoint, GER settlement/injection fixes - #1815
Conversation
There was a problem hiding this comment.
💡 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".
…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.
…r ~30min" This reverts commit bf18a77.
…d of wall-clock" This reverts commit e01e556.
…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>
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>
…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.
A certificate can flip to Settled in the agglayer before its settlement tx is actually visible on L1, letting StepCertificatePending resolve early. - CertificateSource now resolves the settlement tx's block number/timestamp on L1 (settlementBlockInfo) and exposes them as CertificateData.BlockNumber/ BlockTimestamp; both stay nil while the receipt is not mined/visible yet. - CertificatePendingResolver only treats the step as done once the certificate is settled AND BlockNumber is known, otherwise it keeps returning ErrCertificateNotSettled. - SettlementSource now also surfaces SettlementBlockTimestamp/ GERBlockTimestamp on L1SettledGERResult. - Wire the new EthClientResolver dependency into NewCertificateSource (proxy/cmd/run.go). - Update docs/bridgetracker/API.md and tests accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ly fallback Document that Tracker.L2GlobalExitRootAddrs should only be set for a destination network whose bridge-service instance does not report the L2 block a covering GER was injected at. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…activity endpoint
Previously ActivitySource.BridgesFrom aborted the whole GET /activity/from/{address}
scan on the first network it could not reach, even though every other network's
bridges were still available. Now it skips the failing network, logs it, and the
endpoint reports it in a new "warnings" field instead — bridges found on every
other network are still returned.
- domain: new ActivityWarning type; ActivityBridgeScanner.BridgesFrom and
ActivityQuerier.GetActivity now also return []ActivityWarning
- sources: ActivitySource.BridgesFrom continues past a failing network instead of
returning early; gained a logger
- api: ActivityResponse gains an omitempty "warnings" field ([]ActivityWarningItem)
- docs/swagger regenerated for the new field
…back and lookback cap InjectedGERResult now splits into L1InfoTreeLeaf (the L1 UpdateL1InfoTree event that produced the leaf) and the new, 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. - bridgeservice.L1InfoTreeLeafResponse carries the new injected_l2_block_num/injected_l2_block_timestamp fields when the destination's bridge-service instance reports them. - When it doesn't (an older instance), GERSource falls back to scanning the destination network's own GlobalExitRootManagerL2 contract for the UpdateHashChainValue event, backwards in chunks, via the new Tracker.L2GlobalExitRootAddress per-network contract address map. - That backward scan is now bounded by the new Tracker.L2InjectionLookbackBlocks (default 1,000 blocks) instead of always walking back to genesis, so a network where the fallback never finds the event doesn't cost an unbounded eth_getLogs scan on every lookup. - Document both new Tracker config fields (docs/bridgetracker.md, proxy/config/default.go) and the new InjectedGERResult response shape (docs/bridgetracker/API.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1fd1119 to
23e68c8
Compare
|
@claude review |
|
Claude finished @joanestebanr's task in 4m 1s —— View job ✅ Code Review CompleteReviewed PR #1815: Bali integration fixes and activity endpoint 🚀 What's NewThis PR introduces a comprehensive activity tracking endpoint ( 🐛 Bug Fixes
📋 Config Updates
|
…k range findEventUpdateL1InfoTreeBackwards computed fromBlockChunk as toBlock - l1InfoTreeBackwardsSearchChunkSize, which spans chunkSize+1 blocks since FromBlock/ToBlock are both inclusive (e.g. [15000,25000] for a 10_000 chunk). RPC providers enforcing a strict 10_000-block eth_getLogs limit rejected every such query, leaving any settlement without its own UpdateL1InfoTree event stuck in error state. Add the missing +1 (and widen the guard to toBlock >= chunkSize accordingly) so each chunk is exactly l1InfoTreeBackwardsSearchChunkSize blocks. Update the settlement_test.go helper/paginated test that mirrored the same calculation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Fixed the off-by-one in |
…nt log findEventUpdateL1InfoTreeBackwards picked the last UpdateL1InfoTree log in its query range as "most recent", even when that log came from a later transaction in the settlement's own block. If another tx in that block emits UpdateL1InfoTree after the settlement's own log index, its GER did not exist yet when the settlement executed, so the tracker was associating the certificate with the wrong GER/leaf index. Thread settlementLogIndex through to findEventUpdateL1InfoTreeBackwards and filter out any log sharing fromBlock with the settlement at or after that index before picking the latest one — mirroring the position filtering GERSource.FindFirstL1InfoTreeAfterBlock already does in ger.go, just looking backwards instead of forwards. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Fixed the same-block GER exclusion issue (also flagged by Codex as discussion_r3872865292) in ce78dab — |
…indow Addresses review feedback (item 6, issuecomment-5506110812) on the StepWaitingClaim/StepClaimed split: nothing pinned the tick where the on-chain isClaimed() check goes true but the destination bridge service has not indexed the claim tx yet, so StepWaitingClaim completes while StepClaimed stays its own current step (InProgress, no result) instead of being auto-completed alongside it. - domain/resolve_steps_test.go: new TestResolveStepsClaimedNotIndexedYet, exercising ResolveSteps directly against the fakeFacts port. - engine_test.go: TestEngineLifecycleL2ToL2 now ticks through that window (claimed=true, claim=nil) before the bridge service indexes the claim tx on the following tick, asserting both steps' status at each point. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed item 6 (edge case test coverage for the claim step split) in a603660: added |
🔄 Changes Summary
feat(bridgetracker): newGET /tracker/v1/activity/from/{from_address}endpoint that scans every bridge service known to thebridgeservicefinder(via newFinder.NetworkIDs()) for bridges sent by an address and resolves each one's claim state (ActivityCache,ActivitySource,ActivityQuerier/ActivityBridgeScanner/ActivityClaimCheckerports). Opt-in viaincludeTracking=trueto 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):StepClaimedis now its own tracked step with a dedicatedClaimedResolverthat fetches the claim tx/block from the destination network's bridge service, decoupled fromStepWaitingClaim, which now checksisClaimed()on-chain directly through a newClaimCheckerport 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 newClaimedstep result is for). The on-chain binding/cache logic is factored out ofActivitySourceintosources/claim_checker.go(contractClaimCheckers) and shared by both the tracker engine and the activity endpoint.fix(bridgetracker): introducedomain.ScannedBridgeto track which network's bridge service actually reported a scanned bridge (NetworkID), distinct fromBridge.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 wrongsourceBridgeNetworkintoisClaimed()and the wrong network intoTrackingID/the activity endpoint'sbridge_network_idfor such bridges.feat(bridgetracker): newGET /bridge-address[/{network_id}]endpoint, resolving the bridge contract address for one network or every network currently known — opt-in viaConfig.BridgeAddressResolver, wired inproxy/cmd/run.gooffbridgeservicefinder.Finderdirectly. Backed by a newConfig.BridgeAddressoverride map onbridgeservicefinder.Finderitself (priority: per-network entry, then[0]as the default, then the rollup manager's own on-chainBridgeAddress()).feat(bridgetracker):ClaimResultnow also carriesBlockTimestampalongsideBlockNumber.InjectedGERResultis restructured intoL1InfoTreeLeaf(the L1UpdateL1InfoTree/UpdateL1InfoTreeV2event that produced the covering leaf) and an optionalL2InjectedGER(the actual L2 block/timestamp the GER was injected at on the destination network) — fixing bug: proxy: step WaitingGERInjection as result have the L1 block #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_timestamponGET /bridge/v1/injected-l1-info-leaf),GERSourcefalls back to scanning that network's ownGlobalExitRootManagerL2contract for theUpdateHashChainValueevent backwards in chunks, via the newTracker.L2GlobalExitRootAddressper-network contract address map (a workaround-only fallback, see its doc) — bounded by a newTracker.L2InjectionLookbackBlocks(default 1,000 blocks) instead of always walking back to genesis.fix(bridgetracker): resolve the settled GER correctly when the settlement tx has noUpdateL1InfoTreeevent: 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 queryingeth_getLogsin 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 reportingSettled), avoiding a prematureTransactionReceiptlookup that would otherwise fail.Config.BridgeAddressResolverand the newbridgetrackeractivity/bridge-address ports are additive/opt-in.📋 Config Updates
🧾 New optional
BridgeServiceFinder.BridgeAddressmap (networkID → address override), default empty — consulted by the newGET /bridge-address[/{network_id}]endpoint before falling back to the rollup manager's on-chainBridgeAddress(). That endpoint itself is gated byConfig.BridgeAddressResolver(Go-level wiring, not a TOML key); unset leaves both routes unregistered.🧾 New optional
Tracker.ActivityIdleTimeout(defaults toIdleTimeoutif unset) — idle timeout for the activity endpoint's own cache.🧾 New optional
Tracker.L2GlobalExitRootAddress(networkID →GlobalExitRootManagerL2address map, default empty) andTracker.L2InjectionLookbackBlocks(default1000) — workaround-only fallback for a destination network whose bridge-service instance predates L2 injection block reporting (fix(bridgeservice): expose real L2 injection block/timestamp on injected-l1-info-leaf #1819); defaults leave behavior unchanged for everyone else.✅ Testing
go build ./...andgo 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'sTestGERSourceInjectedGER_FallsBackToL2ScanandTestFindL2InjectionBlockBackwards,proxy/config/config_test.go).🐞 Issues
🔗 Related PRs
📝 Notes
bridgeservicefinder'sIgnoreNetworkIDsand thel2gersynceth_getLogschunk-cap fix (originally developed alongside this work) — both landed directly ondevelopin the meantime (feat: bridgeservicefinder: add IgnoreNetworkIDs to skip dead networks 1809 #1810, fix(l2gersync): adaptisGERRemovedFromL2scan to RPC eth_getLogs block-range cap #1813), so neither shows up in this PR's diff anymore despite still being present in the branch's git history.