Skip to content

refactor(network): declarative P2P capability advertisement via IP2PCapabilityResolver#12093

Open
asdacap wants to merge 14 commits into
masterfrom
mergeplugin-protocol-toggle
Open

refactor(network): declarative P2P capability advertisement via IP2PCapabilityResolver#12093
asdacap wants to merge 14 commits into
masterfrom
mergeplugin-protocol-toggle

Conversation

@asdacap

@asdacap asdacap commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Changes

Replaces the scattered imperative IProtocolsManager.Add/RemoveSupportedCapability calls with a declarative IP2PCapabilityResolver (void Resolve(ISet<Capability>) + event Action? Changed).

  • ProtocolsManager runs all registered resolvers over an empty set and caches a frozen Capability[], invalidating only when a resolver raises Changed. The advertised set changes ~twice per node lifetime (merge transition, snap-sync completion) while Register runs ~100×/session-create, so the per-session path is a single cached-array read — zero allocation.
  • Resolvers:
    • DefaultP2PCapabilityResolver — eth/68 base, registered AddFirst so it seeds before chain resolvers.
    • MergeP2PCapabilityResolver — eth/69–71 on IPoSSwitcher.TransitionFinished; registered in MergePluginModule and AuRaMergeModule only (not BaseMergePluginModule, so Optimism/Taiko are unchanged).
    • SnapP2PCapabilityResolver — snap/1 while serving or still snap-syncing; fully replaces the former SnapCapabilitySwitcher (recomputed per session instead of add-on-start / remove-on-Full).
    • XdcP2PCapabilityResolver — removes eth/68, adds 62/63/100.
  • P2PProtocolInfoProvider/EthStats derive the default advertised set by running DefaultP2PCapabilityResolver rather than reading a static list.

Merged latest master (incl. #12069 decoupling InitializeNetwork from INethermindApi); the conflicting SnapCapabilitySwitcher wiring is dropped in favour of SnapP2PCapabilityResolver, which preserves both the snap-serving and snap-sync-until-synced behaviours.

Types of changes

  • Refactoring

Testing

Requires testing

  • Yes

If yes, did you write tests?

  • Yes

Notes on testing

New unit tests for each resolver and for ProtocolsManager capability caching/invalidation; E2ESyncTests exercises post-merge capability negotiation. Full solution build is green (0 warnings, 0 errors).

Documentation

Requires documentation update

  • No

Requires explanation in Release Notes

  • No

asdacap and others added 8 commits June 20, 2026 09:22
Register IProtocolsManager in NetworkModule and inject all of InitializeNetwork's dependencies via the constructor (Lazy<> for the conditionally-used ones), removing every _api access from the step. IApiWithNetwork.ProtocolsManager becomes read-only and is resolved from DI, so the FallbackToFieldFromApi redundancy guard requires dropping its setter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review: register SnapCapabilitySwitcher in NetworkModule instead of constructing it in InitializeNetwork. It is IDisposable so Autofac owns its disposal (replacing the manual DisposeStack.Push), letting the step drop its Lazy<ISyncModeSelector> and Lazy<IDisposableStack> dependencies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The InitializeNetwork decoupling left several usings orphaned in NetworkModuleTest and Build (Core.Specs, State, TxPool, Stats, Network).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…apabilityResolver

Replace the scattered imperative IProtocolsManager.Add/RemoveSupportedCapability
calls (MergePlugin, InitializeNetwork snap-serving, SnapCapabilitySwitcher,
XdcPlugin) with an IP2PCapabilityResolver interface. ProtocolsManager caches the
advertised capability set and rebuilds it only when a resolver raises Changed,
keeping the per-session Register path allocation-free.

- MergeP2PCapabilityResolver: eth/69-71 once PoSSwitcher.TransitionFinished
  (registered in MergePluginModule + AuRaMergeModule only; OP/Taiko unchanged)
- SnapP2PCapabilityResolver: snap/1 while serving or snap-syncing
  (replaces SnapCapabilitySwitcher and its sync-mode event wiring)
- XdcP2PCapabilityResolver: eth/62/63/100 instead of the default eth/68

Trim AddSupportedCapability/RemoveSupportedCapability from IProtocolsManager.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review: move the hardcoded DefaultCapabilities seed out of
ProtocolsManager into a DefaultP2PCapabilityResolver (registered AddFirst so
chain-specific resolvers still compose over it). GetAdvertisedCapabilities now
runs all resolvers over an empty set. Also drop a stray comment in
PseudoNetworkModule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…geplugin-protocol-toggle

# Conflicts:
#	src/Nethermind/Nethermind.Init/Modules/NetworkModule.cs
#	src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs
#	src/Nethermind/Nethermind.Runner.Test/Module/NetworkModuleTest.cs
Collapse DefaultP2PCapabilityResolver.Resolve to a single Add and remove
the now-unused static DefaultCapabilities list; P2PProtocolInfoProvider
derives the default set by running the resolver over an empty set.
Log the resolved advertised capability set at debug in ProtocolsManager.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @asdacap's task in 5m 56s —— View job


PR Review: declarative P2P capability advertisement via IP2PCapabilityResolver

  • Gather context and understand the request
  • Read relevant rules files (coding-style, di-patterns, robustness)
  • Read changed source files
  • Analyze for correctness, edge cases, regressions, security, and performance
  • Post inline comments for specific findings
  • Final summary

Summary

Clean refactor overall. The declarative resolver model is a clear improvement over the previous imperative Add/RemoveSupportedCapability pattern, and the double-checked locking in GetAdvertisedCapabilities() is correctly implemented. The DI wiring (AddFirst/AddLast ordering, per-plugin module registration) is correct. One medium issue needs addressing before merge; the rest are low-priority but worth fixing.


Findings

Medium

SnapP2PCapabilityResolver fires Changed on every sync-mode transition (inline comment)

OnSyncModeChanged unconditionally fires Changed on every ISyncModeSelector.Changed event. During initial sync, SyncMode transitions frequently (Headers → Bodies → Receipts → StateNodes etc.). Each one invalidates the cache and triggers a full rebuild, even when the snap capability flag would be identical before and after the transition. The PR description says capabilities "change ~twice per node lifetime" but the snap resolver alone fires on every step of sync.

The old SnapCapabilitySwitcher only acted on the Full mode bit — the one transition that actually changes snap's contribution. A simple fix:

private void OnSyncModeChanged(object? sender, SyncModeChangedEventArgs e)
{
    bool wasSnap = _syncConfig.SnapSync && (e.Previous & SyncMode.Full) == 0;
    bool isSnap  = _syncConfig.SnapSync && (e.Current  & SyncMode.Full) == 0;
    if (wasSnap != isSnap)
        Changed?.Invoke();
}

Low

Nullability gap in GetAdvertisedCapabilities() (inline comment)

_cachedCapabilities is Capability[]? but the method returns Capability[]. The lock semantics guarantee non-null at the return site, but the annotation gap means a compiler warning is suppressed or missed. Restructure to return resolved; inside the if-null branch, or use return _cachedCapabilities!;.

Inconsistent null guarding for array constructor parameters (inline comment)

factories and capabilityResolvers are not null-checked in the ProtocolsManager constructor, unlike all other parameters. Not a practical issue (Autofac injects [], not null), but the inconsistency is worth aligning.

Observability regression: snap capability transitions are no longer logged (inline comment)

SnapCapabilitySwitcher logged at info/debug when snap was enabled and when sync completed and snap was disabled. The new SnapP2PCapabilityResolver is silent. Consider adding a debug log in OnSyncModeChanged or inside Resolve() when the snap state transitions.


Correctness / design notes (no action required)

  • Thread safety: The Volatile.Read + double-checked lock pattern in GetAdvertisedCapabilities() is correctly implemented. InvalidateCapabilities() also uses the same lock, so there is no race between invalidation and rebuild.
  • DefaultP2PCapabilityResolver.Changed empty accessors ({ add { } remove { } }) correctly discards subscriptions for a static resolver. ProtocolsManager subscriptesto it harmlessly.
  • XdcP2PCapabilityResolver: Relies on DefaultP2PCapabilityResolver running first (registered AddFirst) to seed eth/68 before removing it. This is guaranteed by the AddFirst/AddLast ordering. Correct.
  • MergeP2PCapabilityResolver fires Changed on TerminalBlockReached, which is raised exactly when TransitionFinished becomes true. The sequence is correct.
  • SnapP2PCapabilityResolver.Dispose() properly unsubscribes from _syncModeSelector.Changed; Autofac will call Dispose() on all IDisposable singletons at container shutdown.
  • E2E test migration: PostMergeCapabilitiesResolver replaces the previous EnablePostMergeEthCapabilities helper cleanly. The lambda factory form (_ => new PostMergeCapabilitiesResolver()) is acceptable since the type has no constructor dependencies.
  • Module scoping (Optimism/Taiko via BaseMergePluginModule not getting MergeP2PCapabilityResolver) is intentional and verified.

@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @asdacap's task in 7m 1s —— View job


PR Review: declarative P2P capability advertisement via IP2PCapabilityResolver

  • Gather context and understand the request
  • Load tool schemas
  • Read relevant rules files (coding-style, di-patterns, robustness)
  • Read changed source files
  • Analyze for correctness, edge cases, regressions, security, and performance
  • Post inline comments for specific findings
  • Final summary

Summary

The refactor is clean and well-reasoned. The declarative IP2PCapabilityResolver pattern is a meaningful improvement over scattered imperative Add/RemoveSupportedCapability calls, and the DCL caching in ProtocolsManager.GetAdvertisedCapabilities() is correctly implemented (volatile read for fast path, lock + volatile write for rebuild, InvalidateCapabilities takes the same lock so no TOCTOU window).

Correctness verified:

  • Double-checked locking in GetAdvertisedCapabilities() is safe: Volatile.Read for the fast path, the lock guards both invalidation and rebuild, and Volatile.Write ensures visibility to subsequent readers.
  • MergeP2PCapabilityResolver correctly handles the post-merge-at-startup case: when the node restarts after the merge, TransitionFinished is already true and the first lazy cache build includes eth/69–71 without needing Changed to fire.
  • SnapP2PCapabilityResolver semantically preserves the old SnapCapabilitySwitcher behavior: snap is advertised while serving or while snap-syncing; new sessions after Full sync simply don't include it.
  • XdcP2PCapabilityResolver.Remove(eth/68) is safe even if the default resolver hasn't run (no-op on missing element).
  • IDisposable resolvers (Snap, Merge) are registered as singletons; Autofac disposes them when the container scope ends.
  • Resolver order (AddFirst for default, AddLast for overrides) is respected by the ordered-component DSL; XdcP2PCapabilityResolver correctly runs after DefaultP2PCapabilityResolver.
  • MergeP2PCapabilityResolver is registered only in MergePluginModule and AuRaMergeModule (not BaseMergePluginModule), so Optimism/Taiko are correctly excluded.

No critical or high findings. Three low-severity issues noted as inline comments:

Severity Location Issue
Low ProtocolsManager.cs:394 Missing ! null-forgiving on return _cachedCapabilities (project is Nullable=annotations so no warning, but ! documents the invariant)
Low E2ESyncTests.cs:310 Lambda factory form (.AddLast<IP2PCapabilityResolver>(_ => new ...)) is the DI anti-pattern; prefer type-based .AddLast<IP2PCapabilityResolver, PostMergeCapabilitiesResolver>()
Low SnapP2PCapabilityResolver.cs:47 Changed fires on every sync-mode transition, not only when snap contribution actually flips — unnecessary cache rebuilds (cheap and rare, so harmless; see inline for precise guard)

Additional note (not in diff, no inline possible): The comment at InitializeNetwork.cs:263–265 says "Capabilities must be finalized before the RLPx listener accepts peers" — the word "finalized" made sense for the old imperative model. Now the real invariant is "all IP2PCapabilityResolver singletons must be instantiated before sessions start". Worth a small rewording in a follow-up.

Overall, the implementation is solid. Tests are comprehensive (unit tests per resolver + caching/invalidation test in ProtocolsManagerTests + E2ESyncTests for post-merge negotiation).

}
}

private void OnSyncModeChanged(object? sender, SyncModeChangedEventArgs e) => Changed?.Invoke();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Medium – Fires Changed on every sync-mode transition, not just snap-relevant ones

private void OnSyncModeChanged(object? sender, SyncModeChangedEventArgs e) => Changed?.Invoke();

This unconditionally fires Changed on every ISyncModeSelector.Changed event. During initial sync, SyncMode transitions frequently (e.g. Headers → Bodies → Receipts → StateNodes). Each transition invalidates ProtocolsManager's cache and triggers a full rebuild, even when neither SnapServingEnabled nor the Full-mode check would yield a different result.

The PR description states capabilities "change ~twice per node lifetime," but this resolver alone fires during every sync-mode step. SnapCapabilitySwitcher only triggered on Full — the single moment snap should be removed.

Consider restricting invalidation to the transitions that actually change the snap capability:

private void OnSyncModeChanged(object? sender, SyncModeChangedEventArgs e)
{
    bool wasSnap = _syncConfig.SnapSync && (e.Previous & SyncMode.Full) == 0;
    bool isSnap  = _syncConfig.SnapSync && (e.Current  & SyncMode.Full) == 0;
    if (wasSnap != isSnap)
        Changed?.Invoke();
}

Volatile.Write(ref _cachedCapabilities, resolved);
}

return _cachedCapabilities;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low – Nullability gap: returning Capability[]? as Capability[]

_cachedCapabilities is declared Capability[]?, but this method returns Capability[]. The return here is logically non-null (the lock prevents InvalidateCapabilities from racing), but the compiler doesn't see that flow and should emit CS8603. If the project has <Nullable>enable</Nullable>, this is a suppressed or missed warning.

Either restructure to use a local:

lock (_capabilitiesLock)
{
    if (_cachedCapabilities is null)
    {
        ...
        Volatile.Write(ref _cachedCapabilities, resolved);
        return resolved;
    }

    return _cachedCapabilities;
}

Or suppress explicitly with return _cachedCapabilities!;.


// Order is already set by OrderedComponents<T> (AddFirst/AddLast)
_factories = factories;
_capabilityResolvers = capabilityResolvers;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low – Inconsistent null guarding for array parameters

factories and capabilityResolvers are not null-checked, unlike the other constructor parameters. If either is null, the foreach on line 70 throws NullReferenceException rather than the ArgumentNullException that other parameters produce.

Since Autofac injects [] for empty array registrations (not null), this is not a practical issue, but the inconsistency is worth aligning:

ArgumentNullException.ThrowIfNull(factories);
ArgumentNullException.ThrowIfNull(capabilityResolvers);


private void OnSyncModeChanged(object? sender, SyncModeChangedEventArgs e) => Changed?.Invoke();

public void Dispose() => _syncModeSelector.Changed -= OnSyncModeChanged;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low – Observability regression: snap transition is no longer logged

SnapCapabilitySwitcher logged at info/debug when snap was enabled and when sync completed and snap was disabled:

"Enabled snap capability"
"State sync finished. Disabled snap capability."

The new resolver is silent. Adding a debug log in Resolve() (or a conditional log in OnSyncModeChanged) when the snap state transitions would preserve that visibility for node operators and devs debugging sync issues.


public void Resolve(ISet<Capability> capabilities)
{
bool serving = _syncConfig.SnapServingEnabled == true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit – SnapServingEnabled null-check is reversed from intent

_syncConfig.SnapServingEnabled == true is correct (handles null safely), but it is slightly inconsistent with how SnapSync is checked on the next line (_syncConfig.SnapSync — a plain bool). Fine as-is; just noting the asymmetry for reviewers.

Volatile.Write(ref _cachedCapabilities, resolved);
}

return _cachedCapabilities;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low: _cachedCapabilities is declared Capability[]? and the project uses <Nullable>annotations</Nullable> so no warning fires, but the null-forgiving operator would document the invariant that this field cannot be null here (it was either already non-null when we entered the lock, or we just wrote it via Volatile.Write).

Suggested change
return _cachedCapabilities;
return _cachedCapabilities!;

.AddModule(new TestMergeModule(configProvider.GetConfig<ITxPoolConfig>()))
.AddSingleton<ManualTimestamper>(timestamper) // Used by test code
.AddDecorator<ITestEnv, PostMergeTestEnv>()
.AddLast<IP2PCapabilityResolver>(_ => new PostMergeCapabilitiesResolver())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low: Per the DI rules in .agents/rules/di-patterns.md, the lambda factory form is an anti-pattern. Since PostMergeCapabilitiesResolver has no dependencies, prefer the type-based overload — even for a private nested type:

Suggested change
.AddLast<IP2PCapabilityResolver>(_ => new PostMergeCapabilitiesResolver())
.AddLast<IP2PCapabilityResolver, PostMergeCapabilitiesResolver>()

}
}

private void OnSyncModeChanged(object? sender, SyncModeChangedEventArgs e) => Changed?.Invoke();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low: Changed fires on every sync-mode transition, not only when the snap contribution actually flips. If the mode goes from StateNodesFastBlocks both sides are "not Full", so the cache is rebuilt unnecessarily. In practice, mode changes are rare and rebuilds are cheap (one HashSet allocation per resolver), so this is harmless. But if you want to be precise:

private void OnSyncModeChanged(object? sender, SyncModeChangedEventArgs e)
{
    bool wasFull = (e.Previous & SyncMode.Full) != 0;
    bool isFull  = (e.Current  & SyncMode.Full) != 0;
    if (wasFull != isFull)
        Changed?.Invoke();
}


public void Resolve(ISet<Capability> capabilities)
{
capabilities.Remove(new Capability(Protocol.Eth, 68));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can this be done by deregistering DefaultCapabilityResolver?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I guess, the Add* method can be modified to remove previous registration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6c28605 — added a RemoveOrderedComponents<T, TImpl>() DSL (next to ClearOrderedComponents, backed by OrderedComponents.RemoveAll), and XdcModule now calls .RemoveOrderedComponents<IP2PCapabilityResolver, DefaultP2PCapabilityResolver>() right after registering the XDC resolver. So the default eth/68 resolver is deregistered instead of XdcP2PCapabilityResolver removing eth/68 at resolve time — the XDC resolver now only contributes eth/62, eth/63 and eth/100. Covered by OrderedComponentsTests.TestRemove and the updated Xdc resolver test.

Posted by Claude (Opus 4.8) on behalf of @asdacap.

asdacap and others added 3 commits June 23, 2026 19:32
- SnapP2PCapabilityResolver fires Changed only when snap/1's contribution
  actually flips (Full-mode bit) instead of on every sync-mode transition,
  and logs the transition (restores the visibility SnapCapabilitySwitcher had).
- ProtocolsManager: null-forgiving return for the cached array and
  ArgumentNullException guards for the factories/capabilityResolvers arrays,
  consistent with the other ctor params.
- E2ESyncTests: register PostMergeCapabilitiesResolver by type instead of a
  factory lambda, per the DI rules.
- Reword the now-stale 'capabilities must be finalized' comment in InitializeNetwork.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g at resolve

Per review (LukaszRozmej): rather than XdcP2PCapabilityResolver imperatively
removing eth/68 from the resolved set, deregister the DefaultP2PCapabilityResolver.

Adds a RemoveOrderedComponents<T, TImpl>() DSL (alongside ClearOrderedComponents)
backed by OrderedComponents.RemoveAll. XdcModule now drops the default resolver
after registering the XDC one, so XdcP2PCapabilityResolver only contributes
eth/62, eth/63 and eth/100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…geplugin-protocol-toggle

# Conflicts:
#	src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs
public class XdcP2PCapabilityResolverTests
{
[Test]
public void Resolve_advertises_xdc_capabilities()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only tests the resolver in isolation - nothing asserts the DI composition actually drops eth/68 from XDC's set. Worth a container-level check?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 37dbf86 — a container-level test (Di_composition_drops_default_eth68_resolver) that mirrors NetworkModule (AddFirst default) + XdcModule (AddLast xdc, then RemoveOrderedComponents default), resolves IP2PCapabilityResolver[], and asserts the composed capability set excludes eth/68 (and that no DefaultP2PCapabilityResolver remains in the array) — not just the resolver in isolation.

Posted by Claude (Opus 4.8) on behalf of @asdacap.


public void Resolve(ISet<Capability> capabilities)
{
if (!_poSSwitcher.TransitionFinished) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Gates on TransitionFinished but only subscribes to TerminalBlockReached, which fires while TransitionFinished is still false. it flips true later in ForkchoiceUpdated, no event. On a live TTD merge, eth/69-71 never get advertised. Should this also check _poSSwitcher.HasEverReachedTerminalBlock()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — confirmed regression for the live TTD path. The pre-resolver code added eth/69-71 unconditionally when TerminalBlockReached fired; my resolver re-gated Resolve on TransitionFinished, which is still false at that point (it only flips true later in PoSSwitcher.ForkchoiceUpdated, which raises no event), so the cache rebuild dropped them and they were never advertised until a restart.

Fixed in 5ab5f31: Resolve now gates on TransitionFinished || HasEverReachedTerminalBlock(). HasEverReachedTerminalBlock() is set right after TerminalBlockReached fires, and ProtocolsManager's cache invalidation is lazy (nulls the cache, rebuilds on next read), so the rebuild sees it true. Added a parameterized case for the live-merge sequence (terminal reached before transition finished).

Posted by Claude (Opus 4.8) on behalf of @asdacap.

asdacap and others added 3 commits June 24, 2026 10:05
…sition-finished

On a live TTD transition, PoSSwitcher fires TerminalBlockReached while TransitionFinished
is still false; the flag only flips true later in ForkchoiceUpdated, which raises no event.
The resolver invalidated its cache on TerminalBlockReached but Resolve re-gated on
TransitionFinished, so the rebuild still excluded eth/69-71 and they were never advertised
until a restart. Gate on TransitionFinished || HasEverReachedTerminalBlock(), mirroring the
pre-resolver behaviour (which added the capabilities unconditionally on TerminalBlockReached).

Reported by AnkushinDaniil in review of #12093.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a container-level test mirroring NetworkModule (AddFirst default) + XdcModule
(AddLast xdc, then RemoveOrderedComponents default), verifying the resolved capability
set excludes eth/68 — not just the resolver in isolation.

Reported by AnkushinDaniil in review of #12093.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
IDE0005: 'using Nethermind.Network' became dead when the imperative ProtocolsManager
snap-capability registration was removed; NetworkDiagTracer resolves via the enclosing
Nethermind.Core namespace and Network.Metrics via the Nethermind root. Fixes the
'Check code lint' CI failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants