Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c596a28
fix(e2e/op-pp): anchor L2 genesis timestamp to L1 head instead of wal…
joanestebanr Aug 25, 2026
ffda100
fix(e2e/op-pp): raise max_sequencer_drift to avoid stall after ~30min
joanestebanr Aug 25, 2026
c4df2b4
Revert "fix(e2e/op-pp): raise max_sequencer_drift to avoid stall afte…
joanestebanr Aug 25, 2026
aad7076
Revert "fix(e2e/op-pp): anchor L2 genesis timestamp to L1 head instea…
joanestebanr Aug 25, 2026
8ecf96b
fix(bridgetracker): resolve settled GER when settlement tx has no Upd…
joanestebanr Aug 26, 2026
b48aae4
feat(bridgetracker): add GET /activity/from/{from_address} endpoint
joanestebanr Aug 27, 2026
919ae4f
feat(bridgetracker): activity endpoint improvements β€” filters, networ…
joanestebanr Aug 27, 2026
ed56de5
feat(bridgetracker): add creation/last-updated timestamps to activity…
joanestebanr Aug 28, 2026
7c1afb1
feat(bridgetracker): claimed step resolver, bridge-address endpoint, …
joanestebanr Aug 28, 2026
7ab9701
fix(bridgetracker): gate certificate settlement on L1 tx visibility
joanestebanr Sep 1, 2026
aa14134
docs(bridgetracker): clarify L2GlobalExitRootAddrs is a workaround-on…
joanestebanr Sep 1, 2026
7769551
feat(bridgetracker): report per-network scan failures as warnings in …
joanestebanr Sep 1, 2026
23e68c8
feat(bridgetracker): resolve actual L2 GER injection block, with fall…
joanestebanr Sep 1, 2026
0c021aa
fix(bridgetracker): fix off-by-one in backwards UpdateL1InfoTree bloc…
joanestebanr Sep 2, 2026
ce78dab
fix(bridgetracker): exclude same-block GER updates after the settleme…
joanestebanr Sep 2, 2026
a603660
test(bridgetracker): cover the isClaimed()-true-but-not-indexed-yet w…
joanestebanr Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions autoclaim/proof/leaf_proof_refresher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ func (f *fakeURLResolver) GetURL(networkID uint32) (bridgeservicefinder.NetworkU
return bridgeservicefinder.NetworkURLs{BridgeURL: f.urls[networkID]}, nil
}

func (f *fakeURLResolver) NetworkIDs() []uint32 {
ids := make([]uint32, 0, len(f.urls))
for id := range f.urls {
ids = append(ids, id)
}
return ids
}

func (f *fakeURLResolver) BridgeAddress(context.Context, uint32) (common.Address, error) {
return common.Address{}, nil
}

// fakeClaimProofClient implements claimProofClient for tests, keyed by base URL.
type fakeClaimProofClient struct {
baseURL string
Expand Down
8 changes: 8 additions & 0 deletions autoclaim/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,14 @@ func (noopBridgeServiceFinder) GetURL(networkID uint32) (bridgeservicefinder.Net
networkID)
}

func (noopBridgeServiceFinder) NetworkIDs() []uint32 { return nil }

func (noopBridgeServiceFinder) BridgeAddress(_ context.Context, networkID uint32) (common.Address, error) {
return common.Address{}, fmt.Errorf(
"autoclaim bridge service finder is not configured (AutoClaim.L2ToLxBridgeDetector.Enabled=false): network %d",
networkID)
}

// startRuntimeComponents launches the goroutines for tx managers, claimers, and the bridge detector.
func startRuntimeComponents(
ctx context.Context,
Expand Down
6 changes: 6 additions & 0 deletions autoclaim/runtime/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ func (fakeBridgeServiceFinder) GetURL(uint32) (bridgeservicefinder.NetworkURLs,
return bridgeservicefinder.NetworkURLs{BridgeURL: "http://fake-source"}, nil
}

func (fakeBridgeServiceFinder) NetworkIDs() []uint32 { return nil }

func (fakeBridgeServiceFinder) BridgeAddress(context.Context, uint32) (common.Address, error) {
return common.Address{}, nil
}

func withL2ToLxEnabled(cfg autoclaimcfg.Config) autoclaimcfg.Config {
cfg.L2ToLxBridgeDetector = autoclaimcfg.L2ToLxBridgeDetector{
Enabled: true,
Expand Down
123 changes: 123 additions & 0 deletions bridgeservicefinder/bridge_address_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package bridgeservicefinder

import (
"errors"
"testing"

"github.com/agglayer/aggkit/bridgeservicefinder/mocks"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

// newTestFinderForBridgeAddress builds a *finder (not the Finder interface) with rm as its
// RollupManagerQuerier, so tests can call BridgeAddress directly without going through Start
// (BridgeAddress is resolved lazily, on first use, independently of the cache built by Start).
func newTestFinderForBridgeAddress(t *testing.T, cfg Config, rm RollupManagerQuerier) *finder {
t.Helper()

f, err := New(cfg, Options{RollupManager: rm, LogFilterer: mocks.NewLogFilterer(t), Logger: testLogger()})
require.NoError(t, err)
concrete, ok := f.(*finder)
require.True(t, ok)
return concrete
}

// TestBridgeAddress_DefaultsToRollupManagerBridgeAddress verifies a network absent from
// Config.BridgeAddress resolves to the rollup manager's own on-chain BridgeAddress().
func TestBridgeAddress_DefaultsToRollupManagerBridgeAddress(t *testing.T) {
wantAddr := common.HexToAddress("0xb1123e")
rm := mocks.NewRollupManagerQuerier(t)
rm.EXPECT().BridgeAddress(mock.Anything).Return(wantAddr, nil).Once()

f := newTestFinderForBridgeAddress(t, Config{}, rm)

got, err := f.BridgeAddress(t.Context(), 1)
require.NoError(t, err)
require.Equal(t, wantAddr, got)
}

// TestBridgeAddress_CachesTheOnChainDefaultAcrossNetworksAndCalls verifies the on-chain
// BridgeAddress() call happens at most once, regardless of how many networks or calls ask for
// the default β€” it is an immutable constructor parameter, safe to cache forever.
func TestBridgeAddress_CachesTheOnChainDefaultAcrossNetworksAndCalls(t *testing.T) {
wantAddr := common.HexToAddress("0xb1123e")
rm := mocks.NewRollupManagerQuerier(t)
rm.EXPECT().BridgeAddress(mock.Anything).Return(wantAddr, nil).Once() // .Once(): a second call fails the test

f := newTestFinderForBridgeAddress(t, Config{}, rm)

for _, networkID := range []uint32{1, 2, 1} {
got, err := f.BridgeAddress(t.Context(), networkID)
require.NoError(t, err)
require.Equal(t, wantAddr, got)
}
}

// TestBridgeAddress_OverridePrecedesTheOnChainDefault verifies a networkID present in
// Config.BridgeAddress is served verbatim, without ever consulting the rollup manager.
func TestBridgeAddress_OverridePrecedesTheOnChainDefault(t *testing.T) {
overrideAddr := common.HexToAddress("0xdeaf")
rm := mocks.NewRollupManagerQuerier(t) // no BridgeAddress expectation: must never be called

f := newTestFinderForBridgeAddress(t, Config{BridgeAddress: map[uint32]common.Address{63: overrideAddr}}, rm)

got, err := f.BridgeAddress(t.Context(), 63)
require.NoError(t, err)
require.Equal(t, overrideAddr, got)
}

// TestBridgeAddress_Network0OverrideIsDefaultForOtherNetworks verifies a Config.BridgeAddress[0]
// override doubles as the default for a network with no override of its own, taking precedence
// over the on-chain rollup manager BridgeAddress() (which must never be consulted in this case).
func TestBridgeAddress_Network0OverrideIsDefaultForOtherNetworks(t *testing.T) {
network0Addr := common.HexToAddress("0xcafe")
rm := mocks.NewRollupManagerQuerier(t) // no BridgeAddress expectation: must never be called

f := newTestFinderForBridgeAddress(t, Config{BridgeAddress: map[uint32]common.Address{0: network0Addr}}, rm)

for _, networkID := range []uint32{0, 5, 82} {
got, err := f.BridgeAddress(t.Context(), networkID)
require.NoError(t, err)
require.Equal(t, network0Addr, got)
}
}

// TestBridgeAddress_PerNetworkOverridePrecedesNetwork0Default verifies a network's own override
// wins over Config.BridgeAddress[0], even when both are configured.
func TestBridgeAddress_PerNetworkOverridePrecedesNetwork0Default(t *testing.T) {
network0Addr := common.HexToAddress("0xcafe")
network63Addr := common.HexToAddress("0xdeaf")
rm := mocks.NewRollupManagerQuerier(t) // no BridgeAddress expectation: must never be called

f := newTestFinderForBridgeAddress(t, Config{
BridgeAddress: map[uint32]common.Address{0: network0Addr, 63: network63Addr},
}, rm)

got, err := f.BridgeAddress(t.Context(), 63)
require.NoError(t, err)
require.Equal(t, network63Addr, got)

got, err = f.BridgeAddress(t.Context(), 84)
require.NoError(t, err)
require.Equal(t, network0Addr, got)
}

// TestBridgeAddress_OnChainFailureIsNotCached verifies a failed on-chain read is not cached: the
// next call retries instead of repeating the same error forever.
func TestBridgeAddress_OnChainFailureIsNotCached(t *testing.T) {
wantAddr := common.HexToAddress("0xb1123e")
wantErr := errors.New("rpc unavailable")
rm := mocks.NewRollupManagerQuerier(t)
rm.EXPECT().BridgeAddress(mock.Anything).Return(common.Address{}, wantErr).Once()
rm.EXPECT().BridgeAddress(mock.Anything).Return(wantAddr, nil).Once()

f := newTestFinderForBridgeAddress(t, Config{}, rm)

_, err := f.BridgeAddress(t.Context(), 1)
require.ErrorIs(t, err, wantErr)

got, err := f.BridgeAddress(t.Context(), 1)
require.NoError(t, err)
require.Equal(t, wantAddr, got)
}
57 changes: 56 additions & 1 deletion bridgeservicefinder/bridgeservicefinder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"sync"

"github.com/0xPolygon/cdk-contracts-tooling/contracts/aggchain-multisig/agglayermanager"
aggkitcommon "github.com/agglayer/aggkit/common"
Expand Down Expand Up @@ -74,6 +75,13 @@ type finder struct {
// skipped entirely during enumeration (see buildInitialCache) and live discovery (see listener's
// discoverRollup).
ignoreNetworkIDs map[uint32]struct{}

// bridgeAddrMu guards defaultBridgeAddr
bridgeAddrMu sync.Mutex
// defaultBridgeAddr caches the rollup manager's own BridgeAddress() once resolved (see
// BridgeAddress): nil until the first call needs it. It is an immutable constructor parameter of
// the rollup manager, so once resolved it is cached forever β€” never re-read or invalidated.
defaultBridgeAddr *common.Address
}

// buildIgnoreSet turns Config.IgnoreNetworkIDs into a set for O(1) membership checks.
Expand Down Expand Up @@ -187,7 +195,11 @@ func (f *finder) Start(ctx context.Context) error {
if err := f.buildInitialCache(ctx); err != nil {
return err
}

listNetworksStr := ""
for networkID := range f.cache.entries {
listNetworksStr += fmt.Sprintf("%d, ", networkID)
}
f.logger.Info("Resolved network entries: " + listNetworksStr)
unhealthy := f.probeAll(ctx)
if unhealthy > 0 && f.cfg.RequireAllHealthyOnStart {
return fmt.Errorf("%w: %d unreachable", ErrServicesUnhealthyOnStart, unhealthy)
Expand Down Expand Up @@ -375,3 +387,46 @@ func (f *finder) GetURL(networkID uint32) (NetworkURLs, error) {

return NetworkURLs{BridgeURL: entry.url, JSONRPCURL: entry.jsonRPCURL}, nil
}

// NetworkIDs returns the networkIDs of every network currently resolved (i.e. every network
// GetURL would presently succeed for).
func (f *finder) NetworkIDs() []uint32 {
return f.cache.networkIDs()
}

// BridgeAddress returns the bridge contract address for networkID, in priority order:
// 1. Config.BridgeAddress[networkID], if set.
// 2. Config.BridgeAddress[0], if set β€” network 0's override doubles as the default for every
// other network that has none of its own, since it is typically the shared L1 bridge address.
// 3. The rollup manager's own on-chain BridgeAddress(), resolved once and cached forever (see
// defaultBridgeAddress) β€” only reached when neither override above is configured.
func (f *finder) BridgeAddress(ctx context.Context, networkID uint32) (common.Address, error) {
if addr, ok := f.cfg.BridgeAddress[networkID]; ok {
return addr, nil
}
if addr, ok := f.cfg.BridgeAddress[0]; ok {
return addr, nil
}
return f.defaultBridgeAddress(ctx)
}

// defaultBridgeAddress returns the rollup manager's own BridgeAddress(), resolving it on chain the
// first time it is needed and caching it forever after: it is an immutable constructor parameter
// of the rollup manager, so it can never change once deployed. A transient failure (e.g. a
// transport error) is not cached, so the next call retries the on-chain read.
func (f *finder) defaultBridgeAddress(ctx context.Context) (common.Address, error) {
f.bridgeAddrMu.Lock()
defer f.bridgeAddrMu.Unlock()

if f.defaultBridgeAddr != nil {
return *f.defaultBridgeAddr, nil
}

addr, err := f.rollupManager.BridgeAddress(&bind.CallOpts{Context: ctx})
if err != nil {
return common.Address{}, fmt.Errorf("reading rollup manager's bridge address: %w", err)
}

f.defaultBridgeAddr = &addr
return addr, nil
}
14 changes: 14 additions & 0 deletions bridgeservicefinder/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,17 @@ func (c *cache) set(networkID uint32, entry cacheEntry) {

c.entries[networkID] = entry
}

// networkIDs returns the networkIDs of every network currently cached. It takes a read lock so
// it is safe to call concurrently with set.
func (c *cache) networkIDs() []uint32 {
c.mu.RLock()
defer c.mu.RUnlock()

ids := make([]uint32, 0, len(c.entries))
for id := range c.entries {
ids = append(ids, id)
}

return ids
}
43 changes: 43 additions & 0 deletions bridgeservicefinder/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package bridgeservicefinder

import (
"testing"

"github.com/stretchr/testify/require"
)

// TestCache_NetworkIDs verifies networkIDs returns exactly the networkIDs currently cached,
// with no duplicates and regardless of insertion order.
func TestCache_NetworkIDs(t *testing.T) {
c := newCache()
require.Empty(t, c.networkIDs())

c.set(1, cacheEntry{url: "http://network-1"})
c.set(0, cacheEntry{url: "http://network-0"})
c.set(42, cacheEntry{url: "http://network-42"})

require.ElementsMatch(t, []uint32{0, 1, 42}, c.networkIDs())

// Overwriting an existing entry does not duplicate it
c.set(1, cacheEntry{url: "http://network-1-updated"})
require.ElementsMatch(t, []uint32{0, 1, 42}, c.networkIDs())
}

// TestFinder_NetworkIDs verifies finder.NetworkIDs delegates to the cache, i.e. it reports
// exactly the networks GetURL would presently succeed for.
func TestFinder_NetworkIDs(t *testing.T) {
f := &finder{cache: newCache()}
require.Empty(t, f.NetworkIDs())

f.cache.set(1, cacheEntry{url: "http://network-1"})
f.cache.set(7, cacheEntry{url: "http://network-7"})

ids := f.NetworkIDs()
require.ElementsMatch(t, []uint32{1, 7}, ids)

for _, id := range ids {
urls, err := f.GetURL(id)
require.NoError(t, err)
require.NotEmpty(t, urls.BridgeURL)
}
}
7 changes: 7 additions & 0 deletions bridgeservicefinder/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ type Config struct {
// absent from this map get their JSON-RPC endpoint from the rollup's trustedSequencerURL.
RPCURLs map[uint32]string `mapstructure:"RPCURLs"`

// BridgeAddress is the static override map from networkID to bridge contract address, consulted
// by Finder.BridgeAddress in priority order: BridgeAddress[networkID], then BridgeAddress[0]
// (which doubles as the default for every network without its own entry β€” typically the shared
// L1 bridge address), then finally the rollup manager's own on-chain BridgeAddress() if neither
// is set. Only networks whose bridge contract differs from that default need their own entry.
BridgeAddress map[uint32]common.Address `mapstructure:"BridgeAddress"`

// BlockFinality is the finality level used to bound the upper block of each event scan, so the
// finder does not react to logs that may still be reorged away. See aggkittypes.BlockNumberFinality.
BlockFinality aggkittypes.BlockNumberFinality `jsonschema:"enum=PendingBlock,enum=LatestBlock,enum=SafeBlock,enum=FinalizedBlock,enum=EarliestBlock" mapstructure:"BlockFinality"` //nolint:lll
Expand Down
18 changes: 18 additions & 0 deletions bridgeservicefinder/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ type Finder interface {
// ErrURLNotFound if nothing is cached. networkID follows the mapping documented in doc.go
// (networkID == rollupID; network 0 is L1 and is only served if provided via Config.BridgeURLs).
GetURL(networkID uint32) (NetworkURLs, error)
// NetworkIDs returns the networkIDs of every network currently resolved β€” i.e. every network
// GetURL would presently succeed for. Used by callers that need to enumerate every configured
// bridge service rather than query one network at a time (e.g. the bridge tracker's activity
// scanner). Order is unspecified.
NetworkIDs() []uint32
// BridgeAddress returns the bridge contract address for networkID, in priority order:
// Config.BridgeAddress[networkID] if set; else Config.BridgeAddress[0] if set (network 0's
// override doubles as the default for every network without its own, since it is typically the
// shared L1 bridge address); else the rollup manager's own on-chain BridgeAddress() β€” resolved
// once and cached forever, since it is an immutable constructor parameter of the rollup manager.
// A network whose bridge contract differs from that default needs its own
// Config.BridgeAddress override. Returns an error only when no override applies and the
// on-chain default could not be resolved (e.g. a transport failure) β€” such a failure is not
// cached, so the next call retries.
BridgeAddress(ctx context.Context, networkID uint32) (common.Address, error)
}

// RollupManagerQuerier enumerates the rollups attached to a rollup manager and reads their data.
Expand All @@ -83,6 +98,9 @@ type RollupManagerQuerier interface {
// aggchain-type rollups. ChainID is the rollup's L2 chain id.
RollupIDToRollupData(opts *bind.CallOpts, rollupID uint32) (
agglayermanager.AgglayerManagerRollupDataReturn, error)
// BridgeAddress returns the bridge contract address the rollup manager was constructed with: an
// immutable constructor parameter, so the same value for the lifetime of the contract.
BridgeAddress(opts *bind.CallOpts) (common.Address, error)
}

// RollupContractReader reads the two on-chain sources (metadata and trusted-sequencer URL) from a
Expand Down
Loading
Loading