Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

StepMeaning
WaitingGERUpdateL1-originated bridge: the L1 Global Exit Root has not been updated with this deposit yet.
WaitingLERUpdateL2-originated bridge: the origin network’s Local Exit Root has not been updated yet.
PendingInclusionThe bridge is not yet part of any certificate sent to the agglayer.
CertificatePendingIncluded in a certificate; waiting for it to settle (covers Pending/Proven/Candidate/InError).
WaitL1SettledGERL2-originated only: the certificate settled, waiting for its settlement tx to confirm on L1.
WaitingGERInjectionL1 → 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.
WaitingL1InfoLeafAvailableAlways 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).
WaitingClaimThe bridge is claimable: the proof-building instance has the bridge’s L1 info tree index.
ClaimedTerminal: the bridge has been claimed on the destination network.

Which steps apply, and in which order, depends on the bridge’s direction:

  • L1 → L2: WaitingGERUpdateWaitingGERInjectionWaitingL1InfoLeafAvailableWaitingClaimClaimed
  • L2 → L1: WaitingLERUpdatePendingInclusionCertificatePendingWaitL1SettledGERWaitingL1InfoLeafAvailableWaitingClaimClaimed
  • L2 → L2: WaitingLERUpdatePendingInclusionCertificatePendingWaitL1SettledGERWaitingGERInjectionWaitingL1InfoLeafAvailableWaitingClaimClaimed

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.

MethodPathDescription
GET/tracker/v1/healthHealth 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}/wsSame 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. Unlike RetentionPeriod, 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 bare registered state; 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, so RetentionPeriod and IdleTimeout are what keep the registry under it during normal operation.
  • L2GlobalExitRootAddress: workaround only — a networkID → GlobalExitRootManagerL2 contract 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 the UpdateHashChainValue event 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). Cached is the master switch for ConfigurationCache’s per-method policy (false ignores it entirely). Each method is cached (served from its own TTL cache), passthrough (always calls the agglayer directly, the default for a method left unset), or forbidden (refused without ever reaching the agglayer — the tracker only ever reads agglayer state, so SendCertificate is forbidden here). GetLatestSettledCertificateHeader is intentionally left unset (passthrough): its “latest” answer must always be fresh.

API Documentation