Skip to content

Commit 4b3e905

Browse files
authored
state: better documentation and touchups (#224)
* state: better documentation and touchups * small improvements * small touchups * touchups
1 parent aae2d9a commit 4b3e905

File tree

1 file changed

+78
-37
lines changed
  • src/lean_spec/subspecs/containers/state

1 file changed

+78
-37
lines changed

src/lean_spec/subspecs/containers/state/state.py

Lines changed: 78 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
"""
2-
State Container for the Lean Ethereum consensus specification.
3-
4-
The state contains everything needed for consensus. It tracks the current slot,
5-
recent blocks, and validator attestations. State also records which blocks are
6-
justified and finalized.
7-
"""
1+
"""State Container for the Lean Ethereum consensus specification."""
82

93
from typing import TYPE_CHECKING, AbstractSet, Iterable
104

@@ -173,7 +167,6 @@ def process_slots(self, target_slot: Slot) -> "State":
173167
#
174168
# 2. Slot Increment:
175169
# It always increments the slot number by one.
176-
#
177170
state = state.model_copy(
178171
update={
179172
"latest_block_header": (
@@ -223,58 +216,107 @@ def process_block_header(self, block: Block) -> "State":
223216
If any header check fails.
224217
"""
225218
# Validation
219+
#
220+
# - Retrieve the header of the previous block (the parent).
221+
# - Compute the parent root hash.
226222
parent_header = self.latest_block_header
227223
parent_root = hash_tree_root(parent_header)
228224

229-
# The block must be for the current slot.
225+
# Consensus checks
226+
227+
# Verify the block corresponds to the current state slot.
228+
#
229+
# To move to this slot, we have processed any intermediate slots before.
230230
assert block.slot == self.slot, "Block slot mismatch"
231231

232232
# The block must be newer than the current latest header.
233233
assert block.slot > parent_header.slot, "Block is older than latest header"
234234

235-
# The proposer must be the expected validator for this slot.
235+
# Verify the block proposer.
236+
#
237+
# Ensures the block was proposed by the assigned validator for this round.
236238
assert is_proposer(
237239
validator_index=block.proposer_index,
238240
slot=self.slot,
239241
num_validators=Uint64(self.validators.count),
240242
), "Incorrect block proposer"
241243

242-
# The declared parent must match the hash of the latest block header.
244+
# Verify the chain link.
245+
#
246+
# The block must cryptographically point to the known parent.
243247
assert block.parent_root == parent_root, "Block parent root mismatch"
244248

245-
# State Updates
249+
# Checkpoint Updates
246250

247-
# Special case: first block after genesis.
251+
# Detect if we are transitioning from the genesis block.
252+
#
253+
# This flag is True only when processing the very first block of the chain.
254+
# This means the parent is the Genesis block (Slot 0).
248255
is_genesis_parent = parent_header.slot == Slot(0)
249-
new_latest_justified = (
250-
self.latest_justified.model_copy(update={"root": parent_root})
251-
if is_genesis_parent
252-
else self.latest_justified
253-
)
254-
new_latest_finalized = (
255-
self.latest_finalized.model_copy(update={"root": parent_root})
256-
if is_genesis_parent
257-
else self.latest_finalized
258-
)
259256

260-
# If there were empty slots between parent and this block, fill them.
257+
# Update the consensus checkpoints.
258+
#
259+
# This logic acts as the trust anchor for the chain:
260+
#
261+
# - If the parent is the Genesis block: It cannot receive votes as it
262+
# precedes the start of the chain. Therefore, we explicitly force it
263+
# to be Justified and Finalized immediately.
264+
#
265+
# - For all other blocks: We retain the existing checkpoints. Future
266+
# updates rely entirely on validator attestations which are processed
267+
# later in the block body.
268+
if is_genesis_parent:
269+
new_latest_justified = self.latest_justified.model_copy(update={"root": parent_root})
270+
new_latest_finalized = self.latest_finalized.model_copy(update={"root": parent_root})
271+
else:
272+
new_latest_justified = self.latest_justified
273+
new_latest_finalized = self.latest_finalized
274+
275+
# Historical Data Management
276+
277+
# Calculate the gap between the parent and the current block.
278+
#
279+
# If slots were skipped (missed proposals), we must record them.
280+
#
281+
# Formula: (Current - Parent - 1). Adjacent blocks have a gap of 0.
261282
num_empty_slots = (block.slot - parent_header.slot - Slot(1)).as_int()
262283

263-
# Build new historical hashes list
284+
# Update the list of historical block roots.
285+
#
286+
# Structure: [Existing history] + [Parent root] + [Zero hash for gaps]
264287
new_historical_hashes_data = (
265-
self.historical_block_hashes + [parent_root] + ([ZERO_HASH] * num_empty_slots)
288+
self.historical_block_hashes + [parent_root] + [ZERO_HASH] * num_empty_slots
266289
)
267290

268-
# Build new justified slots list
291+
# Update the list of justified slot flags.
292+
#
293+
# Structure: [Existing flags] + [Is genesis parent?] + [False for gaps]
294+
#
295+
# We construct the new history list by concatenating three segments:
296+
#
297+
# 1. The existing history:
298+
# We preserve the flags for all previously processed blocks.
299+
#
300+
# 2. The parent block status (one entry):
301+
# We append the status of the block immediately preceding any gaps.
302+
# - If Genesis: True (Justified by definition).
303+
# - If Normal: False (Pending). It remains unjustified until validators
304+
# vote for it later in the process.
305+
#
306+
# 3. The skipped slots status (multiple entries):
307+
# We append False for every empty slot between the parent and the
308+
# current block. Since no blocks exist there, they are permanently
309+
# unjustified.
269310
new_justified_slots_data = (
270-
self.justified_slots
271-
+ [Boolean(is_genesis_parent)]
272-
+ ([Boolean(False)] * num_empty_slots)
311+
self.justified_slots + [Boolean(is_genesis_parent)] + [Boolean(False)] * num_empty_slots
273312
)
274313

275314
# Construct the new latest block header.
276315
#
277-
# Leave state_root empty; it will be filled on the next process_slot call.
316+
# The new header object represents the tip of the chain.
317+
#
318+
# Leave state root empty.
319+
# It is not computed until the block body is fully processed or the next slot begins.
278320
new_header = BlockHeader(
279321
slot=block.slot,
280322
proposer_index=block.proposer_index,
@@ -285,15 +327,14 @@ def process_block_header(self, block: Block) -> "State":
285327

286328
# Final Immutable Copy
287329
#
288-
# Return the state with all header updates applied in one go.
330+
# Return a new immutable state instance.
331+
# All calculated updates are applied atomically here.
289332
return self.model_copy(
290333
update={
291334
"latest_justified": new_latest_justified,
292335
"latest_finalized": new_latest_finalized,
293-
"historical_block_hashes": self.historical_block_hashes.__class__(
294-
data=new_historical_hashes_data
295-
),
296-
"justified_slots": self.justified_slots.__class__(data=new_justified_slots_data),
336+
"historical_block_hashes": HistoricalBlockHashes(data=new_historical_hashes_data),
337+
"justified_slots": JustifiedSlots(data=new_justified_slots_data),
297338
"latest_block_header": new_header,
298339
}
299340
)
@@ -472,7 +513,7 @@ def process_attestations(
472513
update={
473514
"justifications_roots": justifications_roots,
474515
"justifications_validators": justifications_validators,
475-
"justified_slots": self.justified_slots.__class__(data=justified_slots),
516+
"justified_slots": JustifiedSlots(data=justified_slots),
476517
"latest_justified": latest_justified,
477518
"latest_finalized": latest_finalized,
478519
}

0 commit comments

Comments
 (0)