Bridge Tracker component
The bridge tracker gives a client a single endpoint to follow one bridge (identified by its
creating transaction) from the moment it is sent until it is claimed, instead of the client
polling the bridge service, the Global/Local Exit Root state and the agglayer certificate status
itself and stitching the result together. It is served by the aggkit-proxy binary (TRACKER
component), alongside the bridge service finder.
How it works
Registering a bridge (GET .../tx/{tx_hash}, or connecting over the WebSocket) adds it to an
in-memory supervised list. A background engine resolves each supervised bridge’s creating
transaction (FindBridge, over the origin network’s JSON-RPC endpoint) and then walks it through
its expected path, one milestone at a time, checking the fact behind the current step and
advancing once it is met:
| Step | Meaning |
|---|---|
WaitingGERUpdate | L1-originated bridge: the L1 Global Exit Root has not been updated with this deposit yet. |
WaitingLERUpdate | L2-originated bridge: the origin network’s Local Exit Root has not been updated yet. |
PendingInclusion | The bridge is not yet part of any certificate sent to the agglayer. |
CertificatePending | Included in a certificate; waiting for it to settle (covers Pending/Proven/Candidate/InError). |
WaitL1SettledGER | L2-originated only: the certificate settled, waiting for its settlement tx to confirm on L1. |
WaitingGERInjection | L1 → L2 and L2 → L2 only: waiting for the covering Global Exit Root’s injection tx to land on the destination network — an L2-side fact; skipped for L2 → L1, since mainnet needs no injection. |
WaitingL1InfoLeafAvailable | Always right before WaitingClaim, on every route: waiting for the bridge-service instance that will build the claim proof — the origin network’s own instance, or the destination’s when the origin is mainnet (which has no bridge-service deployment of its own) — to have its L1 info tree sync caught up to this deposit (GET /bridge/v1/l1-info-tree-index). Unlike WaitingGERInjection, this is never skipped or inferred from a sibling step: injecting a GER on the destination is not the same fact as the proof-building instance having caught up, and that sync can lag behind the finality this tracker uses elsewhere (see #1823). |
WaitingClaim | The bridge is claimable: the proof-building instance has the bridge’s L1 info tree index. |
Claimed | Terminal: the bridge has been claimed on the destination network. |
Which steps apply, and in which order, depends on the bridge’s direction:
- L1 → L2:
WaitingGERUpdate→WaitingGERInjection→WaitingL1InfoLeafAvailable→WaitingClaim→Claimed - L2 → L1:
WaitingLERUpdate→PendingInclusion→CertificatePending→WaitL1SettledGER→WaitingL1InfoLeafAvailable→WaitingClaim→Claimed - L2 → L2:
WaitingLERUpdate→PendingInclusion→CertificatePending→WaitL1SettledGER→WaitingGERInjection→WaitingL1InfoLeafAvailable→WaitingClaim→Claimed
The whole route is published the moment the creating tx resolves, so a client sees every step it will walk through before any milestone has been checked — not just the current one.
TrackingStatus summarizes the bridge’s lifecycle for a client that only needs the high-level
state: registered (added to the list, not resolved yet), running, error (a step, or the
initial resolution itself, failed terminally), or finished (claimed).
Endpoints
All routes are served under /tracker/v1.
| Method | Path | Description |
|---|---|---|
| GET | /tracker/v1/health | Health status, instance identity and build info. |
| GET | /tracker/v1/network/{network_id}/tx/{tx_hash} | Registers (or looks up) the bridge and returns its current TrackingData. |
| GET | /tracker/v1/network/{network_id}/tx/{tx_hash}/ws | Same bridge, pushed as a status WebSocket message on every change instead of polled. |
The response, both over REST and as each WebSocket status message, is a TrackingData: its
bridge_status field stays null until the tracker resolves the creating tx, and all_steps is
null until then too. bridge_status.event carries the facts taken directly from the on-chain
BridgeEvent log (origin/destination network and address, amount, leaf type); block_number,
log_index and block_timestamp sit alongside it as the block-level context the event was
found in, not the event’s own fields.
The WebSocket connection closes normally (code 1000) once the bridge reaches a terminal state —
Claimed, or the tracker giving up trying to resolve the creating tx at all (invalid tx / not a
bridge transaction). A step-level error on an otherwise-resolved bridge is reported in
TrackingData.error but is not terminal: the engine keeps retrying it.
Configuration
Enable the TRACKER component (--components TRACKER,...) and configure the [Tracker]
section:
[Tracker]
RetentionPeriod = "10m"
IdleTimeout = "30m"
RegisterResolveTimeout = "3s"
L1BlockFinality = "LatestBlock"
L2BlockFinality = "LatestBlock"
MaxTrackedBridges = 100000
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..."
[Tracker.AgglayerClient]
Cached = true
[Tracker.AgglayerClient.ConfigurationCache]
TTL = "1s"
Capacity = 100
SendCertificate = "forbidden"
GetCertificateHeader = "cached"
GetEpochConfiguration = "cached"
GetLatestPendingCertificateHeader = "cached"
GetNetworkInfo = "cached"
[Tracker.AgglayerClient.GRPC]
URL = "https://agglayer-dev.polygon.technology"
UseTLS = false
RetentionPeriod: how long a terminal bridge (finished, or failed to ever resolve) stays queryable before the tracker forgets it and a later request re-registers it from scratch.IdleTimeout: how long a bridge — terminal or still active — stays supervised once nobody has read it (REST poll) and it has no active WebSocket subscriber. UnlikeRetentionPeriod, this applies regardless of status: a bridge that never resolves and that nobody is watching would otherwise stay in memory forever.RegisterResolveTimeout: how long the first request for a freshly registered tx waits for the engine’s immediate resolution attempt before answering, so it has a shot at real progress instead of the bareregisteredstate; a lookup of an already-registered tx never waits.L1BlockFinality/L2BlockFinality: the finality a bridge’s creating tx receipt must reach before the tracker accepts it, so a later reorg cannot leave it permanently following an orphaned deposit (a resolved bridge is never re-checked).MaxTrackedBridges: caps the in-memory supervised list; a request beyond it fails instead of registering the bridge — reaching the cap never evicts an existing entry to make room, soRetentionPeriodandIdleTimeoutare what keep the registry under it during normal operation.L2GlobalExitRootAddress: workaround only — a networkID →GlobalExitRootManagerL2contract address map, used solely as a fallback for a destination network whose bridge-service instance does not report the L2 block a covering GER was actually injected at. For a network present here, the tracker scans that network’s own L2 for theUpdateHashChainValueevent instead of leaving it absent. A network absent from this map (the default, empty map) never gets this fallback attempted; it should not be set otherwise.L2InjectionLookbackBlocks: bounds how many blocks that same fallback scans backwards from the destination network’s head before giving up, instead of continuing all the way back to genesis. Defaults to 1,000 blocks when unset or<= 0.AgglayerClient: the client used to resolve an L2-originated bridge’s covering certificate and its status (PendingInclusion/CertificatePending/WaitL1SettledGER).Cachedis the master switch forConfigurationCache’s per-method policy (falseignores it entirely). Each method iscached(served from its own TTL cache),passthrough(always calls the agglayer directly, the default for a method left unset), orforbidden(refused without ever reaching the agglayer — the tracker only ever reads agglayer state, soSendCertificateis forbidden here).GetLatestSettledCertificateHeaderis intentionally left unset (passthrough): its “latest” answer must always be fresh.