@@ -22,7 +22,7 @@ class ChainService:
2222 """Sync service whose store we tick."""
2323
2424 clock : SlotClock
25- """Clock for time calculation ."""
25+ """Source of wall-clock time and interval boundaries ."""
2626
2727 spec : LstarSpec = field (default_factory = LstarSpec )
2828 """Fork spec driving consensus methods."""
@@ -34,40 +34,27 @@ async def run(self) -> None:
3434 """Tick the store forward at each interval boundary, until stopped."""
3535 self ._running = True
3636
37- # Catch up store time to current wall clock.
38- # - Before genesis this returns nothing; the loop handles the wait.
39- # - After genesis this keeps attestation validation from rejecting valid votes.
37+ # Catch the store time up to the wall clock before looping.
38+ # Otherwise stale store time makes attestation validation reject valid votes.
4039 last_handled_total_interval = await self ._initial_tick ()
4140
4241 while self ._running :
43- # Wait for genesis if we are before it.
44- # The clock sleeps exactly until genesis when called before it.
45- if self .clock .current_time () < self .clock .genesis_time :
46- await self .clock .sleep_until_next_interval ()
47- continue
48-
4942 total_interval = self .clock .total_intervals ()
5043
51- # Already handled this interval: sleep to the next boundary.
52- already_handled = (
53- last_handled_total_interval is not None
54- and total_interval <= last_handled_total_interval
55- )
56- if already_handled :
44+ # Wait when no new interval is due yet.
45+ # Before genesis the clock sleeps straight through to it.
46+ # A frozen clock past startup reports the same interval, so keep waiting.
47+ if (
48+ self .clock .current_time () < self .clock .genesis_time
49+ or total_interval <= last_handled_total_interval
50+ ):
5751 await self .clock .sleep_until_next_interval ()
58- if not self ._running :
59- break
60- # Time may not have advanced during the sleep.
61- # Skip this iteration to avoid ticking the same interval twice.
62- total_interval = self .clock .total_intervals ()
63- if total_interval <= last_handled_total_interval :
64- continue
65-
66- # Advance the store to the current interval.
67- # This service never proposes; block production needs validator keys.
52+ continue
53+
54+ # This service only follows the chain; proposing needs validator keys.
6855 new_aggregated_attestations = await self ._tick_to (total_interval )
6956
70- # No publisher is wired in tests or offline runs , so guard on its presence.
57+ # Offline runs and tests wire no publisher , so guard on its presence.
7158 publish = self .sync_service .publish_aggregated_attestation
7259 if new_aggregated_attestations and publish is not None :
7360 for aggregate in new_aggregated_attestations :
@@ -85,23 +72,17 @@ async def run(self) -> None:
8572
8673 async def _tick_to (self , target_interval : Interval ) -> list [SignedAggregatedAttestation ]:
8774 """
88- Advance the store to the target interval, skipping stale work and yielding.
89-
90- When the node falls behind by more than one slot, stale intervals are skipped.
91- Processing every missed interval synchronously blocks the event loop.
92- That starves gossip and pushes the node further behind.
75+ Advance the store to the target interval, one interval at a time.
9376
94- Between remaining ticks, yield so gossip messages can be processed.
95- Update the sync service store after each tick so gossip handlers see current time.
96-
97- Returns aggregated attestations produced during the ticks.
77+ Skip stale intervals when far behind so synchronous ticking never starves gossip.
78+ Returns the aggregated attestations produced along the way.
9879 """
9980 store = self .sync_service .store
10081 all_new_aggregates : list [SignedAggregatedAttestation ] = []
10182
10283 # The target comes from the wall clock, which can step backward.
10384 # NTP slew, a leap second, or a VM migration can move it before the store time.
104- # A backward target ticks nothing, so return the empty result without ticking .
85+ # A backward target would tick nothing, so return early .
10586 if target_interval <= store .time :
10687 return []
10788
@@ -111,13 +92,14 @@ async def _tick_to(self, target_interval: Interval) -> list[SignedAggregatedAtte
11192 # That preserves aggregation, safe target, and attestation acceptance.
11293 #
11394 # Acceptance for the jumped slots waits for the final slot's tick.
114- # That is safe: acceptance is a monotone pool merge, and the head recomputes from scratch.
95+ # That is safe: acceptance only merges into a monotone pool.
96+ # The head recomputes from scratch, so nothing is lost by waiting.
11597 if target_interval - store .time > Interval (INTERVALS_PER_SLOT ):
11698 store = store .model_copy (
11799 update = {"time" : target_interval - Interval (INTERVALS_PER_SLOT )}
118100 )
119101
120- # Tick remaining intervals one at a time.
102+ # Tick the remaining intervals one at a time.
121103 while store .time < target_interval :
122104 store , new_aggregates = self .spec .tick_interval (
123105 store ,
@@ -134,30 +116,19 @@ async def _tick_to(self, target_interval: Interval) -> list[SignedAggregatedAtte
134116
135117 return all_new_aggregates
136118
137- async def _initial_tick (self ) -> Interval | None :
138- """Catch up store time to wall clock at startup."""
139- current_time = self .clock .current_time ()
140-
141- # Only tick once past genesis.
142- if current_time >= self .clock .genesis_time :
143- target_interval = self .clock .total_intervals ()
144-
145- # Reuse the skip-and-yield path for catch-up.
146- # Discard aggregated attestations from catch-up.
147- # During initial sync we may be many slots behind.
148- # Publishing stale aggregations would spam the network.
149- await self ._tick_to (target_interval )
150-
151- return target_interval
119+ async def _initial_tick (self ) -> Interval :
120+ """Catch up store time to the wall clock at startup."""
121+ if self .clock .current_time () < self .clock .genesis_time :
122+ return Interval (0 )
152123
153- return None
124+ # Discard the aggregates: at startup we may be many slots behind.
125+ # Publishing those stale aggregations would spam the network.
126+ target_interval = self .clock .total_intervals ()
127+ await self ._tick_to (target_interval )
128+ return target_interval
154129
155130 def stop (self ) -> None :
156- """
157- Stop the service.
158-
159- The loop exits after its current sleep cycle finishes.
160- """
131+ """Stop the service; the loop exits after its current sleep finishes."""
161132 self ._running = False
162133
163134 @property
0 commit comments