bridgetracker

package
v0.11.0-rc15 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: Apache-2.0, MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// DefaultActivityEnginePollInterval is the default period between activity refresh rounds
	DefaultActivityEnginePollInterval = 30 * time.Second
	// DefaultActivityEngineMaxConcurrentRefreshes is the default
	// ActivityEngineConfig.MaxConcurrentRefreshes: how many addresses tick refreshes at once
	DefaultActivityEngineMaxConcurrentRefreshes = 10
	// DefaultActivityEngineIdleTimeout is the default ActivityEngineConfig.IdleTimeout
	DefaultActivityEngineIdleTimeout = 30 * time.Minute
	// DefaultActivityEngineRefreshTimeout is the default ActivityEngineConfig.RefreshTimeout
	DefaultActivityEngineRefreshTimeout = 2 * time.Minute
)
View Source
const (
	// DefaultEnginePollInterval is the default period between engine resolution rounds
	DefaultEnginePollInterval = 10 * time.Second
	// DefaultEngineResolveTimeout is the default per-bridge budget for one resolution
	DefaultEngineResolveTimeout = 30 * time.Second
	// DefaultEngineUnresolvedTimeout is the default time a supervised tx is given to resolve
	// (FindBridge succeeding) before it is marked as terminally failed, regardless of why it
	// hasn't resolved (the tx may not be mined yet, or a source keeps failing transiently)
	DefaultEngineUnresolvedTimeout = 30 * time.Second
	// DefaultEngineRetentionPeriod is the default time a terminal bridge (Finished, or failed
	// to ever resolve) stays queryable before being forgotten. Clients polling or subscribed
	// observe the terminal TrackingStatus during this window; once forgotten, a new request
	// for the same tx re-registers it and tracking restarts from scratch — the retry path for
	// a tx the tracker gave up on
	DefaultEngineRetentionPeriod = 10 * time.Minute
	// DefaultEngineIdleTimeout is the default time a bridge — terminal or still active — is
	// kept once nobody has read it (no Get/GetAndAwait) and it has no active WebSocket
	// subscriber. It bounds the memory an abandoned tracker holds onto regardless of whether it
	// ever resolves, on top of RetentionPeriod's grace period for the ones that do. Set well
	// above PollInterval and a plausible client poll cadence so a caller polling at a normal
	// pace never sees its bridge evicted between two of its own requests
	DefaultEngineIdleTimeout = 30 * time.Minute
	// DefaultEngineMaxConcurrentResolutions is the default EngineConfig.MaxConcurrentResolutions:
	// how many active bridges tick resolves at once
	DefaultEngineMaxConcurrentResolutions = 50
)
View Source
const DefaultActivityMaxConcurrentRefreshes = DefaultActivityEngineMaxConcurrentRefreshes

DefaultActivityMaxConcurrentRefreshes is the default Config.ActivityMaxConcurrentRefreshes (see DefaultActivityEngineMaxConcurrentRefreshes for the semantics; both must stay in sync with the [Tracker] section of the proxy's default config)

View Source
const DefaultActivitySourceBridgeServiceMaxConcurrentNetworkScans = 10

DefaultActivitySourceBridgeServiceMaxConcurrentNetworkScans is the default Config.ActivitySourceBridgeService.MaxConcurrentNetworkScans: how many of the networks sources.ActivitySource.BridgesFrom knows about it scans at once.

View Source
const DefaultActivitySourceBridgeServicePageSize = uint32(100)

DefaultActivitySourceBridgeServicePageSize is the default Config.ActivitySourceBridgeService.PageSize: the page size used while paging through a network's own GET /bridge/v1/bridges scanning for a from_address (see sources.ActivitySource.BridgesFrom).

View Source
const DefaultL2InjectionLookbackBlocks = 10_000_000

DefaultL2InjectionLookbackBlocks is the default Config.L2InjectionLookbackBlocks (must stay in sync with the [Tracker] section of the proxy's default config)

View Source
const DefaultMaxActivityAddresses = 100_000

DefaultMaxActivityAddresses bounds how many distinct from_addresses ActivityCache accepts before refusing new ones (see ActivityCache.register), mirroring DefaultMaxTrackedBridges' DoS-protection reasoning for the tracker's own registry

View Source
const DefaultMaxConcurrentResolutions = DefaultEngineMaxConcurrentResolutions

DefaultMaxConcurrentResolutions is the default Config.MaxConcurrentResolutions (see DefaultEngineMaxConcurrentResolutions for the semantics; both must stay in sync with the [Tracker] section of the proxy's default config)

View Source
const DefaultMaxTrackedBridges = 100_000

DefaultMaxTrackedBridges is the default Config.MaxTrackedBridges: how many distinct bridges the in-memory registry accepts before refusing new ones (see registry.go's memoryRegistry).

View Source
const DefaultMetricsSampleInterval = 15 * time.Second

DefaultMetricsSampleInterval is how often the Prometheus gauges are refreshed

Variables

View Source
var DefaultActivityPollInterval = types.Duration{Duration: DefaultActivityEnginePollInterval}

DefaultActivityPollInterval is the default Config.ActivityPollInterval (see DefaultActivityEnginePollInterval for the semantics; both must stay in sync with the [Tracker] section of the proxy's default config)

View Source
var DefaultActivityRegisterResolveTimeout = types.Duration{Duration: defaultActivityRegisterResolveTimeoutDuration}

DefaultActivityRegisterResolveTimeout is the default Config.ActivityRegisterResolveTimeout (must stay in sync with the [Tracker] section of the proxy's default config)

View Source
var DefaultActivitySourceBridgeServiceTimeout = types.Duration{
	Duration: defaultActivitySourceBridgeServiceTimeoutDuration,
}

DefaultActivitySourceBridgeServiceTimeout is the default Config.ActivitySourceBridgeService.Timeout.

View Source
var DefaultActivitySourceRPCRangeFromBlock = aggkittypes.BlockNumberFinality{Block: aggkittypes.Latest, Offset: -90}

DefaultActivitySourceRPCRangeFromBlock is the default Config.ActivitySourceRPC.RangeFromBlock: how far back the RPC-based activity fallback (see sources.activityRPCScanner, agglayer/aggkit#1837) scans on each network, relative to that network's own latest block.

View Source
var DefaultActivitySourceRPCRangeToBlock = aggkittypes.LatestBlock

DefaultActivitySourceRPCRangeToBlock is the default Config.ActivitySourceRPC.RangeToBlock.

View Source
var DefaultIdleTimeout = types.Duration{Duration: DefaultEngineIdleTimeout}

DefaultIdleTimeout is the default Config.IdleTimeout (see DefaultEngineIdleTimeout for the semantics; both must stay in sync with the [Tracker] section of the proxy's default config)

View Source
var DefaultL1BlockFinality = aggkittypes.LatestBlock

DefaultL1BlockFinality is the default Config.L1BlockFinality: an L1 bridge's creating tx is only accepted once its receipt reaches this finality, so a reorg cannot leave the tracker permanently following an orphaned deposit/block

View Source
var DefaultL2BlockFinality = aggkittypes.LatestBlock

DefaultL2BlockFinality is the default Config.L2BlockFinality: an L2 bridge's creating tx is only accepted once its receipt reaches this finality, so a reorg cannot leave the tracker permanently following an orphaned deposit/block

View Source
var DefaultRegisterResolveTimeout = types.Duration{Duration: defaultRegisterResolveTimeoutDuration}

DefaultRegisterResolveTimeout is the default Config.RegisterResolveTimeout (must stay in sync with the [Tracker] section of the proxy's default config)

View Source
var DefaultRetentionPeriod = types.Duration{Duration: DefaultEngineRetentionPeriod}

DefaultRetentionPeriod is the default Config.RetentionPeriod (see DefaultEngineRetentionPeriod for the semantics; both must stay in sync with the [Tracker] section of the proxy's default config)

View Source
var ErrActivityRegistryFull = domain.ErrActivityRegistryFull

ErrActivityRegistryFull is returned by ActivitySupervisedStore.RegisterAndAwait when registering a new from_address would exceed the store's configured capacity

View Source
var ErrBridgeTxNotABridge = domain.ErrBridgeTxNotABridge

ErrBridgeTxNotABridge is returned by BridgeEventSource.FindBridge when the transaction exists but is definitely not a bridge transaction (reverted, or mined without emitting a BridgeEvent log): unlike ErrBridgeTxNotFound, retrying cannot change this, so it is wrapped as domain.Permanent — the tracker marks the bridge as terminally failed immediately, with no retries

View Source
var ErrBridgeTxNotFound = domain.ErrBridgeTxNotFound

ErrBridgeTxNotFound is returned by BridgeEventSource.FindBridge when the transaction does not exist on the network yet. domain.ResolveBridgeTx keeps retrying for up to EngineConfig. UnresolvedTimeout (the tx may simply not be mined yet) before marking the bridge as terminally failed

View Source
var ErrSourceUnavailable = domain.ErrSourceUnavailable

ErrSourceUnavailable is returned by BridgeEventSource.FindBridge when the bridge's origin network has no source configured to resolve it (e.g. no JSON-RPC client for a statically-configured network): like ErrBridgeTxNotABridge, this is a permanent condition that retrying cannot change, so it is wrapped as domain.Permanent too

Functions

func BlockTimeOrNow

func BlockTimeOrNow(ts uint64, now time.Time) time.Time

BlockTimeOrNow converts ts, a block's unix-second timestamp, to a UTC time.Time, or returns now when ts is zero — the sentinel BridgeResponse.BlockTimestamp effectively uses for "not resolved yet" (a real on-chain block at exactly the unix epoch does not happen in practice). Exported so bridgetracker/db.sqliteActivityStore.refresh, a deliberate byte-for-byte mirror of ActivityCache.refresh below (see its own doc), stamps ActivityEntry.CreatedAt the same way

func Dependencies

func Dependencies() []proxytypes.Component

Types

type ActivityBridgeScanner

type ActivityBridgeScanner = domain.ActivityBridgeScanner

ActivityBridgeScanner is the driven port to the raw bridge-service data behind the activity endpoint: every bridge sent by a given address, across every configured bridge service

type ActivityCache

type ActivityCache struct {
	// contains filtered or unexported fields
}

ActivityCache implements domain.ActivityRegistry: for a given from_address it scans every configured bridge service (via ActivityBridgeScanner) for bridges it has not already cached, and keeps a running per-address cache of the resulting bridges, so:

  • a bridge already known is never re-scanned from the bridge service again (see ActivityBridgeScanner.BridgesFrom's known parameter);
  • once a bridge is confirmed claimed, isClaimed() is never asked again for it, even if its claim record has not been fetched yet;
  • once a bridge's claim record has been fetched, it is never asked for again;
  • an address nobody has asked about in idleTimeout is forgotten entirely, freeing everything cached for it (see PruneIdle, mirroring SupervisedStore.PruneIdle, driven by ActivityEngine's poll tick instead of a sweep-on-every-request).

None of the scanning/rechecking above happens inline inside GetActivity: it happens in RefreshAddress, called only by ActivityEngine's poll tick or its trigger fast-path (see RegisterAndAwait). GetActivity itself is a cache-only read.

Safe for concurrent use.

func NewActivityCache

func NewActivityCache(
	scanner ActivityBridgeScanner, claims ActivityClaimChecker, supervised SupervisedStore,
	logger aggkitcommon.Logger,
) *ActivityCache

NewActivityCache returns an ActivityCache resolving bridges through scanner, claim state through claims, and (when asked) tracker registration through supervised. Idle eviction is entirely driven by whoever wires an ActivityEngine over the returned cache (see ActivityEngineConfig.IdleTimeout, PruneIdle) — this constructor has no idleTimeout of its own to fall back to

func (*ActivityCache) FlushActivity

func (a *ActivityCache) FlushActivity(fromAddress common.Address)

FlushActivity implements domain.ActivityQuerier: it discards fromAddress's whole per-address cache, if any — every cached bridge (settled or not) is forgotten, so the next background refresh for fromAddress rescans and rechecks everything from scratch, exactly as if it had never been requested before. Safe to call for an address with nothing cached (no-op)

func (*ActivityCache) GetActiveAddresses

func (a *ActivityCache) GetActiveAddresses() ([]common.Address, error)

GetActiveAddresses implements domain.ActivitySupervisedStore: every currently supervised from_address, for ActivityEngine's poll tick to iterate

func (*ActivityCache) GetActivity

func (a *ActivityCache) GetActivity(
	_ context.Context, fromAddress common.Address, includeTracking bool, filter types.ActivityFilter,
) ([]*domain.ActivityEntry, []domain.ActivityWarning, error)

GetActivity implements domain.ActivityQuerier: a cache-only read of whatever the last background refresh (see RefreshAddress) computed for fromAddress, filtered per filter. includeTracking additionally marks fromAddress as wanting tracking enrichment for future refreshes (see activityAddrCache.wantsTracking) — it does not itself fetch anything. An address with no cache yet (never registered, or registered but not refreshed even once) returns an empty result, not an error

func (*ActivityCache) PruneIdle

func (a *ActivityCache) PruneIdle(olderThan time.Time) (int, error)

PruneIdle implements domain.ActivitySupervisedStore: it forgets every address with no in-flight RegisterAndAwait call that was last accessed before olderThan, returning how many were forgotten. Mirrors memoryRegistry.PruneIdle

func (*ActivityCache) RefreshAddress

func (a *ActivityCache) RefreshAddress(ctx context.Context, fromAddress common.Address) error

RefreshAddress implements domain.ActivitySupervisedStore: it rechecks every bridge already cached for fromAddress that is not yet settled (see settled), scans for bridges not seen before (see ActivityBridgeScanner.BridgesFrom), forgets whatever the scan reports as invalidated, records the scan's warnings, and finally wakes every RegisterAndAwait call currently blocked on fromAddress. This is what used to run inline inside GetActivity; it is now only ever called by ActivityEngine. A missing cache (address not currently registered) is a silent no-op — the regular tick already only iterates GetActiveAddresses

func (*ActivityCache) RegisterAndAwait

func (a *ActivityCache) RegisterAndAwait(
	fromAddress common.Address, includeTracking bool, timeout time.Duration,
) (bool, error)

RegisterAndAwait implements domain.ActivitySupervisedStore. On an already-registered address it behaves like a plain touch: no trigger, no wait, ready reports its current refreshed state. On a newly registered address it wakes ActivityEngine (see signalTrigger/Triggers) and waits up to timeout for that first refresh to complete, reporting whether it actually did (ready) or timeout elapsed first with nothing to show yet. includeTracking, when true, sets the sticky wantsTracking flag immediately — not just on a later GetActivity call — so even the very first triggered refresh (the one a caller with timeout > 0 blocks on) already enriches tracking, instead of the flag only taking effect on the refresh after that

func (*ActivityCache) Triggers

func (a *ActivityCache) Triggers() <-chan common.Address

Triggers implements domain.ActivityTriggerable

type ActivityClaimChecker

type ActivityClaimChecker = domain.ActivityClaimChecker

ActivityClaimChecker is the driven port to a bridge's claim state on its destination network

type ActivityEngine

type ActivityEngine struct {
	// contains filtered or unexported fields
}

ActivityEngine is the background counterpart of the activity endpoint: it watches the supervised from_addresses (see domain.ActivitySupervisedStore) and periodically refreshes each one's cache through the store itself (which owns the scanner/claims/tracker driven ports) — mirroring Engine's role for tracked tx IDs, but over from_addresses instead of (network, tx hash) pairs and with no per-step resolution: one refresh is one full scan

func NewActivityEngine

func NewActivityEngine(
	cfg ActivityEngineConfig, logger aggkitcommon.Logger, store domain.ActivitySupervisedStore,
) (*ActivityEngine, error)

NewActivityEngine returns an activity engine over the given store

func (*ActivityEngine) Start

func (e *ActivityEngine) Start(ctx context.Context)

Start launches the refresh loop; it stops when ctx is cancelled. Besides the regular poll cadence, it also watches the store's ActivityTriggerable channel (if implemented) to refresh a freshly registered address right away instead of leaving it for the next tick — see refreshOne and domain.ActivitySupervisedStore.RegisterAndAwait, which is what a caller actually waits on.

Both ticks and triggered refreshes run in their own goroutine (see startTick, refreshOne) rather than inline in this select loop: tick alone can take far longer than one PollInterval once enough addresses are supervised (each refresh is a full multi-network scan), and running it inline here would stop this loop from reading triggers for that whole time — starving every freshly registered address's fast path behind whatever tick happened to be running, so RegisterAndAwait always times out and Execute always 503s for as long as that lasts

type ActivityEngineConfig

type ActivityEngineConfig struct {
	// PollInterval is the period between refresh rounds over the supervised addresses
	PollInterval time.Duration
	// MaxConcurrentRefreshes bounds how many addresses are refreshed at once in total — both
	// tick's regular sweep and the trigger fast-path share this same bound (see refreshOne) —
	// since each refresh is a full multi-network scan (see domain.ActivitySupervisedStore.
	// RefreshAddress), so, unlike the tracker's per-step resolutions, running every supervised
	// address at once could spawn far more outbound bridge-service/RPC calls than the configured
	// concurrency limits on the scanner side expect. A value <= 0 falls back to
	// DefaultActivityEngineMaxConcurrentRefreshes
	MaxConcurrentRefreshes int
	// IdleTimeout is how long an unaccessed address is kept supervised before being forgotten
	// (see domain.ActivitySupervisedStore.PruneIdle). A value <= 0 falls back to
	// DefaultActivityEngineIdleTimeout
	IdleTimeout time.Duration
	// RefreshTimeout bounds a single RefreshAddress call (see refreshOne), mirroring
	// EngineConfig.ResolveTimeout's reasoning for the tracker's own per-step resolutions: without
	// it, one hung network scan or RPC client can occupy a concurrency slot indefinitely, and —
	// because tick waits for every refresh it started before returning — wedge the whole loop
	// behind it (see tick's own doc). A value <= 0 falls back to
	// DefaultActivityEngineRefreshTimeout
	RefreshTimeout time.Duration
}

ActivityEngineConfig holds the activity engine tunables. Zero values take the defaults above

type ActivityEntry

type ActivityEntry = domain.ActivityEntry

ActivityEntry is one bridge found for a from_address by the GET /activity/from/{from_address} endpoint, enriched with its claim/tracking state; see domain.ActivityEntry

type ActivityQuerier

type ActivityQuerier = domain.ActivityQuerier

ActivityQuerier is the driven port the activity endpoint depends on for reading

type ActivityRegistry

type ActivityRegistry = domain.ActivityRegistry

ActivityRegistry is the full activity subsystem: state plus reads, what the activity HTTP command depends on

type ActivitySourceBridgeServiceConfig

type ActivitySourceBridgeServiceConfig struct {
	// PageSize is the page size used while paging through a network's own
	// GET /bridge/v1/bridges scanning for a from_address (see fetchNewBridgesFrom). A value <= 0
	// falls back to DefaultActivitySourceBridgeServicePageSize.
	PageSize uint32 `mapstructure:"PageSize"`

	// Timeout bounds every individual HTTP call this source makes to a network's aggkit bridge
	// service (GetBridges, GetClaims, GetSyncStatus, GetL1InfoTreeIndex,
	// GetInjectedL1InfoLeaf — see bridgeservice/client.Client) — not the overall GET
	// /activity/from/{from_address} request itself, which can still issue several such calls per
	// bridge (see fetchNewBridgesFrom, ActivityCache.refresh). A value <= 0 falls back to
	// DefaultActivitySourceBridgeServiceTimeout.
	Timeout types.Duration `mapstructure:"Timeout"`

	// MaxConcurrentNetworkScans bounds how many networks sources.ActivitySource.BridgesFrom
	// scans at once — each one, in turn, queries this bridge-service source and the RPC
	// fallback (see ActivitySourceRPC) concurrently, so the actual number of in-flight calls is
	// up to twice this. Networks beyond the limit simply wait their turn instead of every
	// configured network being dialed at once, regardless of how many there are. A value <= 0
	// falls back to DefaultActivitySourceBridgeServiceMaxConcurrentNetworkScans.
	MaxConcurrentNetworkScans int `mapstructure:"MaxConcurrentNetworkScans"`
}

ActivitySourceBridgeServiceConfig is [Tracker.ActivitySourceBridgeService].

type ActivitySourceRPCConfig

type ActivitySourceRPCConfig struct {
	// Enabled turns the RPC-based fallback on: GET /activity/from/{from_address} additionally
	// scans each network's own bridge contract directly via RPC over
	// [RangeFromBlock, RangeToBlock], in parallel with ActivitySourceBridgeService, merging in
	// whatever bridges that bridge service has not indexed yet. false keeps today's
	// bridge-service-only behavior.
	Enabled bool `mapstructure:"Enabled"`

	// RangeFromBlock is the lower bound of the RPC scan window, e.g. "LatestBlock/-90" (last 90
	// blocks) or "FinalizedBlock". A value <= 0/empty falls back to
	// DefaultActivitySourceRPCRangeFromBlock. Ignored when Enabled is false.
	RangeFromBlock aggkittypes.BlockNumberFinality `jsonschema:"enum=LatestBlock,enum=SafeBlock,enum=FinalizedBlock,enum=PendingBlock" mapstructure:"RangeFromBlock"` //nolint:lll

	// RangeToBlock is the upper bound of the RPC scan window, typically "LatestBlock". Ignored
	// when Enabled is false.
	RangeToBlock aggkittypes.BlockNumberFinality `jsonschema:"enum=LatestBlock,enum=SafeBlock,enum=FinalizedBlock,enum=PendingBlock" mapstructure:"RangeToBlock"` //nolint:lll
}

ActivitySourceRPCConfig is [Tracker.ActivitySourceRPC].

func (*ActivitySourceRPCConfig) Validate

func (c *ActivitySourceRPCConfig) Validate() error

Validate checks if the ActivitySourceRPCConfig is valid

type ActivitySupervisedStore

type ActivitySupervisedStore = domain.ActivitySupervisedStore

ActivitySupervisedStore is the driven port to the supervised from_addresses list behind the activity endpoint — the engine-facing counterpart of ActivityQuerier

type ActivityTriggerable

type ActivityTriggerable = domain.ActivityTriggerable

ActivityTriggerable is an optional capability of an ActivitySupervisedStore: it exposes freshly registered from_addresses so ActivityEngine can refresh them immediately

type BridgeAddressResolver

type BridgeAddressResolver = domain.BridgeAddressResolver

BridgeAddressResolver is the driven port the bridge-address endpoint depends on: it resolves the bridge contract address for one network, or enumerates every network currently known

type BridgeEventSource

type BridgeEventSource = domain.BridgeEventSource

BridgeEventSource is the driven port resolving the BridgeEvent behind a supervised tx on its origin network (RPC receipt or bridge service, resolved per network via the finder). domain.ResolveBridgeTx calls it directly — see its doc

type BridgeInfo

type BridgeInfo = domain.BridgeInfo

BridgeInfo holds the immutable facts of a bridge, resolved once from its creation tx

type BridgeStepPath

type BridgeStepPath = domain.BridgeStepPath

BridgeStepPath is the domain-internal representation of one step of the expected path of a bridge; see api.BridgeStepPath for the wire shape published to clients

type BridgeTracker

type BridgeTracker struct {
	// contains filtered or unexported fields
}

BridgeTracker is the bridge tracker component: it owns the supervised-bridges registry and the HTTP service implementing the tracker endpoints (API). The tracking engine is wired separately (see NewEngine) over the same registry passed in Config

func New

func New(cfg *Config) *BridgeTracker

New returns an instance of BridgeTracker

func (*BridgeTracker) API

func (b *BridgeTracker) API() *api.API

API returns the HTTP service of the tracker; register it on the shared HTTP server to expose the tracker REST/WS endpoints

func (*BridgeTracker) Activity

func (b *BridgeTracker) Activity() ActivityRegistry

Activity returns the activity subsystem, so its caller can start an ActivityEngine over it (see NewActivityEngine); nil when the activity endpoint is not configured

func (*BridgeTracker) Publish

func (b *BridgeTracker) Publish(id TrackingID, info *BridgeInfo, allSteps []BridgeStepPath)

Publish stores the resolved bridge facts and expected path of a supervised bridge and pushes it to every subscriber (REST polls return it, WebSocket connections receive a "status" message); TrackingStatus and step index are derived from allSteps (see TrackingData), and the public BridgeStatus is derived from info (see api.BridgeStatus). It is a no-op if the bridge is not in the supervised list

func (*BridgeTracker) PublishError

func (b *BridgeTracker) PublishError(id TrackingID, errStep *types.ErrorStep)

PublishError marks a supervised bridge as terminally failed to resolve at all (e.g. the tx does not exist on the network or is not a bridge tx): TrackingStatus becomes Error and errStep is exposed as TrackingData.Error, both to REST polls and WebSocket connections (which then close normally). It is a no-op if the bridge is not in the supervised list

func (*BridgeTracker) StartMetricsSampler

func (b *BridgeTracker) StartMetricsSampler(ctx context.Context, interval time.Duration)

StartMetricsSampler registers the bridgetracker Prometheus metrics and starts refreshing them every interval (DefaultMetricsSampleInterval if <= 0) until ctx is done. The gauges are refreshed once immediately. Call it only when Prometheus is enabled

type CertificateSource

type CertificateSource interface {
	// CertificateFor returns the certificate that includes the bridge, or nil if the bridge
	// is not part of any certificate yet
	CertificateFor(ctx context.Context, bridge *BridgeInfo) (*types.CertificateInclusionData, error)
}

CertificateSource is the driven port to the agglayer: which certificate includes a bridge and in which state it is

type ClaimChecker

type ClaimChecker interface {
	// IsClaimed reports whether bridge has already been claimed on its destination network
	IsClaimed(ctx context.Context, bridge *BridgeInfo) (bool, error)
}

ClaimChecker is the driven port to whether a bridge has been claimed on its destination network, per isClaimed() on the destination bridge contract — the on-chain source of truth StepWaitingClaim resolves against

type ClaimSource

type ClaimSource interface {
	// ClaimFor returns the claim transaction of the bridge on the destination network, or
	// nil if the destination network's bridge service has not indexed it yet
	ClaimFor(ctx context.Context, bridge *BridgeInfo) (*types.ClaimResult, error)
}

ClaimSource is the driven port to the claim record of a bridge on its destination network

type Config

type Config struct {
	// RetentionPeriod is how long a terminal bridge (Finished, or failed to ever resolve)
	// stays queryable before the tracker forgets it. Clients polling or subscribed observe
	// the terminal TrackingStatus during this window; once forgotten, a new request for the
	// same tx re-registers it and tracking restarts from scratch — the retry path for a tx
	// the tracker gave up on
	RetentionPeriod types.Duration `mapstructure:"RetentionPeriod"`

	// IdleTimeout is how long a bridge — terminal or still active — stays in the registry once
	// nobody has read it (no Get/GetAndAwait) and it has no active WebSocket subscriber. Unlike
	// RetentionPeriod, this applies regardless of TrackingStatus: a bridge that never resolves
	// and that nobody is polling or subscribed to would otherwise stay supervised (and in
	// memory) forever. A value <= 0 falls back to DefaultIdleTimeout
	IdleTimeout types.Duration `mapstructure:"IdleTimeout"`

	// RegisterResolveTimeout is how long the GetTxStatus endpoint waits, the first time a tx is
	// registered, for the tracking engine's immediate resolution attempt (triggered right away
	// instead of on the next poll tick, see Registry.GetAndAwait) to produce an update before
	// answering. A value <= 0 disables the wait: the first response is always the bare
	// Registered snapshot, exactly as before this field existed. Looking up an already-registered
	// tx never waits, regardless of this setting.
	RegisterResolveTimeout types.Duration `mapstructure:"RegisterResolveTimeout"`

	// L1BlockFinality is the finality level a bridge's creating tx receipt must reach on L1
	// (network 0) before the tracker accepts it (see sources.BridgeEventSource): accepting
	// a receipt from a block that later gets reorged out would otherwise leave the tracker
	// permanently following an orphaned deposit count/block, since a resolved bridge is never
	// re-checked (see TrackingBridgeTx.IsDone)
	L1BlockFinality aggkittypes.BlockNumberFinality `` //nolint:lll
	/* 132-byte string literal not displayed */

	// L2BlockFinality is the finality level a bridge's creating tx receipt must reach on any
	// L2 (non-zero network) before the tracker accepts it; see L1BlockFinality for the reasoning
	L2BlockFinality aggkittypes.BlockNumberFinality `` //nolint:lll
	/* 132-byte string literal not displayed */

	// BridgeAddrs is the static networkID -> canonical bridge contract address override used
	// to reject a BridgeEvent log emitted by a contract other than the origin network's real
	// bridge (see sources.BridgeEventSource). A network absent from this map (the default,
	// empty map) falls back to resolving its canonical bridge address through the bridge
	// service finder — there is no permissive fallback that matches logs on the event
	// signature alone.
	BridgeAddrs map[uint32]common.Address `mapstructure:"BridgeAddrs"`

	// L1GlobalExitRootAddress is the L1 GlobalExitRoot contract address (see sources.GERSource)
	L1GlobalExitRootAddress common.Address `mapstructure:"L1GlobalExitRootAddress"`

	// L2GlobalExitRootAddress is the static networkID -> GlobalExitRootManagerL2 contract address
	// map used as a fallback when a destination network's bridge-service instance does not
	// report the L2 block a covering GER was actually injected at (see sources.GERSource,
	// WaitingGERInjection's InjectedGERResult.L2InjectedGER): for a network present here, the
	// tracker scans that network's own L2 for the UpdateHashChainValue event instead of leaving
	// L2InjectedGER absent. A network absent from this map (the default, empty map) never gets
	// this fallback attempted — no different from before it existed
	L2GlobalExitRootAddress map[uint32]common.Address `mapstructure:"L2GlobalExitRootAddress"`

	// L2InjectionLookbackBlocks bounds how many blocks the L2GlobalExitRootAddress fallback (see
	// sources.GERSource.findL2InjectionBlockBackwards) scans backwards from the destination
	// network's head before giving up, instead of continuing all the way back to genesis: on a
	// network whose fallback never finds the event (e.g. a wrong L2GlobalExitRootAddress entry,
	// or a GER injected before the contract even existed there), an unbounded scan would walk the
	// entire chain history in bridgeservicefinder.DefaultBlockChunkSize-sized eth_getLogs calls
	// every time. A value <= 0 falls back to DefaultL2InjectionLookbackBlocks.
	L2InjectionLookbackBlocks uint64 `mapstructure:"L2InjectionLookbackBlocks"`

	// MaxTrackedBridges bounds how many distinct bridges the registry (see Registry) accepts at
	// once; a request that would exceed it fails instead of registering the bridge — reaching
	// the cap never evicts an existing entry to make room. On the default in-memory adapter,
	// RetentionPeriod and IdleTimeout are what keep the registry under it during normal
	// operation; the SQLite-backed adapter (see DBPath) does not evict anything yet, so once
	// reached there the cap is effectively permanent until the process restarts. A value <= 0
	// falls back to DefaultMaxTrackedBridges. Only meaningful to the binary wiring Registry
	// from it — ignored when Registry is set to a custom implementation.
	MaxTrackedBridges int `mapstructure:"MaxTrackedBridges"`

	// MaxConcurrentResolutions bounds how many active bridges the engine's poll tick resolves at
	// once (see EngineConfig.MaxConcurrentResolutions) — independent of MaxTrackedBridges, which
	// bounds the registry's size, not how much of it is in flight during a single tick. A value
	// <= 0 falls back to DefaultMaxConcurrentResolutions.
	MaxConcurrentResolutions int `mapstructure:"MaxConcurrentResolutions"`

	// DBPath is the SQLite file the supervised-bridges registry persists to (see
	// bridgetracker/db.NewSQLiteRegistry). Empty (the default) keeps the in-memory adapter
	// (NewMemoryRegistry): state does not survive a restart, exactly as before this field
	// existed. Setting it makes the binary construct a SQLite-backed registry instead, so
	// already-resolved bridges don't need re-resolving — and every bridge-service/agglayer call
	// behind that — after a restart. Only meaningful to the binary wiring Registry from it;
	// ignored once Registry is set.
	DBPath string `mapstructure:"DBPath"`

	// AgglayerClient configures the client used to query the agglayer for a bridge's covering
	// certificate and that certificate's current status (see sources.CertificateSource)
	AgglayerClient agglayer.ClientConfig `mapstructure:"AgglayerClient"`

	Logger aggkitcommon.Logger `mapstructure:"-"`

	// ConfigSHA1 is the sha1sum (hex) of the configuration the binary was started with,
	// exposed by the health endpoint to check that all instances behind a proxy run the
	// same configuration
	ConfigSHA1 string `mapstructure:"-"`

	// Registry is the supervised-bridges subsystem to use. Leave nil to get the in-memory
	// adapter (single instance); inject a shared-store implementation so several tracker
	// instances behind a proxy answer for any registered tx
	Registry SupervisedRegistry `mapstructure:"-"`

	// CORS mirrors the proxy's REST.CORS: it governs which origins may open a WebSocket
	// connection to this tracker's endpoints (see aggkitcommon.CORSConfig.OriginAllowed for
	// why WebSocket needs its own check instead of reusing the REST CORS headers). Wired
	// programmatically from REST.CORS by the binary, not read directly from [Tracker].
	CORS aggkitcommon.CORSConfig `mapstructure:"-"`

	// ActivityScanner and ActivityClaims wire the optional GET /activity/from/{from_address}
	// endpoint (see ActivityCache): ActivityScanner scans every configured bridge service for
	// bridges sent by an address, ActivityClaims resolves each one's claim state. Both are
	// wired programmatically by the binary (see sources.ActivitySource, which implements
	// both); leaving either nil leaves the endpoint unregistered entirely.
	ActivityScanner ActivityBridgeScanner `mapstructure:"-"`
	ActivityClaims  ActivityClaimChecker  `mapstructure:"-"`

	// Activity, when set, is used as the activity endpoint's ActivityRegistry as-is instead of
	// building an in-memory ActivityCache from ActivityScanner/ActivityClaims — the same
	// override pattern as Registry, so a caller can plug in a persisted implementation (see
	// bridgetracker/db.NewSQLiteActivityStore, which itself still needs ActivityScanner/
	// ActivityClaims — it takes them directly as constructor arguments, not through this
	// Config). ActivityScanner/ActivityClaims/ActivityIdleTimeout are ignored once this is set.
	Activity ActivityRegistry `mapstructure:"-"`

	// ActivityIdleTimeout is how long a from_address stays supervised (see ActivityCache,
	// ActivityEngine.PruneIdle) with no GET /activity/from/{from_address} call for it, before
	// being forgotten entirely (bridges, claim state, everything cached for it). Same semantics
	// as IdleTimeout, a separate knob because it governs a different cache. A value <= 0 falls
	// back to DefaultIdleTimeout.
	ActivityIdleTimeout types.Duration `mapstructure:"ActivityIdleTimeout"`

	// ActivityPollInterval is the period between the activity engine's background refresh
	// rounds over every supervised from_address (see ActivityEngine, ActivityEngineConfig.
	// PollInterval). A value <= 0 falls back to DefaultActivityPollInterval.
	ActivityPollInterval types.Duration `mapstructure:"ActivityPollInterval"`

	// MetricsSampleInterval is how often the Prometheus gauges (cache size, alive trackers/
	// activities) are refreshed (see BridgeTracker.StartMetricsSampler). Only used when the
	// proxy's [Prometheus] section is enabled. A value <= 0 falls back to
	// DefaultMetricsSampleInterval.
	MetricsSampleInterval types.Duration `mapstructure:"MetricsSampleInterval"`

	// ActivityRegisterResolveTimeout is how long the activity endpoint waits, the first time a
	// from_address is registered, for the activity engine's immediate refresh attempt
	// (triggered right away instead of on the next poll tick, see ActivitySupervisedStore.
	// RegisterAndAwait) to complete before answering. A value <= 0 disables the wait: the first
	// response is always whatever is cached so far (possibly empty). Looking up an
	// already-registered address never waits, regardless of this setting.
	ActivityRegisterResolveTimeout types.Duration `mapstructure:"ActivityRegisterResolveTimeout"`

	// ActivityMaxConcurrentRefreshes bounds how many supervised addresses the activity engine's
	// poll tick refreshes at once (see ActivityEngineConfig.MaxConcurrentRefreshes). A value
	// <= 0 falls back to DefaultActivityMaxConcurrentRefreshes.
	ActivityMaxConcurrentRefreshes int `mapstructure:"ActivityMaxConcurrentRefreshes"`

	// BridgeAddressResolver wires the optional GET /bridge-address[/{network_id}] endpoint: it
	// resolves the bridge contract address for one network, or every network it currently
	// knows about. Wired programmatically by the binary (bridgeservicefinder.Finder satisfies
	// this port directly); leaving it nil leaves the endpoint unregistered entirely.
	BridgeAddressResolver BridgeAddressResolver `mapstructure:"-"`

	// ActivitySourceBridgeService configures the bridge-service-backed ActivityBridgeScanner —
	// the existing GET /bridge/v1/bridges-based source behind GET /activity/from/{from_address}
	// (see sources.ActivitySource.BridgesFrom's REST path).
	ActivitySourceBridgeService ActivitySourceBridgeServiceConfig `mapstructure:"ActivitySourceBridgeService"`

	// ActivitySourceRPC configures the RPC-based fallback ActivityBridgeScanner (see
	// sources.activityRPCScanner, agglayer/aggkit#1837): a safety net that scans each network's
	// own bridge contract directly via RPC, in parallel with ActivitySourceBridgeService, for
	// bridges that service has not indexed yet. It never calls debug_traceTransaction, so it
	// works against any standard JSON-RPC endpoint (the debug namespace is not required); the
	// trade-off is that an Asset bridge routed through an intermediate contract (not sent
	// directly to the bridge contract) is not resolved by this fallback and is silently skipped —
	// it still shows up once ActivitySourceBridgeService's own indexer (which does trace) catches
	// up with it.
	ActivitySourceRPC ActivitySourceRPCConfig `mapstructure:"ActivitySourceRPC"`

	// PendingNetworksLister exposes the bridge service finder's not-yet-activated networks on the
	// health endpoint. Wired programmatically by the binary; nil is valid (the field is omitted)
	PendingNetworksLister api.PendingNetworksLister `mapstructure:"-"`
}

Config holds the configuration of the bridge tracker service. Only the mapstructure-tagged fields come from the configuration file; the rest are wired programmatically by the binary (see proxy/cmd)

func (*Config) Validate

func (c *Config) Validate() error

Validate checks if the configuration is valid

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is the tracking engine: it watches the supervised list, resolves the status of every active bridge through the fact sources and stores each change (which the registry fans out to REST polls and WebSocket subscribers). All engine-private resolution state (resolved bridge facts, not-found counter, last published status/steps) lives in the store itself, as part of each bridge's TrackingData

func NewEngine

func NewEngine(
	cfg EngineConfig,
	logger aggkitcommon.Logger,
	store SupervisedStore,
	sources EngineSources,
) (*Engine, error)

NewEngine returns a tracking engine over the given store and fact sources

func (*Engine) Start

func (e *Engine) Start(ctx context.Context)

Start launches the resolution loop; it stops when ctx is cancelled. Besides the regular poll cadence, it also watches the store's Triggerable channel (if implemented) to resolve a freshly registered bridge right away instead of leaving it for the next tick — see resolveTriggered and SupervisedStore.GetAndAwait, which is what a caller actually waits on

type EngineConfig

type EngineConfig struct {
	// PollInterval is the period between resolution rounds over the supervised list
	PollInterval time.Duration
	// ResolveTimeout bounds the source calls of a single bridge resolution
	ResolveTimeout time.Duration
	// UnresolvedTimeout is how long a supervised tx is given, since first seen unresolved, to
	// have FindBridge succeed before it is marked terminally failed (TrackingStatus becomes
	// Error, with an exhausted ErrorStep explaining why)
	UnresolvedTimeout time.Duration
	// RetentionPeriod is how long a terminal bridge stays queryable before being forgotten
	// (see DefaultEngineRetentionPeriod)
	RetentionPeriod time.Duration
	// IdleTimeout is how long an unaccessed, unsubscribed bridge — terminal or still active —
	// is kept before being forgotten (see DefaultEngineIdleTimeout)
	IdleTimeout time.Duration
	// MaxConcurrentResolutions bounds how many active bridges tick resolves at once: the
	// supervised list can hold as many entries as Config.MaxTrackedBridges allows (registration
	// is externally driven), so resolving it with one goroutine per entry and no cap could spawn
	// as many outbound source calls at once, exhausting memory/sockets/RPC capacity well before
	// that many bridges are ever actually active. A value <= 0 falls back to
	// DefaultEngineMaxConcurrentResolutions
	MaxConcurrentResolutions int
}

EngineConfig holds the tracking engine tunables. Zero values take the defaults above

type EngineSources

type EngineSources struct {
	Bridges                BridgeEventSource
	Certificates           CertificateSource
	GERs                   GERSource
	WaitingGERUpdateSource domain.WaitingGERUpdateSource
	LERs                   LERSource
	ClaimChecker           ClaimChecker
	Claims                 ClaimSource
	Settlement             SettlementSource
	// SettlementHistory backs WaitL1SettledGERResolver's exact-settlement search (see issue
	// #1817): in practice the same concrete instance as Certificates, which also implements
	// domain.SettlementHistorySource
	SettlementHistory domain.SettlementHistorySource
}

EngineSources groups the driven ports the engine resolves bridge facts through. Each is handed directly to the one or two step resolvers that need it (see createResolvers) rather than adapted into one do-everything port

type GERSource

type GERSource interface {
	// OriginGER returns the GER update on the origin network that covers the bridge, or nil
	// if the bridge is not covered by any GER update yet. Only queried for L1-originated
	// bridges
	OriginGER(ctx context.Context, bridge *BridgeInfo) (*types.GERData, error)

	// InjectedGER returns the GER injected on the destination network that covers the
	// bridge, or nil if no covering GER has been injected yet. Only meaningful when the
	// destination is an L2 (injection does not apply to Mainnet)
	InjectedGER(ctx context.Context, bridge *BridgeInfo) (*types.GERData, error)

	// L1InfoTreeIndexForGER resolves the L1 info tree leaf index ger (produced by the bridge's
	// certificate settlement, see types.L1SettledGERResult) landed at, or nil if the L1 info
	// tree has not caught up with it yet. Only queried by StepWaitL1SettledGER, and only when
	// the settlement tx did not emit UpdateL1InfoTreeV2 (which already carries the index)
	L1InfoTreeIndexForGER(ctx context.Context, bridge *BridgeInfo, ger common.Hash) (*uint32, error)

	// InjectedGERAtIndex returns the GER injected at leafIndex on the bridge's destination
	// network, or nil if it has not been injected yet. Only queried for L2-originated bridges
	// arriving at an L2, right after StepWaitL1SettledGER
	InjectedGERAtIndex(ctx context.Context, bridge *BridgeInfo, leafIndex uint32) (*types.GERData, error)

	// L1InfoTreeIndexForBridge returns the L1 info tree leaf index covering bridge's origin
	// deposit — GET /bridge/v1/l1-info-tree-index, queried against the bridge-service instance
	// that will build the claim proof for it: the origin network's own instance, or, when the
	// origin is mainnet (which has no bridge-service deployment of its own), the destination's
	// — or nil if that instance's own L1 info tree sync has not caught up to this deposit yet.
	// Only queried by StepWaitingL1InfoLeafAvailable
	L1InfoTreeIndexForBridge(ctx context.Context, bridge *BridgeInfo) (*uint32, error)
}

GERSource is the driven port to the Global Exit Root state on both sides of a bridge

type LERSource

type LERSource interface {
	// OriginLER returns the LER update on the origin L2 network that covers the bridge, or
	// nil if the bridge is not covered yet
	OriginLER(ctx context.Context, bridge *BridgeInfo) (*types.LERUpdateResult, error)
}

LERSource is the driven port to the Local Exit Root state on an L2-originated bridge's origin network

type SettlementSource

type SettlementSource interface {
	// SettlementGERUpdate returns the evidence read off settlementTxHash's L1 receipt once it
	// reaches the configured L1 finality, or nil if it is not there yet
	SettlementGERUpdate(
		ctx context.Context, bridge *BridgeInfo, settlementTxHash common.Hash,
	) (*types.L1SettledGERResult, error)
}

SettlementSource is the driven port to the L1 evidence a certificate's settlement produces: the RollupManager/GlobalExitRoot events (VerifyBatchesTrustedAggregator, UpdateL1InfoTree[V2]) emitted by the settlement tx itself

type StatusNotifier

type StatusNotifier = domain.StatusNotifier

StatusNotifier is the driven port push consumers (the WebSocket handler) use to follow a supervised bridge.

Implementations must deliver every SetStatus / SetError of the same bridge as a TrackingData snapshot to all its active subscriptions: both ports are two views of one subsystem and are always implemented together (see SupervisedRegistry).

type SupervisedRegistry

type SupervisedRegistry = domain.SupervisedRegistry

SupervisedRegistry is the full supervised-bridges subsystem: state plus change notification. The in-memory adapter (NewMemoryRegistry) implements it for a single instance; a shared-store adapter can replace it so several tracker instances behind a proxy answer for any registered tx (see the statefulness note in the API doc)

func NewMemoryRegistry

func NewMemoryRegistry(maxEntries int) SupervisedRegistry

NewMemoryRegistry returns an in-memory SupervisedRegistry that refuses to register more than maxEntries distinct bridges at once (see memoryRegistry.maxEntries); maxEntries <= 0 falls back to DefaultMaxTrackedBridges.

type SupervisedStore

type SupervisedStore = domain.SupervisedStore

SupervisedStore is the driven port to the supervised-bridges state. The HTTP handlers use its read side (Register) and the tracking engine its write side (SetStatus / SetError).

Implementations must be safe for concurrent use.

type TrackingID

type TrackingID = domain.TrackingID

TrackingID identifies a supervised bridge: the network the creating tx was sent to plus its hash

Directories

Path Synopsis
api
docs
Package docs Code generated by swaggo/swag.
Package docs Code generated by swaggo/swag.
db
Package db holds everything SQLite-related for the bridge tracker: the sqliteRegistry adapter of domain.SupervisedRegistry and its migrations (see migrations/), mirroring how aggsender/db holds its own component's SQLite storage.
Package db holds everything SQLite-related for the bridge tracker: the sqliteRegistry adapter of domain.SupervisedRegistry and its migrations (see migrations/), mirroring how aggsender/db holds its own component's SQLite storage.
migrations
Package migrations holds the bridge tracker's own SQLite migrations (see bridgetracker's sqliteRegistry), following the same //go:embed + db.RunMigrations pattern as reorgdetector/migrations.
Package migrations holds the bridge tracker's own SQLite migrations (see bridgetracker's sqliteRegistry), following the same //go:embed + db.RunMigrations pattern as reorgdetector/migrations.
Package domain holds the pure business rules of the bridge tracker: decisions that depend only on bridge facts, with no I/O and no dependency on the ports or adapters.
Package domain holds the pure business rules of the bridge tracker: decisions that depend only on bridge facts, with no I/O and no dependency on the ports or adapters.
Package sources implements the driven fact ports of the bridge tracker engine (bridgetracker.BridgeEventSource, GERSource, LERSource, ClaimSource, CertificateSource) over the real backends: the per-network JSON-RPC endpoints and the aggkit bridge service REST API, both resolved per network through the bridgeservicefinder.
Package sources implements the driven fact ports of the bridge tracker engine (bridgetracker.BridgeEventSource, GERSource, LERSource, ClaimSource, CertificateSource) over the real backends: the per-network JSON-RPC endpoints and the aggkit bridge service REST API, both resolved per network through the bridgeservicefinder.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL