@@ -290,14 +290,10 @@ def _build_block_from_spec(
290290 """
291291 Build a full SignedBlockWithAttestation from a lightweight BlockSpec.
292292
293- Builds blocks via state transition dry-run, similar to state transition tests,
294- but also creates a proper proposer attestation for fork choice.
295- This mimics what a local block builder would do.
296-
297- TODO: We cannot use Store.produce_block_with_signatures() because it has
298- side effects (adds block to store at lines 556-559 of store.py). If the spec
299- is refactored to separate block production from store updates, we should use
300- that method instead. Until then, this manual approach is necessary.
293+ This method combines:
294+ - spec logic (via the state block building logic),
295+ - test-specific logic (label resolution and signing),
296+ to produce a complete signed block.
301297
302298 Parameters
303299 ----------
@@ -307,87 +303,32 @@ def _build_block_from_spec(
307303 The fork choice store (used to get head state and latest justified).
308304 block_registry : dict[str, Block]
309305 Registry of labeled blocks for fork creation.
306+ key_manager : XmssKeyManager
307+ Key manager for signing attestations.
310308
311309 Returns:
312310 -------
313311 SignedBlockWithAttestation
314312 A complete signed block ready for processing.
315313 """
316- # Determine proposer
317- if spec .proposer_index is None :
318- validator_count = store .states [store .head ].validators .count
319- proposer_index = Uint64 (int (spec .slot ) % int (validator_count ))
320- else :
321- proposer_index = spec .proposer_index
322-
323- # Resolve parent block if parent_label is specified
324- if spec .parent_label is not None :
325- if spec .parent_label not in block_registry :
326- raise ValueError (
327- f"parent_label '{ spec .parent_label } ' not found - "
328- f"available labels: { list (block_registry .keys ())} "
329- )
330- parent_block = block_registry [spec .parent_label ]
331- parent_root = hash_tree_root (parent_block )
332-
333- # Get state at the parent block
334- if parent_root not in store .states :
335- raise ValueError (
336- f"parent_label '{ spec .parent_label } ' (root=0x{ parent_root .hex ()[:16 ]} ...) "
337- f"has no state in store - cannot build on this fork"
338- )
339- parent_state = store .states [parent_root ]
340-
341- # Advance state to the new block's slot
342- temp_state = parent_state .process_slots (spec .slot )
343- else :
344- # Default: build on current head
345- head_state = store .states [store .head ]
346- temp_state = head_state .process_slots (spec .slot )
347- parent_root = hash_tree_root (temp_state .latest_block_header )
348-
349- # Prepare attestations from spec if provided
350- attestations = []
351- attestation_signatures = []
352- if spec .attestations is not None :
353- for attestation in spec .attestations :
354- if isinstance (attestation , SignedAttestationSpec ):
355- # Use the parent state's latest_justified for source checkpoint
356- parent_state = store .states [parent_root ]
357- signed_attestation = self ._build_signed_attestation_from_spec (
358- attestation , block_registry , parent_state
359- )
360- # Extract the Attestation message and signature
361- attestations .append (signed_attestation .message )
362- attestation_signatures .append (signed_attestation .signature )
363- else :
364- # Already a SignedAttestation, extract the message
365- attestations .append (attestation .message )
366- attestation_signatures .append (attestation .signature )
367-
368- # Build block with collected attestations
369- body = BlockBody (attestations = Attestations (data = attestations ))
370-
371- # Create temporary block for dry-run
372- temp_block = Block (
373- slot = spec .slot ,
374- proposer_index = proposer_index ,
375- parent_root = parent_root ,
376- state_root = Bytes32 .zero (),
377- body = body ,
314+ # Determine proposer index
315+ proposer_index = spec .proposer_index or Uint64 (
316+ int (spec .slot ) % store .states [store .head ].validators .count
378317 )
379318
380- # Process to get correct state root
381- post_state = temp_state .process_block (temp_block )
382- correct_state_root = hash_tree_root (post_state )
319+ # Resolve parent root from label or default to head
320+ parent_root = self ._resolve_parent_root (spec , store , block_registry )
383321
384- # Create final block
385- final_block = Block (
322+ # Build attestations from spec
323+ attestations = self ._build_attestations_from_spec (spec , store , block_registry , parent_root )
324+
325+ # Use State.build_block for core block building (pure spec logic)
326+ parent_state = store .states [parent_root ]
327+ final_block , _ , _ , _ = parent_state .build_block (
386328 slot = spec .slot ,
387329 proposer_index = proposer_index ,
388330 parent_root = parent_root ,
389- state_root = correct_state_root ,
390- body = body ,
331+ attestations = attestations ,
391332 )
392333
393334 # Create proposer attestation for this block
@@ -398,17 +339,13 @@ def _build_block_from_spec(
398339 slot = spec .slot ,
399340 head = Checkpoint (root = block_root , slot = spec .slot ),
400341 target = Checkpoint (root = block_root , slot = spec .slot ),
401- # Use the anchor block as source for genesis case
402- source = Checkpoint (root = parent_root , slot = temp_state .latest_block_header .slot ),
342+ source = Checkpoint (root = parent_root , slot = parent_state .latest_block_header .slot ),
403343 ),
404344 )
405345
406346 # Sign all attestations and the proposer attestation
407- signature_list = []
408- for attestation in final_block .body .attestations :
409- signature_list .append (key_manager .sign_attestation (attestation ))
410- proposer_attestation_signature = key_manager .sign_attestation (proposer_attestation )
411- signature_list .append (proposer_attestation_signature )
347+ signature_list = [key_manager .sign_attestation (att ) for att in attestations ]
348+ signature_list .append (key_manager .sign_attestation (proposer_attestation ))
412349
413350 return SignedBlockWithAttestation (
414351 message = BlockWithAttestation (
@@ -418,6 +355,72 @@ def _build_block_from_spec(
418355 signature = BlockSignatures (data = signature_list ),
419356 )
420357
358+ def _resolve_parent_root (
359+ self ,
360+ spec : BlockSpec ,
361+ store : Store ,
362+ block_registry : dict [str , Block ],
363+ ) -> Bytes32 :
364+ """
365+ Resolve parent root from BlockSpec.
366+ - If parent_label is specified, look it up in the registry.
367+ - Otherwise, default to the current head's parent.
368+ """
369+ # Fast path: no label means build on current head.
370+ if not (label := spec .parent_label ):
371+ return store .head
372+
373+ # Label was provided: look up the block in the registry.
374+ if not (parent_block := block_registry .get (label )):
375+ raise ValueError (f"Parent label '{ label } ' not found. Available: { list (block_registry )} " )
376+
377+ # Compute the SSZ root of the parent block.
378+ #
379+ # This root serves as both:
380+ # - The key to look up the parent's post-state in the store
381+ # - The value to place in the new block's `parent_root` field
382+ parent_root = hash_tree_root (parent_block )
383+
384+ # Verify the parent's state exists in the store.
385+ #
386+ # Building a block requires the parent's post-state to:
387+ # - Advance slots via `process_slots()`
388+ # - Apply the new block via `process_block()`
389+ #
390+ # If the state is missing, we cannot proceed.
391+ if parent_root not in store .states :
392+ raise ValueError (
393+ f"Parent '{ label } ' (root=0x{ parent_root .hex ()[:16 ]} ...) "
394+ "has no state in store - cannot build on this fork"
395+ )
396+
397+ return parent_root
398+
399+ def _build_attestations_from_spec (
400+ self ,
401+ spec : BlockSpec ,
402+ store : Store ,
403+ block_registry : dict [str , Block ],
404+ parent_root : Bytes32 ,
405+ ) -> list [Attestation ]:
406+ """Build attestations list from BlockSpec."""
407+ if spec .attestations is None :
408+ return []
409+
410+ parent_state = store .states [parent_root ]
411+ attestations = []
412+
413+ for att_spec in spec .attestations :
414+ if isinstance (att_spec , SignedAttestationSpec ):
415+ signed_att = self ._build_signed_attestation_from_spec (
416+ att_spec , block_registry , parent_state
417+ )
418+ attestations .append (signed_att .message )
419+ else :
420+ attestations .append (att_spec .message )
421+
422+ return attestations
423+
421424 def _build_signed_attestation_from_spec (
422425 self ,
423426 spec : SignedAttestationSpec ,
@@ -473,8 +476,7 @@ def _build_signed_attestation_from_spec(
473476 message = attestation ,
474477 signature = (
475478 spec .signature
476- if spec .signature is not None
477- else Signature (
479+ or Signature (
478480 path = HashTreeOpening (siblings = HashDigestList (data = [])),
479481 rho = Randomness (data = [Fp (0 ) for _ in range (PROD_CONFIG .RAND_LEN_FE )]),
480482 hashes = HashDigestList (data = []),
0 commit comments