2020 ValidatorIndex ,
2121)
2222from lean_spec .spec .forks .lstar .state_transition import attestation_data_matches_chain
23- from lean_spec .spec .ssz import ZERO_HASH , Bytes32 , Uint8
23+ from lean_spec .spec .ssz import ZERO_HASH , Bytes32
2424
2525
2626class BlockProductionMixin (LstarSpecBase ):
@@ -40,23 +40,29 @@ def build_block(
4040
4141 # Overview
4242
43- A proposer packs attestations into a block and records the post-state root.
44- A vote is eligible only if the point it builds from is already justified.
45- Including a vote can justify a new checkpoint.
46- That, in turn, makes further votes eligible.
43+ A proposer fills a block with attestation votes, then records the post-state root.
44+
45+ Selection is circular:
46+
47+ - A vote may only build from an already-justified source.
48+ - Yet including votes is the act that justifies those sources.
49+
50+ So the eligible set grows as votes are added, and the proposer selects in rounds.
4751
4852 # Algorithm
4953
50- Selection runs as a fixed point :
54+ Each round repeats these steps :
5155
52- 1. Anchor on the checkpoint this chain currently treats as justified.
53- 2. Greedily pick proofs covering the most new validators.
54- 3. Apply the state transition to a trial block.
55- 4. If justification or finalization advanced, repeat from the new checkpoint.
56- 5. Stop when a full pass adds nothing.
56+ 1. Pick the eligible proofs covering the most uncounted validators.
57+ 2. Apply the state transition.
58+ 3. Re-anchor on any newly justified checkpoint.
5759
58- The loop is bounded: justification and finalization only move forward,
59- and the set of chosen entries only grows.
60+ The rounds stop once a pass adds nothing.
61+
62+ # Why it terminates
63+
64+ Justification and finalization only move forward, and the chosen set only grows.
65+ Both are bounded, so the rounds must end.
6066
6167 Args:
6268 state: Pre-state the block builds on.
@@ -73,12 +79,14 @@ def build_block(
7379 aggregated_attestations : list [AggregatedAttestation ] = []
7480 aggregated_signatures : list [SingleMessageAggregate ] = []
7581
82+ # Advance the pre-state to this block's slot once.
83+ advanced_state = self .process_slots (state , slot )
84+
7685 if aggregated_payloads :
7786 # Anchor on the checkpoint this chain treats as justified.
7887 #
79- # Building directly on genesis is special.
80- # Header processing justifies the parent at slot 0.
81- # Anchor on that same checkpoint so eligible sources match.
88+ # On genesis the parent is justified at slot 0 by header processing.
89+ # Anchor there so eligible sources match.
8290 current_justified_checkpoint = (
8391 Checkpoint (slot = Slot (0 ), root = parent_root )
8492 if state .latest_block_header .slot == Slot (0 )
@@ -99,14 +107,20 @@ def build_block(
99107 # 1. History up to the parent.
100108 # 2. The parent root at its own slot.
101109 # 3. A zero hash for each slot skipped before this block.
102- # 4. Source and target roots are validated against this view.
110+ #
111+ # Source and target roots are validated against this view.
103112 num_empty_slots = int (slot - state .latest_block_header .slot - Slot (1 ))
104113 extended_historical_block_hashes : list [Bytes32 ] = (
105114 list (state .historical_block_hashes ) + [parent_root ] + [ZERO_HASH ] * num_empty_slots
106115 )
107116
108117 processed_attestation_data : set [AttestationData ] = set ()
109118
119+ # Order candidates by target slot, once.
120+ candidates_in_target_slot_order = sorted (
121+ aggregated_payloads .items (), key = lambda item : item [0 ].target .slot
122+ )
123+
110124 # Fixed-point selection.
111125 #
112126 # - Each pass scans every candidate once, in target-slot order.
@@ -115,17 +129,13 @@ def build_block(
115129 while True :
116130 found_new_entries = False
117131
118- # Visit candidates in target-slot order.
119- # Earlier targets justify first and unlock later ones.
120- for attestation_data , proofs in sorted (
121- aggregated_payloads .items (), key = lambda item : item [0 ].target .slot
122- ):
132+ for attestation_data , proofs in candidates_in_target_slot_order :
123133 if attestation_data in processed_attestation_data :
124134 continue
125135
126136 # Stop once the block holds the maximum distinct data entries.
127137 # This cap is a proposer-side budget, not a consensus rule.
128- if Uint8 ( len (processed_attestation_data )) >= MAX_ATTESTATIONS_DATA :
138+ if len (processed_attestation_data ) >= int ( MAX_ATTESTATIONS_DATA ) :
129139 break
130140
131141 # Skip votes whose head block the proposer has not seen.
@@ -154,14 +164,14 @@ def build_block(
154164 # - Including them propagates them to peers.
155165 # - Slot 0 counts as justified, so the next check would drop them.
156166 # - This flag lets them through.
157- is_genesis_self_vote = attestation_data .source .slot == Slot (0 ) and (
158- attestation_data .target .slot == Slot (0 )
159- )
167+ source_at_genesis = attestation_data .source .slot == Slot (0 )
168+ target_at_genesis = attestation_data .target .slot == Slot (0 )
169+ is_genesis_self_vote = source_at_genesis and target_at_genesis
160170
161171 # Skip votes whose target slot is already justified.
162172 #
163- # A justified target gains nothing from more votes.
164- # Genesis self-votes are exempt, kept for their head weight.
173+ # - A justified target gains nothing from more votes.
174+ # - Genesis self-votes are exempt, kept for their head weight.
165175 if not is_genesis_self_vote and current_justified_slots .is_slot_justified (
166176 current_finalized_slot , attestation_data .target .slot
167177 ):
@@ -194,11 +204,11 @@ def build_block(
194204 state_root = Bytes32 .zero (),
195205 body = self .block_body_class (
196206 attestations = self .aggregated_attestations_class (
197- data = list ( aggregated_attestations )
207+ data = aggregated_attestations
198208 )
199209 ),
200210 )
201- post_state = self .process_block (self . process_slots ( state , slot ) , candidate_block )
211+ post_state = self .process_block (advanced_state , candidate_block )
202212
203213 # Repeat only if justification or finalization moved.
204214 #
@@ -212,9 +222,10 @@ def build_block(
212222 current_justified_checkpoint = post_state .latest_justified
213223 current_justified_slots = post_state .justified_slots
214224 current_finalized_slot = post_state .latest_finalized .slot
215- # The chain view never changes between passes.
216- # Earlier block hashes are fixed once written.
217- # Attestation processing does not rewrite them.
225+
226+ # Re-anchoring needs no other rebuilds.
227+ # The justified window still covers every slot the loop queries.
228+ # The chain view is fixed once written, never recomputed.
218229 continue
219230
220231 break
@@ -284,7 +295,7 @@ def build_block(
284295 #
285296 # Merging proofs keeps the same voters, so the post-state is unchanged.
286297 # Only the body's shape differs, so just the root is needed.
287- post_state = self .process_block (self . process_slots ( state , slot ) , final_block )
298+ post_state = self .process_block (advanced_state , final_block )
288299 final_block = final_block .model_copy (update = {"state_root" : hash_tree_root (post_state )})
289300
290301 return final_block , post_state , aggregated_attestations , aggregated_signatures
0 commit comments