Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions src/lean_spec/node/chain/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 7 additions & 8 deletions src/lean_spec/node/sync/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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.
"""

Expand Down Expand Up @@ -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)

Expand Down
28 changes: 14 additions & 14 deletions src/lean_spec/node/validator/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand All @@ -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."""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions tests/lean_spec/node/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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).
Expand Down
Loading