From 79fa16627cbcf1ceba92db8aea6c3f0752859272 Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Fri, 29 May 2026 16:41:22 +0200 Subject: [PATCH] refactor(node): replace no-op publisher sentinels with None defaults The sync and validator services defaulted their publish callbacks to no-op async functions used purely as sentinels. Replace them with optional callables defaulting to None and guard each call site. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lean_spec/node/chain/service.py | 7 +++++-- src/lean_spec/node/sync/service.py | 15 +++++++------ src/lean_spec/node/validator/service.py | 28 ++++++++++++------------- tests/lean_spec/node/test_node.py | 2 ++ 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/lean_spec/node/chain/service.py b/src/lean_spec/node/chain/service.py index e3c0ed72..407e9a2a 100644 --- a/src/lean_spec/node/chain/service.py +++ b/src/lean_spec/node/chain/service.py @@ -126,9 +126,12 @@ async def run(self) -> None: new_aggregated_attestations = await self._tick_to(total_interval) # Publish any new aggregated attestations produced this tick. - if new_aggregated_attestations: + # + # No publisher is wired in tests and offline runs. + publish = self.sync_service.publish_aggregated_attestation + if new_aggregated_attestations and publish is not None: for agg in new_aggregated_attestations: - await self.sync_service.publish_aggregated_attestation(agg) + await publish(agg) logger.info( "Tick: slot=%d interval=%d head=%s finalized=slot%d", diff --git a/src/lean_spec/node/sync/service.py b/src/lean_spec/node/sync/service.py index e815bf12..8131d730 100644 --- a/src/lean_spec/node/sync/service.py +++ b/src/lean_spec/node/sync/service.py @@ -46,10 +46,6 @@ logger = logging.getLogger(__name__) -async def _noop_publish_agg(signed_attestation: SignedAggregatedAttestation) -> None: - """No-op default for aggregated attestation publishing.""" - - @dataclass(slots=True) class SyncService: """Central coordinator for the sync state machine.""" @@ -78,12 +74,12 @@ class SyncService: is_aggregator: bool = field(default=False) """Whether this node functions as an aggregator.""" - publish_aggregated_attestation: Callable[ - [SignedAggregatedAttestation], Coroutine[None, None, None] - ] = field(default=_noop_publish_agg) + publish_aggregated_attestation: ( + Callable[[SignedAggregatedAttestation], Coroutine[None, None, None]] | None + ) = field(default=None) """Async callback for publishing aggregated attestations to the network. - Defaults to a no-op so tests and offline runs do not need a publisher wired. + Defaults to None so tests and offline runs do not need a publisher wired. Assign after construction once NetworkService is built. """ @@ -684,6 +680,9 @@ async def _publish_pending_block_aggregates(self) -> None: return pending = self._pending_block_aggregates self._pending_block_aggregates = [] + # No publisher wired (tests, offline runs): drop the drained aggregates. + if self.publish_aggregated_attestation is None: + return for signed_attestation in pending: await self.publish_aggregated_attestation(signed_attestation) diff --git a/src/lean_spec/node/validator/service.py b/src/lean_spec/node/validator/service.py index db87ca98..d53b4722 100644 --- a/src/lean_spec/node/validator/service.py +++ b/src/lean_spec/node/validator/service.py @@ -65,14 +65,6 @@ """Callback for publishing produced attestations.""" -async def _noop_block_publisher(block: SignedBlock) -> None: # noqa: ARG001 - """Default no-op block publisher.""" - - -async def _noop_attestation_publisher(attestation: SignedAttestation) -> None: # noqa: ARG001 - """Default no-op attestation publisher.""" - - @dataclass(slots=True) class ValidatorService: """ @@ -94,11 +86,17 @@ class ValidatorService: spec: LstarSpec = field(default_factory=LstarSpec) """Fork spec driving consensus methods. Default lets tests skip wiring.""" - on_block: BlockPublisher = field(default=_noop_block_publisher) - """Callback invoked when a block is produced.""" + on_block: BlockPublisher | None = field(default=None) + """Callback invoked when a block is produced. - on_attestation: AttestationPublisher = field(default=_noop_attestation_publisher) - """Callback invoked when an attestation is produced.""" + Defaults to None so tests and offline runs do not need a publisher wired. + """ + + on_attestation: AttestationPublisher | None = field(default=None) + """Callback invoked when an attestation is produced. + + Defaults to None so tests and offline runs do not need a publisher wired. + """ _running: bool = field(default=False, repr=False) """Whether the service is running.""" @@ -314,7 +312,8 @@ async def _maybe_produce_block(self, slot: Slot) -> None: self._blocks_produced += 1 # Emit the block for network propagation. - await self.on_block(signed_block) + if self.on_block is not None: + await self.on_block(signed_block) except AssertionError as e: # Proposer validation failed. @@ -396,7 +395,8 @@ async def _produce_attestations(self, slot: Slot) -> None: ) # Emit the attestation for network propagation. - await self.on_attestation(signed_attestation) + if self.on_attestation is not None: + await self.on_attestation(signed_attestation) def _sign_block( self, diff --git a/tests/lean_spec/node/test_node.py b/tests/lean_spec/node/test_node.py index cda5a558..d6f2ef7b 100644 --- a/tests/lean_spec/node/test_node.py +++ b/tests/lean_spec/node/test_node.py @@ -466,6 +466,7 @@ async def test_block_publish_wrapper_calls_both_services( ) -> None: """Block wrapper publishes to network and processes locally.""" assert node_with_validator.validator_service is not None + assert node_with_validator.validator_service.on_block is not None mock_block = MagicMock() publish_block = AsyncMock() @@ -492,6 +493,7 @@ async def test_attestation_publish_wrapper_calls_both_services( verifies the computed value is forwarded correctly to the network. """ assert node_with_validator.validator_service is not None + assert node_with_validator.validator_service.on_attestation is not None mock_attestation = MagicMock() # The wrapper calls validator_id.compute_subnet_id(ATTESTATION_COMMITTEE_COUNT).