Skip to content
Open
45 changes: 34 additions & 11 deletions bridgeservicefinder/bridgeservicefinder.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ type finder struct {
// Start from the enumeration and is the routing table the (S4) event listener will use to apply
// an incoming log to the correct cache entry.
addrToNetworkID map[common.Address]uint32

// ignoreNetworkIDs is the set built from Config.IgnoreNetworkIDs. Networks in this set are
// skipped entirely during enumeration (see buildInitialCache) and live discovery (see listener's
// discoverRollup).
ignoreNetworkIDs map[uint32]struct{}
}

// buildIgnoreSet turns Config.IgnoreNetworkIDs into a set for O(1) membership checks.
func buildIgnoreSet(ids []uint32) map[uint32]struct{} {
set := make(map[uint32]struct{}, len(ids))
for _, id := range ids {
set[id] = struct{}{}
}

return set
}

// New constructs a bridge service finder from cfg and the injectable dependencies in opts. Any
Expand Down Expand Up @@ -114,16 +129,17 @@ func New(cfg Config, opts Options) (Finder, error) {
}

return &finder{
cfg: cfg,
logger: logger,
rollupManager: rollupManager,
readerFactory: readerFactory,
healthChecker: healthChecker,
logFilterer: logFilterer,
ethClient: opts.EthClient,
resolver: newResolver(cfg.BridgeURLs, cfg.RPCURLs, DefaultBridgeServicePort),
cache: newCache(),
addrToNetworkID: make(map[common.Address]uint32),
cfg: cfg,
logger: logger,
rollupManager: rollupManager,
readerFactory: readerFactory,
healthChecker: healthChecker,
logFilterer: logFilterer,
ethClient: opts.EthClient,
resolver: newResolver(cfg.BridgeURLs, cfg.RPCURLs, DefaultBridgeServicePort),
cache: newCache(),
addrToNetworkID: make(map[common.Address]uint32),
ignoreNetworkIDs: buildIgnoreSet(cfg.IgnoreNetworkIDs),
}, nil
}

Expand Down Expand Up @@ -197,7 +213,9 @@ func (f *finder) Start(ctx context.Context) error {
// buildInitialCache enumerates rollups 1..RollupCount(), resolves each network's URLs and installs
// a cache entry (source-tagged, healthy defaults to false and is set by probeAll). Config-only
// networks (e.g. network 0 / L1) present in Config.BridgeURLs are also installed, together with
// their Config.RPCURLs override if any. Networks with no source are skipped.
// their Config.RPCURLs override if any. Networks with no source are skipped. Networks listed in
// Config.IgnoreNetworkIDs are skipped entirely (no on-chain read at all), though a config override
// for them, if any, was already installed by the seeding step above.
func (f *finder) buildInitialCache(ctx context.Context) error {
// Seed config-only entries first (including network 0 / L1) so they are served even if they are
// not among the enumerated rollups. Enumeration re-installs an enumerated network's entry with
Expand All @@ -223,6 +241,11 @@ func (f *finder) buildInitialCache(ctx context.Context) error {
return err
}

if _, ignored := f.ignoreNetworkIDs[rollupID]; ignored {
f.logger.Infof("network %d is in IgnoreNetworkIDs, skipping on-chain resolution", rollupID)
continue
Comment on lines +244 to +246

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip startup probes for ignored static overrides

When a network appears in both IgnoreNetworkIDs and BridgeURLs, the earlier config-seeding loop still places it in the cache, and Start subsequently calls probeAll, which probes every cached entry. An unreachable ignored override therefore still incurs the configured health-check timeout and, with RequireAllHealthyOnStart=true, makes startup fail with ErrServicesUnhealthyOnStart, contrary to the new option's promise to avoid health probes for ignored networks while continuing to serve their static overrides. The startup probing step needs to exclude ignored IDs.

Useful? React with πŸ‘Β / πŸ‘Ž.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch β€” fixed in 769721c: probeAll now skips any networkID in IgnoreNetworkIDs, so a config-overridden-but-ignored network's cache entry is served but never health-probed (never counted toward RequireAllHealthyOnStart either). Added TestStart_IgnoreNetworkIDs_SkipsHealthProbe to cover it.

}

if err := f.resolveNetwork(ctx, rollupID); err != nil {
return fmt.Errorf("failed to build initial cache for network %d: %w", rollupID, err)
}
Expand Down
10 changes: 10 additions & 0 deletions bridgeservicefinder/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,14 @@ type Config struct {
// is unreachable during initial cache construction. When false, unreachable services are cached
// with healthy=false and may be healed by a later on-chain update per the health-gating rule.
RequireAllHealthyOnStart bool `mapstructure:"RequireAllHealthyOnStart"`

// IgnoreNetworkIDs lists networkIDs (rollupIDs) that are entirely excluded from on-chain
// resolution: buildInitialCache skips them during enumeration (no RollupIDToRollupData call, no
// contract reads, no health probe) and the live listener skips them when a rollup-manager
// lifecycle event announces them. This is meant for known "dead" networks (e.g. decommissioned
// or unreachable test rollups) whose on-chain reads and health-check timeouts would otherwise
// slow down startup and event processing for no benefit. A networkID present in Config.BridgeURLs
// is still served from config even if it is also listed here: the ignore only skips on-chain
// inspection, never a static override.
IgnoreNetworkIDs []uint32 `mapstructure:"IgnoreNetworkIDs"`
}
15 changes: 15 additions & 0 deletions bridgeservicefinder/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@
// The rollup manager address is always part of the watched-address filter (even when the initial
// enumeration found zero rollups), so the very first rollups can be discovered this way.
//
// # Ignoring known-dead networks (Config.IgnoreNetworkIDs)
//
// A networkID listed in Config.IgnoreNetworkIDs is skipped entirely from on-chain resolution: it is
// excluded from buildInitialCache's enumeration loop (no RollupIDToRollupData call, no contract
// reader, no health probe) and from the listener's discoverRollup (a CreateNewRollup /
// CreateNewAggchain / AddExistingRollup event announcing it is a no-op, and its contract address is
// never added to the watched set). This exists to avoid known-dead networks - decommissioned or
// permanently unreachable test rollups - from slowing down startup and event processing with RPC
// calls and health-check timeouts that can never succeed.
//
// The ignore list only ever skips on-chain inspection; it never suppresses a static override. A
// networkID present in both Config.IgnoreNetworkIDs and Config.BridgeURLs is still served from
// config, exactly as if it were not ignored (the config-seeding step in buildInitialCache runs
// before, and independently of, the enumeration loop the ignore list affects).
//
// # Error handling at Start (fail loudly vs graceful skip)
//
// During the initial cache build a per-network outcome is classified as either a hard failure or a
Expand Down
15 changes: 14 additions & 1 deletion bridgeservicefinder/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ type listener struct {
// addrToNetworkID, is only mutated on the listener goroutine.
watchedAddresses []common.Address

// ignoreNetworkIDs is the set built from Config.IgnoreNetworkIDs. A rollup-manager lifecycle
// event announcing one of these networkIDs is skipped by discoverRollup: no on-chain reads, no
// cache entry, and its contract is never added to watchedAddresses.
ignoreNetworkIDs map[uint32]struct{}

blockFinality aggkittypes.BlockNumberFinality
pollInterval time.Duration
blockChunkSize uint64
Expand Down Expand Up @@ -170,6 +175,7 @@ func newListener(
ethClient: ethClient,
addrToNetworkID: addrToNetworkID,
watchedAddresses: watched,
ignoreNetworkIDs: buildIgnoreSet(cfg.IgnoreNetworkIDs),
blockFinality: cfg.BlockFinality,
pollInterval: cfg.PollInterval.Duration,
blockChunkSize: cfg.BlockChunkSize,
Expand Down Expand Up @@ -431,7 +437,9 @@ func (l *listener) processRollupManagerLog(ctx context.Context, lg types.Log) {
// discoverRollup registers a rollup that was attached to the rollup manager after Start. It resolves
// the rollup's bridge service URL (same priority rules as the initial cache build), installs a cache
// entry and adds the rollup contract to the watched set so its later URL-changing events are picked
// up. It is a no-op if the rollup contract is already watched.
// up. It is a no-op if the rollup contract is already watched, or if rollupID is listed in
// Config.IgnoreNetworkIDs (in which case it is never added to the watched set either, so its later
// events are not observed).
//
// Unlike the initial cache build (which aborts Start on a hard error), discovery runs on the polling
// goroutine and must not tear it down, so failures are logged rather than propagated. The address is
Expand All @@ -441,6 +449,11 @@ func (l *listener) discoverRollup(ctx context.Context, rollupID uint32, addr com
return
}

if _, ignored := l.ignoreNetworkIDs[rollupID]; ignored {
l.logger.Debugf("network %d (%s) is in IgnoreNetworkIDs, skipping live discovery", rollupID, addr)
return
}

reader, err := l.readerFactory(addr, l.ethClient)
if err != nil {
l.logger.Warnf("discovered network %d (%s): failed to build contract reader: %v", rollupID, addr, err)
Expand Down
41 changes: 41 additions & 0 deletions bridgeservicefinder/listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,47 @@ func TestLiveDiscovery_NewRollupNoSourceThenHealedByEvent(t *testing.T) {
"the healing SetTrustedSequencerURL event must also install the json-rpc endpoint")
}

// TestLiveDiscovery_IgnoredNetworkIsNeverRegistered verifies that a CreateNewRollup event announcing
// a networkID listed in Config.IgnoreNetworkIDs is a no-op: no cache entry is installed and the
// rollup's contract address is never added to the routing table or the watched-address set, even
// though it exposes a perfectly resolvable on-chain source.
func TestLiveDiscovery_IgnoredNetworkIsNeverRegistered(t *testing.T) {
backend, auth := newTestBackend(t)
mgrAddr, _ := deployRollupManagerWithRollups(t, backend, auth, 1)

const ignoredNetworkID = uint32(2)

cfg := baseTestConfig(mgrAddr)
cfg.IgnoreNetworkIDs = []uint32{ignoredNetworkID}

f := startFinder(t, cfg, Options{
EthClient: newTestEthClient(backend),
HealthChecker: newMapHealthChecker(nil),
})

sleepPastSeedTick(testPollInterval)

newRollup := deployStandaloneRollup(t, backend, auth, ignoredNetworkID)
const metadataURL = "https://ignored-new-rollup.example.com:5577"
_, err := newRollup.contract.SetAggchainMetadata(auth, MetadataBridgeServiceURLKey, metadataURL)
require.NoError(t, err)
backend.Commit()

mgr := newRollupManagerContract(t, backend, mgrAddr)
_, err = mgr.EmitCreateNewRollup(auth, ignoredNetworkID, newRollup.addr)
require.NoError(t, err)
backend.Commit()

// Give the listener several ticks to (not) act on the event.
sleepPastSeedTick(testPollInterval)
sleepPastSeedTick(testPollInterval)

_, err = f.GetURL(ignoredNetworkID)
require.ErrorIs(t, err, ErrURLNotFound, "an ignored network must never be discovered live")
require.NotContains(t, f.addrToNetworkID, newRollup.addr,
"an ignored network's contract must never be registered in the routing table")
}

// TestLiveUpdate_SequencerThenMetadataUpgrades covers matrix item #4a: a sequencer-sourced network
// is upgraded to metadata (higher priority) via a live AggchainMetadataSet event.
func TestLiveUpdate_SequencerThenMetadataUpgrades(t *testing.T) {
Expand Down
67 changes: 67 additions & 0 deletions bridgeservicefinder/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,73 @@ func TestStart_AllHealthy_RequireAllHealthyOnStartTrue(t *testing.T) {
require.NoError(t, f.Start(ctx))
}

// TestStart_IgnoreNetworkIDs_SkipsEnumeration verifies that a networkID listed in
// Config.IgnoreNetworkIDs is skipped entirely during buildInitialCache's enumeration, even though it
// exposes a perfectly resolvable on-chain source: no cache entry is installed for it, while sibling
// networks are still resolved normally. It also verifies a config override for the SAME networkID is
// still served, since the ignore only skips on-chain inspection, never a static override.
func TestStart_IgnoreNetworkIDs_SkipsEnumeration(t *testing.T) {
backend, auth := newTestBackend(t)
mgrAddr, rollups := deployRollupManagerWithRollups(t, backend, auth, 3)

const (
ignoredNetwork = uint32(1)
ignoredWithConfig = uint32(2)
normalNetwork = uint32(3)
ignoredConfigURL = "https://ignored-config.example.com:5577"
ignoredMetadataURL = "https://ignored-metadata.example.com:5577" // must never surface
normalMetadataURL = "https://normal-metadata.example.com:5577"
)

// Give the purely-ignored network a perfectly good on-chain source: it must still be skipped.
_, err := rollups[ignoredNetwork-1].contract.SetAggchainMetadata(auth, MetadataBridgeServiceURLKey, ignoredMetadataURL)
require.NoError(t, err)
backend.Commit()

// The ignored-but-config-overridden network also has an on-chain source; it must never surface,
// only the config URL should.
_, err = rollups[ignoredWithConfig-1].contract.SetAggchainMetadata(auth, MetadataBridgeServiceURLKey, ignoredMetadataURL)
require.NoError(t, err)
backend.Commit()

_, err = rollups[normalNetwork-1].contract.SetAggchainMetadata(auth, MetadataBridgeServiceURLKey, normalMetadataURL)
require.NoError(t, err)
backend.Commit()

cfg := baseTestConfig(mgrAddr)
cfg.BridgeURLs = map[uint32]string{ignoredWithConfig: ignoredConfigURL}
cfg.IgnoreNetworkIDs = []uint32{ignoredNetwork, ignoredWithConfig}

f, err := New(cfg, Options{
EthClient: newTestEthClient(backend),
HealthChecker: newMapHealthChecker(nil),
Logger: testLogger(),
})
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

require.NoError(t, f.Start(ctx))

_, err = f.GetURL(ignoredNetwork)
require.ErrorIs(t, err, ErrURLNotFound, "ignored network must not be resolved despite exposing a valid on-chain source")

gotConfig, err := f.GetURL(ignoredWithConfig)
require.NoError(t, err)
require.Equal(t, ignoredConfigURL, gotConfig.BridgeURL, "config override must still be served for an ignored network")
require.Empty(t, gotConfig.JSONRPCURL, "on-chain enrichment must not happen for an ignored network")

gotNormal, err := f.GetURL(normalNetwork)
require.NoError(t, err)
require.Equal(t, normalMetadataURL, gotNormal.BridgeURL, "sibling network must resolve normally")

concrete, ok := f.(*finder)
require.True(t, ok)
require.NotContains(t, concrete.addrToNetworkID, rollups[ignoredNetwork-1].addr,
"an ignored network's contract must never be registered in the routing table")
}

// TestStart_NetworkZero covers matrix item #11: network 0 / L1 is never enumerated on-chain; it is
// only served if provided via Config.URLs.
func TestStart_NetworkZero(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions docs/autoclaim.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ it has no GER-injection gate at all, since the GER already exists on L1 by const
| `AutoClaim.BridgeServiceFinder.HealthCheckPath` | `/health` | No | HTTP path probed to assert a resolved bridge service is alive. Empty inherits the default. |
| `AutoClaim.BridgeServiceFinder.HealthCheckTimeout` | `5s` | No | Timeout applied to each health-check HTTP request. `0` inherits the default. |
| `AutoClaim.BridgeServiceFinder.RequireAllHealthyOnStart` | `false` | No | When `true`, finder startup fails if any resolved bridge service is unreachable; when `false`, unreachable services are cached as unhealthy and may heal from a later on-chain update. |
| `AutoClaim.BridgeServiceFinder.IgnoreNetworkIDs` | `[]` | No | Network IDs to exclude entirely from on-chain resolution (e.g. `[5, 12]`): no `RollupIDToRollupData` call, no contract reads, no health probe during enumeration, and rollup-manager lifecycle events announcing them are ignored by live discovery too. Intended for known-dead networks whose unreachable on-chain reads/health checks would otherwise slow down startup and event processing. A network listed here is still served if also present in `BridgeURLs`. |

The `BlockFinality`, `BlockChunkSize`, `HealthCheckPath`, `HealthCheckTimeout`, and `RequireAllHealthyOnStart` values
above are the finder's built-in defaults applied whenever the corresponding field is left unset (zero value); the
Expand Down
Loading