diff --git a/pysetup/helpers.py b/pysetup/helpers.py index 2cf5b4fe8e..d819a974eb 100644 --- a/pysetup/helpers.py +++ b/pysetup/helpers.py @@ -295,6 +295,7 @@ def combine_dicts(old_dict: dict[str, T], new_dict: dict[str, T]) -> dict[str, T "ProgressiveBitlist", "ProgressiveList", "Sequence", + "set", "Set", "Tuple", "uint128", diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index b01c0c5f14..35c3f4c9da 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -69,6 +69,8 @@ - [New `process_builder_pending_payments`](#new-process_builder_pending_payments) - [New `process_ptc_window`](#new-process_ptc_window) - [Block processing](#block-processing) + - [Parent execution payload](#parent-execution-payload) + - [New `process_parent_execution_payload`](#new-process_parent_execution_payload) - [Withdrawals](#withdrawals) - [New `get_builder_withdrawals`](#new-get_builder_withdrawals) - [New `get_builders_sweep_withdrawals`](#new-get_builders_sweep_withdrawals) @@ -265,6 +267,8 @@ class ExecutionPayloadBid(Container): value: Gwei execution_payment: Gwei blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + # [New in Gloas:EIP7732] + execution_requests_root: Root ``` #### `SignedExecutionPayloadBid` @@ -284,7 +288,6 @@ class ExecutionPayloadEnvelope(Container): builder_index: BuilderIndex beacon_block_root: Root slot: Slot - state_root: Root ``` #### `SignedExecutionPayloadEnvelope` @@ -324,6 +327,8 @@ class BeaconBlockBody(Container): signed_execution_payload_bid: SignedExecutionPayloadBid # [New in Gloas:EIP7732] payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] + # [New in Gloas:EIP7732] + parent_execution_requests: ExecutionRequests ``` #### `BeaconState` @@ -797,12 +802,15 @@ transitions that trigger an unhandled exception (e.g. a failed `assert` or an out-of-range list access) are considered invalid. State transitions that cause a `uint64` overflow or underflow are also considered invalid. -The post-state corresponding to a pre-state `state` and a signed execution -payload envelope `signed_envelope` is defined as -`process_execution_payload(state, signed_envelope, execution_engine)`. State -transitions that trigger an unhandled exception (e.g. a failed `assert` or an -out-of-range list access) are considered invalid. State transitions that cause -an `uint64` overflow or underflow are also considered invalid. +The validity of a signed execution payload envelope `signed_envelope` against a +pre-state `state` is checked by +`process_execution_payload(state, signed_envelope, execution_engine)`. Deferred +effects from the parent payload — execution requests, builder payment, payload +availability, and latest block hash — are applied in the next beacon block via +`process_parent_execution_payload`. Verification failures that trigger an +unhandled exception (e.g. a failed `assert` or an out-of-range list access) are +considered invalid. Verification failures that cause a `uint64` overflow or +underflow are also considered invalid. ### Modified `process_slot` @@ -890,6 +898,8 @@ def process_ptc_window(state: BeaconState) -> None: ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: + # [New in Gloas:EIP7732] + process_parent_execution_payload(state, block) process_block_header(state, block) # [Modified in Gloas:EIP7732] process_withdrawals(state) @@ -904,6 +914,79 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: process_sync_aggregate(state, block.body.sync_aggregate) ``` +#### Parent execution payload + +##### New `process_parent_execution_payload` + +```python +def process_parent_execution_payload(state: BeaconState, block: BeaconBlock) -> None: + """ + Process deferred effects from the parent's execution payload. + Must run first in ``process_block``, before ``process_block_header``, because it reads + ``state.latest_block_header.slot`` and ``state.latest_execution_payload_bid`` + which are overwritten by ``process_block_header`` and ``process_execution_payload_bid``. + """ + bid = block.body.signed_execution_payload_bid.message + parent_bid = state.latest_execution_payload_bid + + # Determine parent payload status from block data + # Note: cannot use is_parent_block_full(state) here because latest_block_hash + # has not been updated yet -- this function is responsible for updating it. + is_parent_full = bid.parent_block_hash == parent_bid.block_hash + + if is_parent_full: + parent_slot = state.latest_block_header.slot + parent_epoch = compute_epoch_at_slot(parent_slot) + current_epoch = get_current_epoch(state) + previous_epoch = get_previous_epoch(state) + + # Mark the parent payload as available before any later state transition logic observes it. + state.execution_payload_availability[parent_slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1 + + # Verify execution requests match the bid commitment + assert ( + hash_tree_root(block.body.parent_execution_requests) + == parent_bid.execution_requests_root + ) + + # Process deferred execution requests from parent's payload + # Note: execution requests observe state.slot (child's slot), not the parent's. + requests = block.body.parent_execution_requests + + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + for_ops(requests.deposits, process_deposit_request) + for_ops(requests.withdrawals, process_withdrawal_request) + for_ops(requests.consolidations, process_consolidation_request) + + # Queue the builder payment + if parent_epoch == current_epoch: + payment_index = SLOTS_PER_EPOCH + parent_slot % SLOTS_PER_EPOCH + payment = state.builder_pending_payments[payment_index] + amount = payment.withdrawal.amount + if amount > 0: + state.builder_pending_withdrawals.append(payment.withdrawal) + state.builder_pending_payments[payment_index] = BuilderPendingPayment() + elif parent_epoch == previous_epoch: + payment_index = parent_slot % SLOTS_PER_EPOCH + payment = state.builder_pending_payments[payment_index] + amount = payment.withdrawal.amount + if amount > 0: + state.builder_pending_withdrawals.append(payment.withdrawal) + state.builder_pending_payments[payment_index] = BuilderPendingPayment() + # Note: if parent is older than previous_epoch, the payment entry + # has already been settled or evicted by process_builder_pending_payments + # at epoch boundaries. No action needed. + + # Update latest block hash + state.latest_block_hash = bid.parent_block_hash + else: + # Parent was EMPTY -- no execution requests expected + assert block.body.parent_execution_requests == ExecutionRequests() +``` + #### Withdrawals ##### New `get_builder_withdrawals` @@ -1545,9 +1628,11 @@ def verify_execution_payload_envelope_signature( #### New `process_execution_payload` -*Note*: `process_execution_payload` is now an independent check in state -transition. It is called when importing a signed execution payload proposed by -the builder of the current slot. +*Note*: `process_execution_payload` is a verification function called by +fork-choice when importing a signed execution payload. It verifies the payload +against the execution engine without processing execution requests or updating +state. Actual state mutations are deferred to `process_parent_execution_payload` +in the next block. ```python def process_execution_payload( @@ -1559,7 +1644,7 @@ def process_execution_payload( execution_engine: ExecutionEngine, # [New in Gloas:EIP7732] verify: bool = True, -) -> None: +) -> ExecutionRequests: envelope = signed_envelope.message payload = envelope.payload @@ -1567,11 +1652,6 @@ def process_execution_payload( if verify: assert verify_execution_payload_envelope_signature(state, signed_envelope) - # Cache latest block header state root - previous_state_root = hash_tree_root(state) - if state.latest_block_header.state_root == Root(): - state.latest_block_header.state_root = previous_state_root - # Verify consistency with the beacon block assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header) assert envelope.slot == state.slot @@ -1595,7 +1675,6 @@ def process_execution_payload( # Verify the execution payload is valid versioned_hashes = [ kzg_commitment_to_versioned_hash(commitment) - # [Modified in Gloas:EIP7732] for commitment in committed_bid.blob_kzg_commitments ] requests = envelope.execution_requests @@ -1608,28 +1687,7 @@ def process_execution_payload( ) ) - def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: - for operation in operations: - fn(state, operation) - - for_ops(requests.deposits, process_deposit_request) - for_ops(requests.withdrawals, process_withdrawal_request) - for_ops(requests.consolidations, process_consolidation_request) - - # Queue the builder payment - payment = state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] - amount = payment.withdrawal.amount - if amount > 0: - state.builder_pending_withdrawals.append(payment.withdrawal) - state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] = ( - BuilderPendingPayment() - ) - - # Cache the execution payload hash - state.execution_payload_availability[state.slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1 - state.latest_block_hash = payload.block_hash - - # Verify the state root - if verify: - assert envelope.state_root == hash_tree_root(state) + # Execution request processing, builder payment queueing, availability updates, + # and latest block hash updates are deferred to the next beacon block. + return requests ``` diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index d9fab6dff7..f720e211f1 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -244,9 +244,7 @@ After setting these parameters, the builder assembles `signed_execution_payload_envelope = SignedExecutionPayloadEnvelope(message=envelope, signature=BLSSignature())`, then verify that the envelope is valid with `process_execution_payload(state, signed_execution_payload_envelope, execution_engine, verify=False)`. -This function should not trigger an exception. - -7. Set `envelope.state_root` to `hash_tree_root(state)`. +This function should not trigger an exception and does not mutate `state`. After preparing the `envelope` the builder should sign the envelope using: diff --git a/specs/gloas/fork-choice.md b/specs/gloas/fork-choice.md index d12b9a341a..cfc732475b 100644 --- a/specs/gloas/fork-choice.md +++ b/specs/gloas/fork-choice.md @@ -122,9 +122,8 @@ def update_latest_messages( ### Modified `Store` -*Note*: `Store` is modified to track the intermediate states of "empty" -consensus blocks, that is, those consensus blocks for which the corresponding -execution payload has not been revealed or has not been included on chain. +*Note*: `Store` is modified to track blocks whose execution payloads have been +verified. ```python @dataclass @@ -146,7 +145,7 @@ class Store(object): latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) # [New in Gloas:EIP7732] - payload_states: Dict[Root, BeaconState] = field(default_factory=dict) + payloads: Set[Root] = field(default_factory=set) # [New in Gloas:EIP7732] payload_timeliness_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict) # [New in Gloas:EIP7732] @@ -181,7 +180,7 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) - checkpoint_states={justified_checkpoint: copy(anchor_state)}, unrealized_justifications={anchor_root: justified_checkpoint}, # [New in Gloas:EIP7732] - payload_states={anchor_root: copy(anchor_state)}, + payloads=set(), # [New in Gloas:EIP7732] payload_timeliness_vote={ anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE)) @@ -230,9 +229,9 @@ def is_payload_timely(store: Store, root: Root) -> bool: # The beacon block root must be known assert root in store.payload_timeliness_vote - # If the payload is not locally available, the payload + # If the payload is not locally verified, the payload # is not considered available regardless of the PTC vote - if root not in store.payload_states: + if root not in store.payloads: return False return sum(store.payload_timeliness_vote[root]) > PAYLOAD_TIMELY_THRESHOLD @@ -249,9 +248,9 @@ def is_payload_data_available(store: Store, root: Root) -> bool: # The beacon block root must be known assert root in store.payload_data_availability_vote - # If the payload is not locally available, the blob data + # If the payload is not locally verified, the blob data # is not considered available regardless of the PTC vote - if root not in store.payload_states: + if root not in store.payloads: return False return sum(store.payload_data_availability_vote[root]) > DATA_AVAILABILITY_TIMELY_THRESHOLD @@ -488,7 +487,7 @@ def get_node_children( ) -> Sequence[ForkChoiceNode]: if node.payload_status == PAYLOAD_STATUS_PENDING: children = [ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_EMPTY)] - if node.root in store.payload_states: + if node.root in store.payloads: children.append(ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_FULL)) return children else: @@ -603,9 +602,9 @@ def validate_on_attestation(store: Store, attestation: Attestation, is_from_bloc if block_slot == attestation.data.slot: assert attestation.data.index == 0 # [New in Gloas:EIP7732] - # If attesting for a full node, the payload must be known + # If attesting for a full node, the payload must be verified if attestation.data.index == 1: - assert attestation.data.beacon_block_root in store.payload_states + assert attestation.data.beacon_block_root in store.payloads # LMD vote must be consistent with FFG vote target assert target.root == get_checkpoint_block( @@ -726,10 +725,11 @@ def get_payload_attestation_due_ms(epoch: Epoch) -> uint64: ### Modified `on_block` -*Note*: The handler `on_block` is modified to consider the pre `state` of the -given consensus beacon block depending not only on the parent block root, but -also on the parent blockhash. In addition we delay the checking of blob data -availability until the processing of the execution payload. +*Note*: The handler `on_block` is modified to verify the parent's deferred +execution requests against the parent bid's `execution_requests_root` +commitment, and to assert that the parent payload has been verified +(`store.payloads`). In addition we delay the checking of blob data availability +until the processing of the execution payload. ```python def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: @@ -744,13 +744,24 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: parent_block = store.blocks[block.parent_root] bid = block.body.signed_execution_payload_bid.message parent_bid = parent_block.body.signed_execution_payload_bid.message - # Make a copy of the state to avoid mutability issues + # [Modified in Gloas:EIP7732] + # Verify parent execution requests against the parent bid commitment if is_parent_node_full(store, block): - assert block.parent_root in store.payload_states - state = copy(store.payload_states[block.parent_root]) + # Finalized parents are trusted without local payload verification + assert ( + block.parent_root in store.payloads + or compute_epoch_at_slot(parent_block.slot) <= store.finalized_checkpoint.epoch + ) + assert ( + hash_tree_root(block.body.parent_execution_requests) + == parent_bid.execution_requests_root + ) else: assert bid.parent_block_hash == parent_bid.parent_block_hash - state = copy(store.block_states[block.parent_root]) + assert block.body.parent_execution_requests == ExecutionRequests() + + # [Modified in Gloas:EIP7732] + state = copy(store.block_states[block.parent_root]) # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. current_slot = get_current_slot(store) @@ -830,14 +841,19 @@ def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEn # If not, this payload MAY be queued and subsequently considered when blob data becomes available assert is_data_available(envelope.beacon_block_root) - # Make a copy of the state to avoid mutability issues + # Verify execution payload on a temporary state copy state = copy(store.block_states[envelope.beacon_block_root]) - - # Process the execution payload + # Cache latest block header state root (process_execution_payload must not mutate state) + if state.latest_block_header.state_root == Root(): + state.latest_block_header.state_root = hash_tree_root(state) process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) - # Add new state for this payload to the store - store.payload_states[envelope.beacon_block_root] = state + # Verify that the execution requests match the bid commitment + bid = state.latest_execution_payload_bid + assert hash_tree_root(envelope.execution_requests) == bid.execution_requests_root + + # Mark this block's execution payload as verified + store.payloads.add(envelope.beacon_block_root) ``` ### New `on_payload_attestation_message` diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index ea0970fb11..6af5b4a555 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -118,6 +118,10 @@ If `state.slot % SLOTS_PER_EPOCH == 0` and `compute_epoch_at_slot(state.slot) == GLOAS_FORK_EPOCH`, an irregular state change is made to upgrade to Gloas. +*Note*: `latest_execution_payload_bid.execution_requests_root` is initialized to +`hash_tree_root(ExecutionRequests())` so the first Gloas block treats its +pre-Gloas parent as having empty deferred execution requests. + ```python def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: epoch = fulu.get_current_epoch(pre) @@ -157,6 +161,7 @@ def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: # [New in Gloas:EIP7732] latest_execution_payload_bid=ExecutionPayloadBid( block_hash=pre.latest_execution_payload_header.block_hash, + execution_requests_root=hash_tree_root(ExecutionRequests()), ), next_withdrawal_index=pre.next_withdrawal_index, next_withdrawal_validator_index=pre.next_withdrawal_validator_index, diff --git a/specs/gloas/p2p-interface.md b/specs/gloas/p2p-interface.md index 417a5b5502..7afc61f26c 100644 --- a/specs/gloas/p2p-interface.md +++ b/specs/gloas/p2p-interface.md @@ -292,6 +292,12 @@ And instead the following validations are set in place with the alias `bid.parent_block_hash`) passes all validation. - [REJECT] The bid's parent (defined by `bid.parent_block_root`) equals the block's parent (defined by `block.parent_root`). +- _[REJECT]_ If the parent block was FULL (i.e. + `bid.parent_block_hash == parent_bid.block_hash` where `parent_bid` is the + parent block's `signed_execution_payload_bid.message`), verify that + `hash_tree_root(block.body.parent_execution_requests) == parent_bid.execution_requests_root`. +- _[REJECT]_ If the parent block was EMPTY, verify that + `block.body.parent_execution_requests == ExecutionRequests()`. ###### `execution_payload` @@ -303,11 +309,11 @@ The following validations MUST pass before forwarding the `envelope = signed_execution_payload_envelope.message`, `payload = envelope.payload`: -- _[IGNORE]_ The envelope's block root `envelope.block_root` has been seen (via - gossip or non-gossip sources) (a client MAY queue payload for processing once - the block is retrieved). +- _[IGNORE]_ The envelope's beacon block root `envelope.beacon_block_root` has + been seen (via gossip or non-gossip sources) (a client MAY queue payload for + processing once the block is retrieved). - _[IGNORE]_ The node has not seen another valid - `SignedExecutionPayloadEnvelope` for this block root from this builder. + `SignedExecutionPayloadEnvelope` for this beacon block root from this builder. - _[IGNORE]_ The envelope is from a slot greater than or equal to the latest finalized slot -- i.e. validate that `envelope.slot >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)` @@ -320,6 +326,8 @@ obtained from the `state.latest_execution_payload_bid`) - _[REJECT]_ `block.slot` equals `envelope.slot`. - _[REJECT]_ `envelope.builder_index == bid.builder_index` - _[REJECT]_ `payload.block_hash == bid.block_hash` +- _[REJECT]_ + `hash_tree_root(envelope.execution_requests) == bid.execution_requests_root`. - _[REJECT]_ `signed_execution_payload_envelope.signature` is valid as verified by `verify_execution_payload_envelope_signature`. diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 18c013bdf4..6fb7e12bae 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -18,6 +18,7 @@ - [Constructing the `BeaconBlockBody`](#constructing-the-beaconblockbody) - [Signed execution payload bid](#signed-execution-payload-bid) - [Payload attestations](#payload-attestations) + - [Parent execution requests](#parent-execution-requests) - [ExecutionPayload](#executionpayload) - [Payload timeliness attestation](#payload-timeliness-attestation) - [Constructing the `PayloadAttestationMessage`](#constructing-the-payloadattestationmessage) @@ -219,6 +220,19 @@ construct the `payload_attestations` field in `BeaconBlockBody`: indices with respect to the PTC that is obtained from `get_ptc(state, Slot(block_slot - 1))`. +##### Parent execution requests + +The `parent_execution_requests` field contains the execution requests from the +parent's execution payload. The proposer constructs this field as follows: + +- If the parent block is pre-Gloas (first Gloas block), set + `parent_execution_requests` to an empty `ExecutionRequests()`. +- If the parent block was FULL (i.e., the execution payload was delivered and + verified), set `parent_execution_requests` to the `ExecutionRequests` from the + parent's `ExecutionPayloadEnvelope`. +- If the parent block was EMPTY (i.e., no execution payload was delivered), set + `parent_execution_requests` to an empty `ExecutionRequests()`. + ##### ExecutionPayload ```python diff --git a/specs/heze/beacon-chain.md b/specs/heze/beacon-chain.md index a3a2a63f38..9a26bbb5fa 100644 --- a/specs/heze/beacon-chain.md +++ b/specs/heze/beacon-chain.md @@ -87,6 +87,7 @@ class ExecutionPayloadBid(Container): value: Gwei execution_payment: Gwei blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + execution_requests_root: Root # [New in Heze:EIP7805] inclusion_list_bits: Bitvector[INCLUSION_LIST_COMMITTEE_SIZE] ``` diff --git a/specs/heze/fork-choice.md b/specs/heze/fork-choice.md index 9331b37384..b1aabab0a4 100644 --- a/specs/heze/fork-choice.md +++ b/specs/heze/fork-choice.md @@ -128,7 +128,7 @@ class Store(object): checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) - payload_states: Dict[Root, BeaconState] = field(default_factory=dict) + payloads: Set[Root] = field(default_factory=set) payload_timeliness_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict) payload_data_availability_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field( default_factory=dict @@ -161,7 +161,7 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) - block_timeliness={anchor_root: [True, True]}, checkpoint_states={justified_checkpoint: copy(anchor_state)}, unrealized_justifications={anchor_root: justified_checkpoint}, - payload_states={anchor_root: copy(anchor_state)}, + payloads=set(), payload_timeliness_vote={ anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE)) }, @@ -213,7 +213,7 @@ def is_payload_inclusion_list_satisfied(store: Store, root: Root) -> bool: # If the payload is not locally available, the payload # is not considered to satisfy the inclusion list constraints - if root not in store.payload_states: + if root not in store.payloads: return False return store.payload_inclusion_list_satisfaction[root] @@ -300,12 +300,17 @@ def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEn # If not, this payload MAY be queued and subsequently considered when blob data becomes available assert is_data_available(envelope.beacon_block_root) - # Make a copy of the state to avoid mutability issues + # Verify execution payload on a temporary state copy state = copy(store.block_states[envelope.beacon_block_root]) - - # Process the execution payload + # Cache latest block header state root (process_execution_payload must not mutate state) + if state.latest_block_header.state_root == Root(): + state.latest_block_header.state_root = hash_tree_root(state) process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) + # Verify that the execution requests match the bid commitment + bid = state.latest_execution_payload_bid + assert hash_tree_root(envelope.execution_requests) == bid.execution_requests_root + # [New in Heze:EIP7805] # Check if this payload satisfies the inclusion list constraints # If not, add this payload to the store as inclusion list constraints unsatisfied @@ -313,6 +318,6 @@ def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEn store, state, envelope.beacon_block_root, envelope.payload, EXECUTION_ENGINE ) - # Add new state for this payload to the store - store.payload_states[envelope.beacon_block_root] = state + # Mark this block's execution payload as verified + store.payloads.add(envelope.beacon_block_root) ``` diff --git a/specs/heze/fork.md b/specs/heze/fork.md index 7aef37d7df..84cf981f31 100644 --- a/specs/heze/fork.md +++ b/specs/heze/fork.md @@ -47,6 +47,7 @@ def upgrade_to_heze(pre: gloas.BeaconState) -> BeaconState: value=pre.latest_execution_payload_bid.value, execution_payment=pre.latest_execution_payload_bid.execution_payment, blob_kzg_commitments=pre.latest_execution_payload_bid.blob_kzg_commitments, + execution_requests_root=pre.latest_execution_payload_bid.execution_requests_root, # [New in Heze:EIP7805] inclusion_list_bits=Bitvector[INCLUSION_LIST_COMMITTEE_SIZE](), ) diff --git a/tests/core/pyspec/eth_consensus_specs/test/bellatrix/block_processing/test_process_execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/bellatrix/block_processing/test_process_execution_payload.py index d5f1364ce4..e6cdd04bbb 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/bellatrix/block_processing/test_process_execution_payload.py +++ b/tests/core/pyspec/eth_consensus_specs/test/bellatrix/block_processing/test_process_execution_payload.py @@ -38,13 +38,13 @@ def run_execution_payload_processing( # Before Deneb, only `body.execution_payload` matters. `BeaconBlockBody` is just a wrapper. # After Gloas the execution payload is no longer in the body if is_post_gloas(spec): + # Fill state_root in header if empty for beacon_block_root computation + if state.latest_block_header.state_root == spec.Root(): + state.latest_block_header.state_root = state.hash_tree_root() envelope = spec.ExecutionPayloadEnvelope( payload=execution_payload, beacon_block_root=state.latest_block_header.hash_tree_root(), ) - post_state = state.copy() - post_state.latest_block_hash = execution_payload.block_hash - envelope.state_root = post_state.hash_tree_root() if envelope.builder_index == spec.BUILDER_INDEX_SELF_BUILD: privkey = privkeys[state.latest_block_header.proposer_index] else: @@ -96,9 +96,7 @@ def call_process_execution_payload(): yield "post", state - if is_post_gloas(spec): - assert state.latest_block_hash == execution_payload.block_hash - else: + if not is_post_gloas(spec): assert state.latest_execution_payload_header == get_execution_payload_header( spec, state, body.execution_payload ) diff --git a/tests/core/pyspec/eth_consensus_specs/test/deneb/block_processing/test_process_execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/deneb/block_processing/test_process_execution_payload.py index c30e7af5d2..f462be0b95 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/deneb/block_processing/test_process_execution_payload.py +++ b/tests/core/pyspec/eth_consensus_specs/test/deneb/block_processing/test_process_execution_payload.py @@ -45,25 +45,10 @@ def run_execution_payload_processing( # Ensure bid fields match payload for assertions to pass state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit state.latest_execution_payload_bid.block_hash = execution_payload.block_hash - post_state = state.copy() - previous_state_root = state.hash_tree_root() - if post_state.latest_block_header.state_root == spec.Root(): - post_state.latest_block_header.state_root = previous_state_root - envelope.beacon_block_root = post_state.latest_block_header.hash_tree_root() - - payment = post_state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - amount = payment.withdrawal.amount - if amount > 0: - post_state.builder_pending_withdrawals.append(payment.withdrawal) - post_state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] = spec.BuilderPendingPayment() - - post_state.execution_payload_availability[state.slot % spec.SLOTS_PER_HISTORICAL_ROOT] = 0b1 - post_state.latest_block_hash = execution_payload.block_hash - envelope.state_root = post_state.hash_tree_root() + # Fill state_root in header if empty for beacon_block_root computation + if state.latest_block_header.state_root == spec.Root(): + state.latest_block_header.state_root = state.hash_tree_root() + envelope.beacon_block_root = state.latest_block_header.hash_tree_root() if envelope.builder_index == spec.BUILDER_INDEX_SELF_BUILD: privkey = privkeys[state.latest_block_header.proposer_index] else: @@ -118,12 +103,7 @@ def call_process_execution_payload(): yield "post", state - if is_post_gloas(spec): - assert ( - state.execution_payload_availability[state.slot % spec.SLOTS_PER_HISTORICAL_ROOT] == 0b1 - ) - assert state.latest_block_hash == execution_payload.block_hash - else: + if not is_post_gloas(spec): assert state.latest_execution_payload_header == get_execution_payload_header( spec, state, execution_payload ) diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_execution_payload.py index 89962068c0..62680fe123 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_execution_payload.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_execution_payload.py @@ -47,12 +47,15 @@ def verify_and_notify_new_payload(self, new_payload_request) -> bool: yield "post", None return - # Use full verification including state root - spec.process_execution_payload(state, signed_envelope, TestEngine(), verify=True) + # Pure verification — process_execution_payload returns ExecutionRequests, does not mutate state + result = spec.process_execution_payload(state, signed_envelope, TestEngine(), verify=True) # Make sure we called the engine assert called_new_payload + # Verify returned requests match the envelope + assert result == signed_envelope.message.execution_requests + yield "post", state @@ -62,7 +65,6 @@ def prepare_execution_payload_envelope( builder_index=None, slot=None, beacon_block_root=None, - state_root=None, execution_payload=None, execution_requests=None, valid_signature=True, @@ -97,49 +99,12 @@ def prepare_execution_payload_envelope( ](), ) - # Create a copy of state for computing state_root after execution payload processing - if state_root is None: - post_state = state.copy() - # Simulate the state changes that process_execution_payload will make - - # Cache latest block header state root if empty (matches process_execution_payload) - previous_state_root = post_state.hash_tree_root() - if post_state.latest_block_header.state_root == spec.Root(): - post_state.latest_block_header.state_root = previous_state_root - - # Process execution requests if any - if execution_requests is not None: - for deposit in execution_requests.deposits: - spec.process_deposit_request(post_state, deposit) - for withdrawal in execution_requests.withdrawals: - spec.process_withdrawal_request(post_state, withdrawal) - for consolidation in execution_requests.consolidations: - spec.process_consolidation_request(post_state, consolidation) - - # Process builder payment (only if amount > 0) - payment = post_state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - if payment.withdrawal.amount > 0: - post_state.builder_pending_withdrawals.append(payment.withdrawal) - - # Clear the pending payment - post_state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] = spec.BuilderPendingPayment() - - # Update execution payload availability and latest block hash - post_state.execution_payload_availability[state.slot % spec.SLOTS_PER_HISTORICAL_ROOT] = 0b1 - post_state.latest_block_hash = execution_payload.block_hash - state_root = post_state.hash_tree_root() - envelope = spec.ExecutionPayloadEnvelope( payload=execution_payload, execution_requests=execution_requests, builder_index=builder_index, beacon_block_root=beacon_block_root, slot=slot, - state_root=state_root, ) if valid_signature: @@ -242,34 +207,8 @@ def test_process_execution_payload_valid(spec, state): spec, state, builder_index=builder_index, execution_payload=execution_payload ) - pre_payment = state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - pre_pending_withdrawals_len = len(state.builder_pending_withdrawals) - yield from run_execution_payload_processing(spec, state, signed_envelope) - # Verify state updates - assert state.execution_payload_availability[state.slot % spec.SLOTS_PER_HISTORICAL_ROOT] == 0b1 - assert state.latest_block_hash == execution_payload.block_hash - - # Verify pending withdrawal was added - assert len(state.builder_pending_withdrawals) == pre_pending_withdrawals_len + 1 - new_withdrawal = state.builder_pending_withdrawals[len(state.builder_pending_withdrawals) - 1] - assert new_withdrawal.amount == pre_payment.withdrawal.amount - assert new_withdrawal.builder_index == builder_index - assert new_withdrawal.fee_recipient == pre_payment.withdrawal.fee_recipient - - # Verify pending payment was cleared - cleared_payment = state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - # Check if it's been cleared by checking that it equals an empty BuilderPendingPayment - empty_payment = spec.BuilderPendingPayment() - assert cleared_payment.weight == empty_payment.weight - assert cleared_payment.withdrawal.amount == empty_payment.withdrawal.amount - assert cleared_payment.withdrawal.builder_index == empty_payment.withdrawal.builder_index - @with_gloas_and_later @spec_state_test @@ -293,27 +232,8 @@ def test_process_execution_payload_self_build_zero_value(spec, state): execution_payload=execution_payload, ) - # Capture pre-state for verification - pre_pending_withdrawals_len = len(state.builder_pending_withdrawals) - yield from run_execution_payload_processing(spec, state, signed_envelope) - # Verify state updates - assert state.execution_payload_availability[state.slot % spec.SLOTS_PER_HISTORICAL_ROOT] == 0b1 - assert state.latest_block_hash == execution_payload.block_hash - - # In self-build with zero value, no withdrawal is added since amount is zero - assert len(state.builder_pending_withdrawals) == pre_pending_withdrawals_len - - # Verify pending payment remains cleared (it was already empty) - cleared_payment = state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - empty_payment = spec.BuilderPendingPayment() - assert cleared_payment.weight == empty_payment.weight - assert cleared_payment.withdrawal.amount == empty_payment.withdrawal.amount - assert cleared_payment.withdrawal.builder_index == empty_payment.withdrawal.builder_index - @with_gloas_and_later @spec_state_test @@ -340,30 +260,8 @@ def test_process_execution_payload_large_payment_churn_impact(spec, state): execution_payload=execution_payload, ) - # Capture pre-state for churn verification - pre_payment = state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - pre_pending_withdrawals_len = len(state.builder_pending_withdrawals) - yield from run_execution_payload_processing(spec, state, signed_envelope) - # Verify builder payment was processed correctly - assert len(state.builder_pending_withdrawals) == pre_pending_withdrawals_len + 1 - new_withdrawal = state.builder_pending_withdrawals[pre_pending_withdrawals_len] - assert new_withdrawal.amount == pre_payment.withdrawal.amount - assert new_withdrawal.builder_index == builder_index - assert new_withdrawal.fee_recipient == pre_payment.withdrawal.fee_recipient - - # Verify pending payment was cleared - cleared_payment = state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - empty_payment = spec.BuilderPendingPayment() - assert cleared_payment.weight == empty_payment.weight - assert cleared_payment.withdrawal.amount == empty_payment.withdrawal.amount - assert cleared_payment.withdrawal.builder_index == empty_payment.withdrawal.builder_index - @with_gloas_and_later @spec_state_test @@ -395,31 +293,8 @@ def test_process_execution_payload_with_blob_commitments(spec, state): execution_payload=execution_payload, ) - # Capture pre-state for payment verification - pre_payment = state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - pre_pending_withdrawals_len = len(state.builder_pending_withdrawals) - yield from run_execution_payload_processing(spec, state, signed_envelope) - # Verify builder payment was processed correctly - # 1. Verify pending withdrawal was added with correct amount and withdrawable epoch - assert len(state.builder_pending_withdrawals) == pre_pending_withdrawals_len + 1 - new_withdrawal = state.builder_pending_withdrawals[pre_pending_withdrawals_len] - assert new_withdrawal.amount == pre_payment.withdrawal.amount - assert new_withdrawal.builder_index == builder_index - assert new_withdrawal.fee_recipient == pre_payment.withdrawal.fee_recipient - - # Verify pending payment was cleared - cleared_payment = state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - empty_payment = spec.BuilderPendingPayment() - assert cleared_payment.weight == empty_payment.weight - assert cleared_payment.withdrawal.amount == empty_payment.withdrawal.amount - assert cleared_payment.withdrawal.builder_index == empty_payment.withdrawal.builder_index - @with_gloas_and_later @spec_state_test @@ -480,39 +355,8 @@ def test_process_execution_payload_with_execution_requests(spec, state): execution_requests=execution_requests, ) - # Capture pre-state for verification - pre_pending_deposits_len = len(state.pending_deposits) - pre_payment = state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - pre_pending_withdrawals_len = len(state.builder_pending_withdrawals) - yield from run_execution_payload_processing(spec, state, signed_envelope) - # Verify deposit request was processed - deposits are always added to pending queue - deposit_request = execution_requests.deposits[0] - assert len(state.pending_deposits) == pre_pending_deposits_len + 1 - new_pending_deposit = state.pending_deposits[pre_pending_deposits_len] - assert new_pending_deposit.pubkey == deposit_request.pubkey - assert new_pending_deposit.withdrawal_credentials == deposit_request.withdrawal_credentials - assert new_pending_deposit.amount == deposit_request.amount - - # Verify builder payment was processed correctly - assert len(state.builder_pending_withdrawals) == pre_pending_withdrawals_len + 1 - new_withdrawal = state.builder_pending_withdrawals[pre_pending_withdrawals_len] - assert new_withdrawal.amount == pre_payment.withdrawal.amount - assert new_withdrawal.builder_index == builder_index - assert new_withdrawal.fee_recipient == pre_payment.withdrawal.fee_recipient - - # Verify pending payment was cleared - cleared_payment = state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH - ] - empty_payment = spec.BuilderPendingPayment() - assert cleared_payment.weight == empty_payment.weight - assert cleared_payment.withdrawal.amount == empty_payment.withdrawal.amount - assert cleared_payment.withdrawal.builder_index == empty_payment.withdrawal.builder_index - @with_gloas_and_later @spec_state_test @@ -578,23 +422,8 @@ def test_process_execution_payload_with_builder_deposit_after_pending_validator( execution_requests=execution_requests, ) - pre_pending_deposits_len = len(state.pending_deposits) - pre_builder_count = len(state.builders) - yield from run_execution_payload_processing(spec, state, signed_envelope) - # Both deposits must end up in the pending queue, with no new builder created - assert len(state.pending_deposits) == pre_pending_deposits_len + 2 - assert len(state.builders) == pre_builder_count - first = state.pending_deposits[pre_pending_deposits_len] - second = state.pending_deposits[pre_pending_deposits_len + 1] - assert first.pubkey == deposit_request_1.pubkey - assert first.withdrawal_credentials == deposit_request_1.withdrawal_credentials - assert first.amount == deposit_request_1.amount - assert second.pubkey == deposit_request_2.pubkey - assert second.withdrawal_credentials == deposit_request_2.withdrawal_credentials - assert second.amount == deposit_request_2.amount - # # Invalid signature tests diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py new file mode 100644 index 0000000000..7f00df0d64 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py @@ -0,0 +1,459 @@ +from eth_consensus_specs.test.context import ( + always_bls, + default_activation_threshold, + expect_assertion_error, + scaled_churn_balances_exceed_activation_exit_churn_limit, + spec_state_test, + spec_test, + with_gloas_and_later, + with_presets, +) +from eth_consensus_specs.test.helpers.constants import MINIMAL +from eth_consensus_specs.test.helpers.deposits import ( + make_withdrawal_credentials, + prepare_deposit_request, +) +from eth_consensus_specs.test.helpers.withdrawals import ( + prepare_withdrawal_request, + set_compounding_withdrawal_credential_with_balance, + set_eth1_withdrawal_credential_with_balance, +) +from tests.core.pyspec.eth_consensus_specs.test.helpers.genesis import create_genesis_state + + +def _make_hash(spec, byte): + return spec.Hash32(bytes([byte]) * 32) + + +def _build_execution_requests( + spec, + deposits=None, + withdrawals=None, + consolidations=None, +): + return spec.ExecutionRequests( + deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]( + deposits or [] + ), + withdrawals=spec.List[spec.WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD]( + withdrawals or [] + ), + consolidations=spec.List[ + spec.ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD + ](consolidations or []), + ) + + +def _setup_parent_payload_state( + spec, + state, + parent_slot, + builder_index=0, + value=None, +): + if value is None: + value = spec.Gwei(0) + + state.slot = parent_slot + state.latest_block_header.slot = parent_slot + state.latest_block_hash = _make_hash(spec, 0x11) + + parent_bid = spec.ExecutionPayloadBid( + parent_block_hash=state.latest_block_hash, + parent_block_root=state.latest_block_header.hash_tree_root(), + block_hash=_make_hash(spec, 0x22), + execution_requests_root=spec.hash_tree_root(spec.ExecutionRequests()), + prev_randao=spec.get_randao_mix(state, spec.get_current_epoch(state)), + fee_recipient=spec.ExecutionAddress(b"\xaa" * 20), + gas_limit=spec.uint64(60_000_000), + builder_index=builder_index, + slot=parent_slot, + value=value, + blob_kzg_commitments=spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK](), + ) + state.latest_execution_payload_bid = parent_bid + + if value > 0: + payment_index = spec.SLOTS_PER_EPOCH + parent_slot % spec.SLOTS_PER_EPOCH + state.builder_pending_payments[payment_index] = spec.BuilderPendingPayment( + weight=0, + withdrawal=spec.BuilderPendingWithdrawal( + fee_recipient=parent_bid.fee_recipient, + amount=value, + builder_index=builder_index, + ), + ) + + return parent_bid + + +def _rotate_builder_pending_payments(spec, state): + old_payments = state.builder_pending_payments[spec.SLOTS_PER_EPOCH :] + new_payments = [spec.BuilderPendingPayment() for _ in range(spec.SLOTS_PER_EPOCH)] + state.builder_pending_payments = old_payments + new_payments + + +def _build_block_for_parent_processing( + spec, + state, + slot, + parent_bid, + parent_execution_requests=None, + parent_full=True, +): + if parent_execution_requests is None: + parent_execution_requests = spec.ExecutionRequests() + if parent_full: + execution_requests_root = spec.hash_tree_root(parent_execution_requests) + state.latest_execution_payload_bid.execution_requests_root = execution_requests_root + parent_bid.execution_requests_root = execution_requests_root + + child_bid = spec.ExecutionPayloadBid( + parent_block_hash=(parent_bid.block_hash if parent_full else parent_bid.parent_block_hash), + parent_block_root=state.latest_block_header.hash_tree_root(), + block_hash=_make_hash(spec, 0x33), + prev_randao=spec.get_randao_mix(state, spec.get_current_epoch(state)), + fee_recipient=spec.ExecutionAddress(), + gas_limit=spec.uint64(60_000_000), + builder_index=spec.BUILDER_INDEX_SELF_BUILD, + slot=slot, + value=spec.Gwei(0), + blob_kzg_commitments=spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK](), + ) + + block = spec.BeaconBlock(slot=slot) + block.body.signed_execution_payload_bid = spec.SignedExecutionPayloadBid( + message=child_bid, + signature=spec.G2_POINT_AT_INFINITY, + ) + block.body.parent_execution_requests = parent_execution_requests + return block + + +def run_parent_execution_payload_processing(spec, state, block, valid=True): + """ + Run ``process_parent_execution_payload``, yielding: + - pre-state ('pre') + - block ('block') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + yield "pre", state + yield "block", block + + if not valid: + expect_assertion_error(lambda: spec.process_parent_execution_payload(state, block)) + yield "post", None + return + + spec.process_parent_execution_payload(state, block) + yield "post", state + + +@with_gloas_and_later +@with_presets([MINIMAL], "need sufficient consolidation churn limit") +@spec_test +@always_bls +def test_process_parent_execution_payload_full_parent(spec, phases): + state = create_genesis_state( + spec, + scaled_churn_balances_exceed_activation_exit_churn_limit(spec), + default_activation_threshold(spec), + ) + active_epoch = spec.Epoch(spec.config.SHARD_COMMITTEE_PERIOD + 2) + parent_slot = spec.compute_start_slot_at_epoch(active_epoch) + 1 + child_slot = parent_slot + 1 + builder_index = 0 + payment_amount = spec.Gwei(5_000_000) + + parent_bid = _setup_parent_payload_state( + spec, state, parent_slot, builder_index=builder_index, value=payment_amount + ) + state.slot = child_slot + + for validator_index in (0, 1, 2): + state.validators[validator_index].activation_epoch = spec.Epoch(0) + state.validators[validator_index].exit_epoch = spec.FAR_FUTURE_EPOCH + + deposit_request = prepare_deposit_request( + spec, + len(state.validators), + spec.MIN_DEPOSIT_AMOUNT, + index=0, + withdrawal_credentials=make_withdrawal_credentials( + spec, spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX, b"\x44" + ), + signed=True, + ) + withdrawal_request = prepare_withdrawal_request(spec, state, 0) + + source_address = b"\x12" * 20 + set_eth1_withdrawal_credential_with_balance(spec, state, 1, address=source_address) + set_compounding_withdrawal_credential_with_balance(spec, state, 2, address=b"\x34" * 20) + state.earliest_consolidation_epoch = spec.compute_activation_exit_epoch( + spec.get_current_epoch(state) + ) + state.consolidation_balance_to_consume = spec.get_consolidation_churn_limit(state) + consolidation_request = spec.ConsolidationRequest( + source_address=spec.ExecutionAddress(source_address), + source_pubkey=state.validators[1].pubkey, + target_pubkey=state.validators[2].pubkey, + ) + + execution_requests = _build_execution_requests( + spec, + deposits=[deposit_request], + withdrawals=[withdrawal_request], + consolidations=[consolidation_request], + ) + parent_bid = state.latest_execution_payload_bid + block = _build_block_for_parent_processing( + spec, + state, + child_slot, + parent_bid, + parent_execution_requests=execution_requests, + parent_full=True, + ) + + payment_index = spec.SLOTS_PER_EPOCH + parent_slot % spec.SLOTS_PER_EPOCH + pre_pending_deposits_len = len(state.pending_deposits) + pre_pending_consolidations_len = len(state.pending_consolidations) + pre_pending_withdrawals_len = len(state.builder_pending_withdrawals) + + yield from run_parent_execution_payload_processing(spec, state, block) + + assert len(state.pending_deposits) == pre_pending_deposits_len + 1 + pending_deposit = state.pending_deposits[pre_pending_deposits_len] + assert pending_deposit.pubkey == deposit_request.pubkey + assert pending_deposit.withdrawal_credentials == deposit_request.withdrawal_credentials + assert pending_deposit.amount == deposit_request.amount + assert pending_deposit.slot == child_slot + + assert state.validators[0].exit_epoch != spec.FAR_FUTURE_EPOCH + + assert len(state.pending_consolidations) == pre_pending_consolidations_len + 1 + pending_consolidation = state.pending_consolidations[pre_pending_consolidations_len] + assert pending_consolidation.source_index == 1 + assert pending_consolidation.target_index == 2 + assert state.validators[1].exit_epoch != spec.FAR_FUTURE_EPOCH + + assert len(state.builder_pending_withdrawals) == pre_pending_withdrawals_len + 1 + pending_withdrawal = state.builder_pending_withdrawals[pre_pending_withdrawals_len] + assert pending_withdrawal.amount == payment_amount + assert pending_withdrawal.builder_index == builder_index + assert pending_withdrawal.fee_recipient == parent_bid.fee_recipient + + assert state.builder_pending_payments[payment_index] == spec.BuilderPendingPayment() + assert state.execution_payload_availability[parent_slot % spec.SLOTS_PER_HISTORICAL_ROOT] == 0b1 + assert state.latest_block_hash == parent_bid.block_hash + + +@with_gloas_and_later +@spec_state_test +def test_process_parent_execution_payload_empty_parent(spec, state): + parent_slot = state.slot + child_slot = parent_slot + 1 + + parent_bid = _setup_parent_payload_state(spec, state, parent_slot) + state.slot = child_slot + block = _build_block_for_parent_processing( + spec, state, child_slot, parent_bid, parent_full=False + ) + pre_state = state.copy() + + yield from run_parent_execution_payload_processing(spec, state, block) + + assert state == pre_state + + +@with_gloas_and_later +@spec_state_test +def test_process_parent_execution_payload_empty_parent_with_nonempty_requests_invalid(spec, state): + parent_slot = state.slot + child_slot = parent_slot + 1 + + parent_bid = _setup_parent_payload_state(spec, state, parent_slot) + state.slot = child_slot + execution_requests = _build_execution_requests( + spec, + withdrawals=[spec.WithdrawalRequest(amount=spec.FULL_EXIT_REQUEST_AMOUNT)], + ) + block = _build_block_for_parent_processing( + spec, + state, + child_slot, + parent_bid, + parent_execution_requests=execution_requests, + parent_full=False, + ) + + yield from run_parent_execution_payload_processing(spec, state, block, valid=False) + + +@with_gloas_and_later +@spec_state_test +@always_bls +def test_process_parent_execution_payload_with_deposit_requests(spec, state): + parent_slot = state.slot + child_slot = parent_slot + 1 + + parent_bid = _setup_parent_payload_state(spec, state, parent_slot) + state.slot = child_slot + + deposit_requests = [ + prepare_deposit_request( + spec, + len(state.validators), + spec.MIN_DEPOSIT_AMOUNT, + index=0, + signed=True, + ), + prepare_deposit_request( + spec, + len(state.validators) + 1, + spec.MIN_DEPOSIT_AMOUNT + spec.Gwei(1), + index=1, + signed=True, + ), + ] + execution_requests = _build_execution_requests(spec, deposits=deposit_requests) + parent_bid = state.latest_execution_payload_bid + block = _build_block_for_parent_processing( + spec, + state, + child_slot, + parent_bid, + parent_execution_requests=execution_requests, + ) + + pre_pending_deposits_len = len(state.pending_deposits) + + yield from run_parent_execution_payload_processing(spec, state, block) + + assert len(state.pending_deposits) == pre_pending_deposits_len + len(deposit_requests) + for i, deposit_request in enumerate(deposit_requests): + pending_deposit = state.pending_deposits[pre_pending_deposits_len + i] + assert pending_deposit.pubkey == deposit_request.pubkey + assert pending_deposit.withdrawal_credentials == deposit_request.withdrawal_credentials + assert pending_deposit.amount == deposit_request.amount + assert pending_deposit.slot == child_slot + + +@with_gloas_and_later +@spec_state_test +def test_process_parent_execution_payload_builder_payment(spec, state): + parent_slot = state.slot + child_slot = parent_slot + 1 + builder_index = 0 + payment_amount = spec.Gwei(7_000_000) + + parent_bid = _setup_parent_payload_state( + spec, state, parent_slot, builder_index=builder_index, value=payment_amount + ) + state.slot = child_slot + block = _build_block_for_parent_processing(spec, state, child_slot, parent_bid) + + payment_index = spec.SLOTS_PER_EPOCH + parent_slot % spec.SLOTS_PER_EPOCH + pre_pending_withdrawals_len = len(state.builder_pending_withdrawals) + + yield from run_parent_execution_payload_processing(spec, state, block) + + assert len(state.builder_pending_withdrawals) == pre_pending_withdrawals_len + 1 + withdrawal = state.builder_pending_withdrawals[pre_pending_withdrawals_len] + assert withdrawal.amount == payment_amount + assert withdrawal.builder_index == builder_index + assert withdrawal.fee_recipient == parent_bid.fee_recipient + assert state.builder_pending_payments[payment_index] == spec.BuilderPendingPayment() + + +@with_gloas_and_later +@spec_state_test +def test_process_parent_execution_payload_cross_epoch(spec, state): + parent_slot = spec.compute_start_slot_at_epoch(spec.Epoch(1)) - 1 + child_slot = parent_slot + 1 + builder_index = 0 + payment_amount = spec.Gwei(9_000_000) + + parent_bid = _setup_parent_payload_state( + spec, state, parent_slot, builder_index=builder_index, value=payment_amount + ) + _rotate_builder_pending_payments(spec, state) + state.slot = child_slot + block = _build_block_for_parent_processing(spec, state, child_slot, parent_bid) + + payment_index = parent_slot % spec.SLOTS_PER_EPOCH + pre_pending_withdrawals_len = len(state.builder_pending_withdrawals) + + yield from run_parent_execution_payload_processing(spec, state, block) + + assert len(state.builder_pending_withdrawals) == pre_pending_withdrawals_len + 1 + withdrawal = state.builder_pending_withdrawals[pre_pending_withdrawals_len] + assert withdrawal.amount == payment_amount + assert withdrawal.builder_index == builder_index + assert state.builder_pending_payments[payment_index] == spec.BuilderPendingPayment() + + +@with_gloas_and_later +@spec_state_test +@always_bls +def test_process_parent_execution_payload_builder_deposit_after_pending_validator(spec, state): + parent_slot = state.slot + child_slot = parent_slot + 1 + + parent_bid = _setup_parent_payload_state(spec, state, parent_slot) + state.slot = child_slot + + new_validator_index = len(state.validators) + amount = spec.MIN_DEPOSIT_AMOUNT + deposit_request_1 = prepare_deposit_request( + spec, + new_validator_index, + amount, + index=0, + withdrawal_credentials=make_withdrawal_credentials( + spec, spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX, b"\xab" + ), + signed=True, + ) + deposit_request_2 = prepare_deposit_request( + spec, + new_validator_index, + amount, + index=1, + withdrawal_credentials=make_withdrawal_credentials( + spec, spec.BUILDER_WITHDRAWAL_PREFIX, b"\x59" + ), + signed=True, + ) + + execution_requests = _build_execution_requests( + spec, + deposits=[deposit_request_1, deposit_request_2], + ) + parent_bid = state.latest_execution_payload_bid + block = _build_block_for_parent_processing( + spec, + state, + child_slot, + parent_bid, + parent_execution_requests=execution_requests, + ) + + pre_pending_deposits_len = len(state.pending_deposits) + pre_builder_count = len(state.builders) + + yield from run_parent_execution_payload_processing(spec, state, block) + + assert len(state.pending_deposits) == pre_pending_deposits_len + 2 + assert len(state.builders) == pre_builder_count + first = state.pending_deposits[pre_pending_deposits_len] + second = state.pending_deposits[pre_pending_deposits_len + 1] + assert first.pubkey == deposit_request_1.pubkey + assert first.withdrawal_credentials == deposit_request_1.withdrawal_credentials + assert first.amount == deposit_request_1.amount + assert first.slot == child_slot + assert second.pubkey == deposit_request_2.pubkey + assert second.withdrawal_credentials == deposit_request_2.withdrawal_credentials + assert second.amount == deposit_request_2.amount + assert second.slot == child_slot diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/fork_choice/test_on_execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/fork_choice/test_on_execution_payload.py index 4b72c32734..9ebd4c31cc 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/fork_choice/test_on_execution_payload.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/fork_choice/test_on_execution_payload.py @@ -61,8 +61,8 @@ def test_on_execution_payload(spec, state): envelope = build_signed_execution_payload_envelope(spec, state, block_root, signed_block) yield from add_execution_payload(spec, store, envelope, test_steps, valid=True) - # Block root should now be stored in payload_states after payload reveal - assert block_root in store.payload_states + # Block root should now be stored in payloads after payload reveal + assert block_root in store.payloads head = spec.get_head(store) assert head.payload_status == spec.PAYLOAD_STATUS_FULL diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/sanity/test_blocks.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/sanity/test_blocks.py index def49a218d..1046cd6250 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/sanity/test_blocks.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/sanity/test_blocks.py @@ -95,7 +95,6 @@ def _attempt_payload_with_withdrawals(spec, state, withdrawals): builder_index=committed_bid.builder_index, beacon_block_root=test_state.latest_block_header.hash_tree_root(), slot=test_state.slot, - state_root=spec.Root(), ) signed_envelope = spec.SignedExecutionPayloadEnvelope( diff --git a/tests/core/pyspec/eth_consensus_specs/test/helpers/execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/execution_payload.py index 04af151fc8..bf01c6b916 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/execution_payload.py @@ -92,6 +92,7 @@ def get_execution_payload_bid(spec, state, execution_payload): slot=state.slot, value=spec.Gwei(0), blob_kzg_commitments=kzg_list, + execution_requests_root=hash_tree_root(spec.ExecutionRequests()), ) @@ -319,10 +320,12 @@ def build_empty_post_gloas_execution_payload_bid(spec, state): kzg_list = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]() # Use self-build: builder_index is the same as the beacon proposer index builder_index = spec.BUILDER_INDEX_SELF_BUILD - # Set block_hash to a different value than spec.Hash32(), - # to distinguish it from the genesis block hash and have - # is_parent_node_full correctly return False - empty_payload_hash = spec.Hash32(b"\x01" + b"\x00" * 31) + # Use a deterministic synthetic hash derived from the current slot and parent hash. + # This keeps helper-built empty payload bids distinct across consecutive empty blocks, + # which is necessary for FULL vs EMPTY parent-path tests around fork transitions. + empty_payload_hash = spec.Hash32( + sha256(state.latest_block_hash + int(state.slot).to_bytes(8, "little")).digest() + ) prev_randao = spec.get_randao_mix(state, spec.get_current_epoch(state)) return spec.ExecutionPayloadBid( parent_block_hash=state.latest_block_hash, @@ -335,6 +338,7 @@ def build_empty_post_gloas_execution_payload_bid(spec, state): slot=state.slot, value=spec.Gwei(0), blob_kzg_commitments=kzg_list, + execution_requests_root=hash_tree_root(spec.ExecutionRequests()), ) @@ -486,44 +490,15 @@ def build_signed_execution_payload_envelope(spec, state, block_root, signed_bloc payload.gas_limit = state.latest_execution_payload_bid.gas_limit payload.parent_hash = state.latest_block_hash - # Simulate process_execution_payload state changes to compute correct state_root - temp_state = state.copy() - - # Cache latest block header state root - previous_state_root = temp_state.hash_tree_root() - if temp_state.latest_block_header.state_root == spec.Root(): - temp_state.latest_block_header.state_root = previous_state_root - - # Process builder payment: move pending payment to withdrawals if amount > 0 - payment = temp_state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + temp_state.slot % spec.SLOTS_PER_EPOCH - ] - if payment.withdrawal.amount > 0: - temp_state.builder_pending_withdrawals.append(payment.withdrawal) - - # Clear pending payment slot - temp_state.builder_pending_payments[ - spec.SLOTS_PER_EPOCH + temp_state.slot % spec.SLOTS_PER_EPOCH - ] = spec.BuilderPendingPayment() - - # Update execution payload availability for this slot - temp_state.execution_payload_availability[temp_state.slot % spec.SLOTS_PER_HISTORICAL_ROOT] = ( - 0b1 - ) - - # Advance EL chain head - temp_state.latest_block_hash = payload.block_hash - - post_processing_state_root = temp_state.hash_tree_root() - # Create the execution payload envelope message + # Note: process_execution_payload no longer mutates state, so no state simulation needed. + # state_root was removed from ExecutionPayloadEnvelope in deferred-payload redesign. envelope_message = spec.ExecutionPayloadEnvelope( beacon_block_root=block_root, payload=payload, execution_requests=spec.ExecutionRequests(), builder_index=builder_index, slot=signed_block.message.slot, - state_root=post_processing_state_root, ) # Sign the envelope: self-builds use proposer key, external builds use builder key diff --git a/tests/core/pyspec/eth_consensus_specs/test/helpers/fork_choice.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/fork_choice.py index 8ebd21ab14..f8a26da671 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/fork_choice.py @@ -22,6 +22,43 @@ def check_head_against_root(spec, store, root): assert head == root +def mark_block_payload_available(spec, store, block_or_root): + """Mark a locally built post-Gloas block as having a locally available payload.""" + if not is_post_gloas(spec): + return + + if hasattr(block_or_root, "message"): + block_root = block_or_root.message.hash_tree_root() + elif hasattr(block_or_root, "hash_tree_root"): + block_root = block_or_root.hash_tree_root() + else: + block_root = block_or_root + + assert block_root in store.blocks + if hasattr(store, "execution_payloads"): + store.execution_payloads.add(block_root) + elif hasattr(store, "payloads"): + if hasattr(store.payloads, "add"): + store.payloads.add(block_root) + else: + store.payloads[block_root] = store.block_states[block_root] + else: + raise AssertionError("Store has neither execution_payloads nor payloads") + if hasattr(store, "payload_timeliness_vote") and block_root in store.payload_timeliness_vote: + vote = store.payload_timeliness_vote[block_root] + for i in range(len(vote)): + vote[i] = True + if ( + hasattr(store, "payload_data_availability_vote") + and block_root in store.payload_data_availability_vote + ): + vote = store.payload_data_availability_vote[block_root] + for i in range(len(vote)): + vote[i] = True + if hasattr(store, "payload_inclusion_list_satisfaction"): + store.payload_inclusion_list_satisfaction[block_root] = True + + class BlobData(NamedTuple): """ The return values of blob/sidecar retrieval helpers. @@ -255,6 +292,9 @@ def get_genesis_forkchoice_store_and_block(spec, genesis_state): genesis_block.body.signed_execution_payload_bid.message.block_hash = ( genesis_state.latest_block_hash ) + genesis_block.body.signed_execution_payload_bid.message.execution_requests_root = ( + spec.hash_tree_root(spec.ExecutionRequests()) + ) store = spec.get_forkchoice_store(genesis_state, genesis_block) return store, genesis_block @@ -405,9 +445,14 @@ def run_on_execution_payload(spec, store, signed_envelope, valid=True): spec.on_execution_payload(store, signed_envelope) - # Verify the envelope was processed, block should now have FULL state + # Verify the envelope was processed and the block is now FULL locally envelope_root = signed_envelope.message.beacon_block_root - assert envelope_root in store.payload_states + if hasattr(store, "execution_payloads"): + assert envelope_root in store.execution_payloads + elif hasattr(store, "payloads"): + assert envelope_root in store.payloads + else: + raise AssertionError("Store has neither execution_payloads nor payloads") def get_execution_payload_envelope_file_name(signed_envelope): @@ -517,7 +562,14 @@ def output_store_checks(spec, store, test_steps, with_viable_for_head_weights=Fa def apply_next_epoch_with_attestations( - spec, state, store, fill_cur_epoch, fill_prev_epoch, participation_fn=None, test_steps=None + spec, + state, + store, + fill_cur_epoch, + fill_prev_epoch, + participation_fn=None, + test_steps=None, + mark_payload_available=False, ): if test_steps is None: test_steps = [] @@ -528,6 +580,8 @@ def apply_next_epoch_with_attestations( for signed_block in new_signed_blocks: block = signed_block.message yield from tick_and_add_block(spec, store, signed_block, test_steps) + if mark_payload_available: + mark_block_payload_available(spec, store, signed_block) block_root = block.hash_tree_root() assert store.blocks[block_root] == block last_signed_block = signed_block @@ -538,7 +592,15 @@ def apply_next_epoch_with_attestations( def apply_next_slots_with_attestations( - spec, state, store, slots, fill_cur_epoch, fill_prev_epoch, test_steps, participation_fn=None + spec, + state, + store, + slots, + fill_cur_epoch, + fill_prev_epoch, + test_steps, + participation_fn=None, + mark_payload_available=False, ): _, new_signed_blocks, post_state = next_slots_with_attestations( spec, state, slots, fill_cur_epoch, fill_prev_epoch, participation_fn=participation_fn @@ -546,6 +608,8 @@ def apply_next_slots_with_attestations( for signed_block in new_signed_blocks: block = signed_block.message yield from tick_and_add_block(spec, store, signed_block, test_steps) + if mark_payload_available: + mark_block_payload_available(spec, store, signed_block) block_root = block.hash_tree_root() assert store.blocks[block_root] == block last_signed_block = signed_block diff --git a/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py index 86dbaa877c..821e417d40 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py @@ -149,6 +149,11 @@ def create_genesis_state(spec, validator_balances, activation_threshold): current_version = get_fork_version(spec, spec.fork) genesis_block_body = spec.BeaconBlockBody() + if is_post_gloas(spec): + empty_execution_requests_root = spec.hash_tree_root(spec.ExecutionRequests()) + genesis_block_body.signed_execution_payload_bid.message.execution_requests_root = ( + empty_execution_requests_root + ) state = spec.BeaconState( genesis_time=0, @@ -197,8 +202,8 @@ def create_genesis_state(spec, validator_balances, activation_threshold): state.next_sync_committee = spec.get_next_sync_committee(state) if is_post_gloas(spec): - # Initialize the latest_execution_payload_bid - genesis_block_body.signed_execution_payload_bid.message.block_hash = eth1_block_hash + # Initialize the latest_execution_payload_bid commitment for the genesis parent. + state.latest_execution_payload_bid.execution_requests_root = empty_execution_requests_root elif is_post_bellatrix(spec): # Initialize the execution payload header (with block number and genesis time set to 0) state.latest_execution_payload_header = get_sample_genesis_execution_payload_header( diff --git a/tests/core/pyspec/eth_consensus_specs/test/phase0/fork_choice/test_get_head.py b/tests/core/pyspec/eth_consensus_specs/test/phase0/fork_choice/test_get_head.py index 5b2609d5e5..cfeef29c41 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/phase0/fork_choice/test_get_head.py +++ b/tests/core/pyspec/eth_consensus_specs/test/phase0/fork_choice/test_get_head.py @@ -58,9 +58,9 @@ def test_genesis(spec, state): if is_post_gloas(spec): # Verify Gloas store fields - assert hasattr(store, "payload_states") + assert hasattr(store, "payloads") assert hasattr(store, "payload_timeliness_vote") - assert anchor_root in store.payload_states + assert anchor_root in store.payloads assert anchor_root in store.payload_timeliness_vote # Check PTC vote initialization @@ -454,6 +454,9 @@ def test_discard_equivocations_slashed_validator_censoring(spec, state): anchor_block.body.signed_execution_payload_bid.message.block_hash = ( anchor_state.latest_block_hash ) + anchor_block.body.signed_execution_payload_bid.message.execution_requests_root = ( + spec.hash_tree_root(spec.ExecutionRequests()) + ) yield "anchor_state", anchor_state yield "anchor_block", anchor_block diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index 2c6959a0fa..8124bd66e1 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -291,8 +291,7 @@ Each file is an SSZ-snappy encoded `SignedExecutionPayloadEnvelope`. initialize the local store object with `get_forkchoice_store(anchor_state, anchor_block)` helper. For Gloas and later forks: the anchor block's payload state is initialized by seeding - `payload_states[anchor_root]` from `anchor_state`, as specified in - `get_forkchoice_store`. + `payloads` with `{anchor_root}`, as specified in `get_forkchoice_store`. 2. Iterate sequentially through `steps.yaml` - For each execution, look up the corresponding ssz_snappy file. Execute the corresponding helper function on the current store.