Move BlockCacheService forkchoice hashes and gossip policy to their proper owners#12049
Move BlockCacheService forkchoice hashes and gossip policy to their proper owners#12049asdacap wants to merge 6 commits into
Conversation
…ategy The CL-declared finalized/head hashes now live on BeaconSync instead of BlockCacheService. Reads go through IBeaconSyncStrategy (GetFinalizedHash/ GetHeadBlockHash); writes go through new IMergeSyncController setters, giving a clean read/write split. BlockCacheService consumes the strategy to decide which blocks to protect from pruning; the resulting BeaconSync<->cache cycle is broken with Lazy<IBlockCacheService> on BeaconSync (only used for Clear()). The direct cache readers (MergeGossipPolicy, MergeFinalizedStateProvider, TaikoBeaconHeadAdvancer) now depend on IBeaconSyncStrategy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The finalized and head hashes were always set together as a pair, so collapse the two IMergeSyncController setters into a single SetForkchoiceHashes call. Reduces the write-side interface to one method and each FCU write site to one line; the validated-finalized vs raw-forkchoice distinction between the two sites is preserved by passing the value at each call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… cycle Address review feedback: instead of BeaconSync reaching into IBlockCacheService to Clear() (which forced a Lazy<> to break the BeaconSync<->cache cycle), expose a BeaconSyncStopped event on IBeaconSyncStrategy. BlockCacheService subscribes and clears itself. BeaconSync no longer depends on the cache at all, so the cycle and the Lazy<> are gone; dependency direction is now one-way (cache -> strategy). No/Xdc null-objects get no-op event accessors; TaikoBeaconSync forwards to inner. Also resolve IBeaconSyncStrategy into a local in MergePlugin instead of an inline container lookup when constructing MergeGossipPolicy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the imperative _api.GossipPolicy = ... mutation in plugin Init() with declarative Autofac registration: NetworkModule registers the Policy.FullGossip base; MergePluginModule and AuRaMergeModule decorate it with MergeGossipPolicy; Optimism/Taiko modules override it with ShouldNotGossip. The GossipPolicy api property is removed (FallbackToFieldFromApi throws if a service has both a registration and a settable api property), making the gossip policy a normal DI-resolved singleton like its existing consumers (Eth handlers, SyncServer) already expect. AuRaMergeModule needs its own decorator: AuRaMergePlugin returns AuRaMergeModule (not MergePluginModule) and previously got MergeGossipPolicy only via the deleted imperative path. Test side: MergePluginTests reads the policy via the container; the now-redundant decorator in TestMergeModule is removed (MergePluginModule provides it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BeaconSync: remove the self-evident XML summary on IMergeSyncController.SetForkchoiceHashes. MergeGossipPolicy: now that it is DI-constructed via AddDecorator, its dependencies are always resolved non-null, so the ?? throw ArgumentNullException guards and the nullable parameter annotations are removed (and the now-unused using System). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @asdacap's task in 6m 4s —— View job PR Review: Move BlockCacheService forkchoice hashes and gossip policy to their proper owners
SummaryThis is a clean dependency-ownership refactoring with no user-facing behavior change. The three independent cleanups are all logically correct. No critical or high findings. DI wiring — verified correctThe gossip-policy layering is consistent across all chain configurations:
Cache clearing inversion — correct
Forkchoice hash ownership — correct
FindingsLow — double
|
| Count | |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 0 |
| Low | 2 |
Both Low findings are pre-existing patterns (not introduced by this PR) and carry no safety risk. The PR is mergeable.
| // We MUST discard NewBlock/NewBlockHash messages after receiving FIRST_FINALIZED_BLOCK. | ||
| public bool ShouldDiscardBlocks => _poSSwitcher.TransitionFinished || | ||
| (_blockCacheService.FinalizedHash is not null && _blockCacheService.FinalizedHash != Keccak.Zero); /* _blockCacheService.FinalizedHash is not null && _blockCacheService.FinalizedHash != Keccak.Zero | ||
| (_beaconSyncStrategy.GetFinalizedHash() is not null && _beaconSyncStrategy.GetFinalizedHash() != Keccak.Zero); /* _beaconSyncStrategy.GetFinalizedHash() is not null && _beaconSyncStrategy.GetFinalizedHash() != Keccak.Zero |
There was a problem hiding this comment.
Low — double method call / TOCTOU smell (pre-existing, carried over from old property access)
GetFinalizedHash() is called twice in the same expression. Theoretically, SetForkchoiceHashes on another thread could update the field between the two reads, producing an incoherent evaluation (e.g., first call returns a non-null non-zero value, then SetForkchoiceHashes(null, null) fires, second call returns null).
In practice the only observable effect is a transient false-positive on ShouldDiscardBlocks (we discard blocks we could have accepted), so this is conservative and safe. Still, a local variable would be cleaner:
| (_beaconSyncStrategy.GetFinalizedHash() is not null && _beaconSyncStrategy.GetFinalizedHash() != Keccak.Zero); /* _beaconSyncStrategy.GetFinalizedHash() is not null && _beaconSyncStrategy.GetFinalizedHash() != Keccak.Zero | |
| public bool ShouldDiscardBlocks => _poSSwitcher.TransitionFinished || | |
| (_beaconSyncStrategy.GetFinalizedHash() is { } finalizedHash && finalizedHash != Keccak.Zero); /* _beaconSyncStrategy.GetFinalizedHash() is not null && _beaconSyncStrategy.GetFinalizedHash() != Keccak.Zero | |
| This condition was added for edge case situation. | |
| We started beacon sync, and we hadn't reached transition yet. If CL sent us non zero finalization hash, it would mean that network reached transition. | |
| However, in edge case situation (verified by merge hive tests), our node needs to be reorged to PoW again, so we can't add this condition _beaconSyncStrategy.GetFinalizedHash() != Keccak.Zero | |
| to PoSSwitcher.TransitionFinished. On the other hand, we don't want to receive any blocks from the network, so we want to discard blocks. */ |
| private readonly IBlockCacheService _blockCacheService = blockCacheService; | ||
| private readonly IPoSSwitcher _poSSwitcher = poSSwitcher; | ||
| private bool _isInBeaconModeControl = false; | ||
| private Hash256? _finalizedHash; |
There was a problem hiding this comment.
Low — hash fields are not volatile
_finalizedHash and _headBlockHash are written by SetForkchoiceHashes (engine API thread) and read concurrently by the gossip policy and cache-protection checks on network/sync threads. Without volatile (or Interlocked.Exchange) there's no cross-thread visibility guarantee; the JIT is permitted to cache the value in a register and miss subsequent writes.
This is not a regression — the original BlockCacheService { get; set; } properties were equally unsynchronized. But since ownership of these fields is now concentrated here, this is a good time to add volatile:
| private Hash256? _finalizedHash; | |
| private volatile Hash256? _finalizedHash; | |
| private volatile Hash256? _headBlockHash; |
MergeGossipPolicy.ShouldDiscardBlocks: read GetFinalizedHash() once via a pattern match instead of twice, removing the TOCTOU smell. BeaconSync: mark _finalizedHash/_headBlockHash volatile since they are written by the engine-API thread (SetForkchoiceHashes) and read on network/sync threads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changes
A series of dependency-injection / ownership cleanups around
BlockCacheService, the beacon sync strategy, and the block-gossip policy. No user-facing behavior change.FinalizedHash/HeadBlockHashout ofIBlockCacheServiceinto the beacon sync strategy. These CL-declared forkchoice hashes are now owned byBeaconSync: reads go throughIBeaconSyncStrategy.GetFinalizedHash/GetHeadBlockHash, writes through a newIMergeSyncController.SetForkchoiceHashes.BlockCacheServicereads them back from the strategy to decide which blocks to protect from pruning. Direct cache readers (MergeGossipPolicy,MergeFinalizedStateProvider,TaikoBeaconHeadAdvancer) now depend onIBeaconSyncStrategy.BeaconSyncreaching intoIBlockCacheService.Clear()(which forced aLazy<>to break theBeaconSync ↔ cachecycle),IBeaconSyncStrategyexposes aBeaconSyncStoppedevent thatBlockCacheServicesubscribes to. The dependency is now one-way (cache → strategy); the cycle and theLazy<>are gone.IGossipPolicyfrom the mutableINethermindApi.GossipPolicyproperty to Autofac DI.NetworkModuleregisters thePolicy.FullGossipbase;MergePluginModuleandAuRaMergeModuledecorate it withMergeGossipPolicy; Optimism/Taiko modules override it withShouldNotGossip. The api property is removed (the container'sFallbackToFieldFromApiguard forbids a service having both a registration and a settable api property), so the gossip policy is now a normal DI-resolved singleton like its existing consumers already expect.Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
Added
BlockCacheServiceTest.clears_cache_when_beacon_sync_stopsfor the new event-driven clear; updatedBlockCacheServiceTest,MergeFinalizedStateProviderTests,MergePluginTests, andE2ESyncTestsfor the moved ownership/DI.Verified locally (release):
EthereumRunnerTests271 pass / 2 env-skip.Nethermind.Merge.Plugin.Test,Nethermind.Merge.AuRa.Test,Nethermind.Optimism.Test,Nethermind.Taiko.Test,Nethermind.Network.Test,Nethermind.Synchronization.Testgreen (one pre-existing flaky block-production timing test in AuRa passes in isolation).Documentation
Requires documentation update
Requires explanation in Release Notes
🤖 Generated with Claude Code