Skip to content

Commit 019d3cb

Browse files
committed
Merge remote-tracking branch 'origin/main' into restore-blocks-finalized-endpoint
# Conflicts: # src/lean_spec/node/sync/checkpoint_sync.py
2 parents 0351ea5 + 03e946a commit 019d3cb

27 files changed

Lines changed: 864 additions & 2519 deletions

‎src/lean_spec/node/anchor.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,12 @@ async def from_checkpoint(
125125

126126
# The protocol return type is structural, but only one concrete store ships.
127127
store = cast(Store, fork.create_store(state, signed_block.block, validator_index))
128-
head_slot = store.blocks[store.head].slot
129128

130129
return cls(
131130
validators=state.validators,
132131
store=store,
133132
initial_status=Status(
134133
finalized=store.latest_finalized,
135-
head=Checkpoint(root=store.head, slot=head_slot),
134+
head=Checkpoint(root=store.head, slot=store.blocks[store.head].slot),
136135
),
137136
)

‎src/lean_spec/node/chain/service.py‎

Lines changed: 31 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -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

‎src/lean_spec/node/storage/__init__.py‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
"""
2-
Storage module for persistent block and state storage.
3-
4-
Provides database abstraction for consensus data persistence.
5-
Uses SQLite for simplicity and correctness.
6-
"""
1+
"""Persistent storage for consensus blocks, states, and checkpoints."""
72

83
from lean_spec.node.storage.database import Database
94
from lean_spec.node.storage.exceptions import (

0 commit comments

Comments
 (0)