|
14 | 14 | ) |
15 | 15 | from lean_spec.subspecs.containers import ( |
16 | 16 | Block, |
| 17 | + BlockBody, |
17 | 18 | Checkpoint, |
18 | 19 | Config, |
19 | 20 | SignedVote, |
20 | 21 | State, |
| 22 | + Vote, |
21 | 23 | ) |
22 | 24 | from lean_spec.subspecs.containers.slot import Slot |
23 | 25 | from lean_spec.subspecs.ssz.hash import hash_tree_root |
24 | | -from lean_spec.types import Bytes32, Uint64, ValidatorIndex |
| 26 | +from lean_spec.types import Bytes32, Uint64, ValidatorIndex, is_proposer |
25 | 27 | from lean_spec.types.container import Container |
26 | 28 |
|
27 | 29 | from .helpers import get_fork_choice_head, get_latest_justified |
@@ -362,3 +364,152 @@ def get_vote_target(self) -> Checkpoint: |
362 | 364 |
|
363 | 365 | target_block = self.blocks[target_block_root] |
364 | 366 | return Checkpoint(root=hash_tree_root(target_block), slot=target_block.slot) |
| 367 | + |
| 368 | + def produce_block(self, slot: Slot, validator_index: ValidatorIndex) -> Block: |
| 369 | + """ |
| 370 | + Produce a new block for the given slot and validator. |
| 371 | +
|
| 372 | + Algorithm Overview: |
| 373 | + 1. Validate proposer authorization for the target slot |
| 374 | + 2. Get the current chain head as the parent block |
| 375 | + 3. Iteratively build attestation set: |
| 376 | + - Create candidate block with current attestations |
| 377 | + - Apply state transition (slot advancement + block processing) |
| 378 | + - Find new valid attestations matching post-state requirements |
| 379 | + - Continue until no new attestations can be added |
| 380 | + 4. Finalize block with computed state root and store it |
| 381 | +
|
| 382 | + Args: |
| 383 | + slot: Target slot number for block production |
| 384 | + validator_index: Index of validator authorized to propose this block |
| 385 | +
|
| 386 | + Returns: |
| 387 | + Complete block with maximal attestation set and valid state root |
| 388 | +
|
| 389 | + Raises: |
| 390 | + AssertionError: If validator lacks proposer authorization for slot |
| 391 | + """ |
| 392 | + # Validate proposer authorization for this slot |
| 393 | + if not is_proposer(validator_index, slot, self.config.num_validators): |
| 394 | + msg = f"Validator {validator_index} is not the proposer for slot {slot}" |
| 395 | + raise AssertionError(msg) |
| 396 | + |
| 397 | + # Get parent block and state to build upon |
| 398 | + head_root = self.get_proposal_head(slot) |
| 399 | + head_state = self.states[head_root] |
| 400 | + |
| 401 | + # Initialize empty attestation set for iterative collection |
| 402 | + attestations: list[SignedVote] = [] |
| 403 | + |
| 404 | + # Iteratively collect valid attestations using fixed-point algorithm |
| 405 | + # |
| 406 | + # Continue until no new attestations can be added to the block |
| 407 | + while True: |
| 408 | + # Create candidate block with current attestation set |
| 409 | + candidate_block = Block( |
| 410 | + slot=slot, |
| 411 | + proposer_index=validator_index, |
| 412 | + parent_root=head_root, |
| 413 | + state_root=Bytes32.zero(), # Temporary; updated after state computation |
| 414 | + body=BlockBody(attestations=attestations), |
| 415 | + ) |
| 416 | + |
| 417 | + # Apply state transition to get the post-block state |
| 418 | + # First advance state to target slot, then process the block |
| 419 | + advanced_state = head_state.process_slots(slot) |
| 420 | + post_state = advanced_state.process_block(candidate_block) |
| 421 | + |
| 422 | + # Find new valid attestations matching post-state justification |
| 423 | + new_attestations: list[SignedVote] = [] |
| 424 | + for validator_id, checkpoint in self.latest_known_votes.items(): |
| 425 | + # Skip if target block is unknown in our store |
| 426 | + if checkpoint.root not in self.blocks: |
| 427 | + continue |
| 428 | + |
| 429 | + # Create attestation with post-state's latest justified as source |
| 430 | + vote = Vote( |
| 431 | + validator_id=validator_id, |
| 432 | + slot=checkpoint.slot, |
| 433 | + head=checkpoint, |
| 434 | + target=checkpoint, |
| 435 | + source=post_state.latest_justified, |
| 436 | + ) |
| 437 | + signed_vote = SignedVote(data=vote, signature=Bytes32.zero()) |
| 438 | + |
| 439 | + # Include if not already in attestation set |
| 440 | + if signed_vote not in attestations: |
| 441 | + new_attestations.append(signed_vote) |
| 442 | + |
| 443 | + # Fixed point reached: no new attestations found |
| 444 | + if not new_attestations: |
| 445 | + break |
| 446 | + |
| 447 | + # Add new attestations and continue iteration |
| 448 | + attestations.extend(new_attestations) |
| 449 | + |
| 450 | + # Create final block with all collected attestations |
| 451 | + final_state = head_state.process_slots(slot) |
| 452 | + final_block = Block( |
| 453 | + slot=slot, |
| 454 | + proposer_index=validator_index, |
| 455 | + parent_root=head_root, |
| 456 | + state_root=Bytes32.zero(), # Will be updated with computed hash |
| 457 | + body=BlockBody(attestations=attestations), |
| 458 | + ) |
| 459 | + |
| 460 | + # Apply state transition to get final post-state and compute state root |
| 461 | + final_post_state = final_state.process_block(final_block) |
| 462 | + finalized_block = final_block.model_copy( |
| 463 | + update={"state_root": hash_tree_root(final_post_state)} |
| 464 | + ) |
| 465 | + |
| 466 | + # Store block and state in forkchoice store |
| 467 | + block_hash = hash_tree_root(finalized_block) |
| 468 | + self.blocks[block_hash] = finalized_block |
| 469 | + self.states[block_hash] = final_post_state |
| 470 | + |
| 471 | + return finalized_block |
| 472 | + |
| 473 | + def produce_attestation_vote(self, slot: Slot, validator_index: ValidatorIndex) -> Vote: |
| 474 | + """ |
| 475 | + Produce an attestation vote for the given slot and validator. |
| 476 | +
|
| 477 | + This method constructs a Vote object according to the lean protocol |
| 478 | + specification for attestation voting. The vote represents the |
| 479 | + validator's view of the chain state and their choice for the |
| 480 | + next justified checkpoint. |
| 481 | +
|
| 482 | + The algorithm: |
| 483 | + 1. Get the current head block for proposal |
| 484 | + 2. Calculate the appropriate vote target using current forkchoice state |
| 485 | + 3. Use the store's latest justified checkpoint as the vote source |
| 486 | + 4. Construct and return the complete Vote object |
| 487 | +
|
| 488 | + Args: |
| 489 | + slot: The slot for which to produce the attestation vote. |
| 490 | + validator_index: The validator index producing the vote. |
| 491 | +
|
| 492 | + Returns: |
| 493 | + A fully constructed Vote object ready for signing and broadcast. |
| 494 | + """ |
| 495 | + # Get the head block the validator sees for this slot |
| 496 | + head_root = self.get_proposal_head(slot) |
| 497 | + head_checkpoint = Checkpoint( |
| 498 | + root=head_root, |
| 499 | + slot=self.blocks[head_root].slot, |
| 500 | + ) |
| 501 | + |
| 502 | + # Calculate the target checkpoint for this vote |
| 503 | + # |
| 504 | + # This uses the store's current forkchoice state to determine |
| 505 | + # the appropriate attestation target |
| 506 | + target_checkpoint = self.get_vote_target() |
| 507 | + |
| 508 | + # Create the vote using current forkchoice state |
| 509 | + return Vote( |
| 510 | + validator_id=validator_index, |
| 511 | + slot=slot, |
| 512 | + head=head_checkpoint, |
| 513 | + target=target_checkpoint, |
| 514 | + source=self.latest_justified, |
| 515 | + ) |
0 commit comments