diff --git a/pysetup/spec_builders/gloas.py b/pysetup/spec_builders/gloas.py index f4304866bb..db23e66fc0 100644 --- a/pysetup/spec_builders/gloas.py +++ b/pysetup/spec_builders/gloas.py @@ -33,6 +33,7 @@ def deprecate_functions(cls) -> set[str]: return set( [ "compute_proposer_index", + "process_execution_payload", "retrieve_column_sidecars", ] ) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 11c46e07bb..8856ba6451 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -45,7 +45,6 @@ - [New `is_builder_withdrawal_credential`](#new-is_builder_withdrawal_credential) - [New `is_attestation_same_slot`](#new-is_attestation_same_slot) - [New `is_valid_indexed_payload_attestation`](#new-is_valid_indexed_payload_attestation) - - [New `is_parent_block_full`](#new-is_parent_block_full) - [New `is_pending_validator`](#new-is_pending_validator) - [Misc](#misc-2) - [New `convert_builder_index_to_validator_index`](#new-convert_builder_index_to_validator_index) @@ -63,6 +62,7 @@ - [New `get_builder_payment_quorum_threshold`](#new-get_builder_payment_quorum_threshold) - [Beacon state mutators](#beacon-state-mutators) - [New `initiate_builder_exit`](#new-initiate_builder_exit) + - [New `settle_builder_payment`](#new-settle_builder_payment) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - [Modified `process_slot`](#modified-process_slot) - [Epoch processing](#epoch-processing) @@ -70,6 +70,9 @@ - [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 `apply_parent_execution_payload`](#new-apply_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) @@ -79,6 +82,8 @@ - [New `update_builder_pending_withdrawals`](#new-update_builder_pending_withdrawals) - [New `update_next_withdrawal_builder_index`](#new-update_next_withdrawal_builder_index) - [Modified `process_withdrawals`](#modified-process_withdrawals) + - [Execution payload](#execution-payload) + - [Removed `process_execution_payload`](#removed-process_execution_payload) - [Execution payload bid](#execution-payload-bid) - [New `verify_execution_payload_bid_signature`](#new-verify_execution_payload_bid_signature) - [New `process_execution_payload_bid`](#new-process_execution_payload_bid) @@ -97,9 +102,6 @@ - [New `process_payload_attestation`](#new-process_payload_attestation) - [Proposer slashing](#proposer-slashing) - [Modified `process_proposer_slashing`](#modified-process_proposer_slashing) - - [Execution payload processing](#execution-payload-processing) - - [New `verify_execution_payload_envelope_signature`](#new-verify_execution_payload_envelope_signature) - - [New `process_execution_payload`](#new-process_execution_payload) @@ -267,6 +269,7 @@ class ExecutionPayloadBid(Container): value: Gwei execution_payment: Gwei blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + execution_requests_root: Root ``` #### `SignedExecutionPayloadBid` @@ -286,7 +289,6 @@ class ExecutionPayloadEnvelope(Container): builder_index: BuilderIndex beacon_block_root: Root slot: Slot - state_root: Root ``` #### `SignedExecutionPayloadEnvelope` @@ -326,6 +328,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` @@ -507,18 +511,6 @@ def is_valid_indexed_payload_attestation( return bls.FastAggregateVerify(pubkeys, signing_root, attestation.signature) ``` -#### New `is_parent_block_full` - -*Note*: This function returns true if the last committed payload bid was -fulfilled with a payload, which can only happen when both beacon block and -payload were present. This function must be called on a beacon state before -processing the execution payload bid in the block. - -```python -def is_parent_block_full(state: BeaconState) -> bool: - return state.latest_execution_payload_bid.block_hash == state.latest_block_hash -``` - #### New `is_pending_validator` *Note*: This function naively revalidates deposit signatures on every call. @@ -812,6 +804,17 @@ def initiate_builder_exit(state: BeaconState, builder_index: BuilderIndex) -> No builder.withdrawable_epoch = get_current_epoch(state) + MIN_BUILDER_WITHDRAWABILITY_DELAY ``` +#### New `settle_builder_payment` + +```python +def settle_builder_payment(state: BeaconState, payment_index: uint64) -> None: + assert payment_index < len(state.builder_pending_payments) + payment = state.builder_pending_payments[payment_index] + if payment.withdrawal.amount > 0: + state.builder_pending_withdrawals.append(payment.withdrawal) + state.builder_pending_payments[payment_index] = BuilderPendingPayment() +``` + ## Beacon chain state transition function State transition is fundamentally modified in Gloas. The full state transition @@ -824,12 +827,14 @@ 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 +`verify_execution_payload_envelope(state, signed_envelope, execution_engine)`. +Payload processing is deferred to the next beacon block via +`process_parent_execution_payload`. Payloads that trigger an unhandled exception +(e.g. a failed `assert` or an out-of-range list access) are considered invalid. +Payloads that cause a `uint64` overflow or underflow are also considered +invalid. ### Modified `process_slot` @@ -917,6 +922,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) @@ -931,6 +938,81 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: process_sync_aggregate(state, block.body.sync_aggregate) ``` +#### Parent execution payload + +##### New `apply_parent_execution_payload` + +*Note*: This function processes the parent's execution requests, queues the +builder payment, updates payload availability, and updates the latest block +hash. It is called by `process_parent_execution_payload` during block processing +and by the validator during block production before computing withdrawals. + +```python +def apply_parent_execution_payload( + state: BeaconState, + parent_bid: ExecutionPayloadBid, + requests: ExecutionRequests, +) -> None: + parent_slot = parent_bid.slot + parent_epoch = compute_epoch_at_slot(parent_slot) + + # Process execution requests from parent's payload. The execution + # requests are processed at state.slot (child's slot), not the parent's slot. + 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) + + # Settle the builder payment + if parent_epoch == get_current_epoch(state): + payment_index = SLOTS_PER_EPOCH + parent_slot % SLOTS_PER_EPOCH + settle_builder_payment(state, payment_index) + elif parent_epoch == get_previous_epoch(state): + payment_index = parent_slot % SLOTS_PER_EPOCH + settle_builder_payment(state, payment_index) + elif parent_bid.value > 0: + state.builder_pending_withdrawals.append( + BuilderPendingWithdrawal( + fee_recipient=parent_bid.fee_recipient, + amount=parent_bid.value, + builder_index=parent_bid.builder_index, + ) + ) + + # Update parent payload availability and latest block hash + state.execution_payload_availability[parent_slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1 + state.latest_block_hash = parent_bid.block_hash +``` + +##### New `process_parent_execution_payload` + +*Note*: This function validates and processes the parent's execution payload. +`process_parent_execution_payload` must be called before +`process_execution_payload_bid` (which overwrites +`state.latest_execution_payload_bid`). + +```python +def process_parent_execution_payload(state: BeaconState, block: BeaconBlock) -> None: + bid = block.body.signed_execution_payload_bid.message + parent_bid = state.latest_execution_payload_bid + requests = block.body.parent_execution_requests + + # True if this block built on the parent's full payload + is_parent_block_full = bid.parent_block_hash == parent_bid.block_hash + + if not is_parent_block_full: + # Parent was EMPTY -- no execution requests expected + assert requests == ExecutionRequests() + return + + # Parent was FULL -- verify the bid commitment and apply the payload + assert hash_tree_root(requests) == parent_bid.execution_requests_root + apply_parent_execution_payload(state, parent_bid, requests) +``` + #### Withdrawals ##### New `get_builder_withdrawals` @@ -1103,8 +1185,9 @@ def update_next_withdrawal_builder_index( *Note*: This is modified to only take the `state` as parameter. Withdrawals are deterministic given the beacon state, any execution payload that has the corresponding block as parent beacon block is required to honor these -withdrawals in the execution layer. `process_withdrawals` must be called before -`process_execution_payload_bid` as the latter function affects validator +withdrawals in the execution layer. `process_withdrawals` must be called after +`process_parent_execution_payload` (which updates `state.latest_block_hash`) and +before `process_execution_payload_bid` as the latter function affects validator balances. ```python @@ -1115,7 +1198,7 @@ def process_withdrawals( ) -> None: # [New in Gloas:EIP7732] # Return early if the parent block is empty - if not is_parent_block_full(state): + if state.latest_block_hash != state.latest_execution_payload_bid.block_hash: return # Get expected withdrawals @@ -1136,6 +1219,15 @@ def process_withdrawals( update_next_withdrawal_validator_index(state, expected.withdrawals) ``` +#### Execution payload + +##### Removed `process_execution_payload` + +`process_execution_payload` has been replaced by +`verify_execution_payload_envelope`, a pure verification helper called from +`on_execution_payload_envelope`. Payload processing is deferred to the next +beacon block via `process_parent_execution_payload`. + #### Execution payload bid ##### New `verify_execution_payload_bid_signature` @@ -1548,115 +1640,3 @@ def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSla slash_validator(state, header_1.proposer_index) ``` - -### Execution payload processing - -#### New `verify_execution_payload_envelope_signature` - -```python -def verify_execution_payload_envelope_signature( - state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope -) -> bool: - builder_index = signed_envelope.message.builder_index - if builder_index == BUILDER_INDEX_SELF_BUILD: - validator_index = state.latest_block_header.proposer_index - pubkey = state.validators[validator_index].pubkey - else: - pubkey = state.builders[builder_index].pubkey - - signing_root = compute_signing_root( - signed_envelope.message, get_domain(state, DOMAIN_BEACON_BUILDER) - ) - return bls.Verify(pubkey, signing_root, signed_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. - -```python -def process_execution_payload( - state: BeaconState, - # [Modified in Gloas:EIP7732] - # Removed `body` - # [New in Gloas:EIP7732] - signed_envelope: SignedExecutionPayloadEnvelope, - execution_engine: ExecutionEngine, - # [New in Gloas:EIP7732] - verify: bool = True, -) -> None: - envelope = signed_envelope.message - payload = envelope.payload - - # Verify signature - 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 - - # Verify consistency with the committed bid - committed_bid = state.latest_execution_payload_bid - assert envelope.builder_index == committed_bid.builder_index - assert committed_bid.prev_randao == payload.prev_randao - - # Verify consistency with expected withdrawals - assert hash_tree_root(payload.withdrawals) == hash_tree_root(state.payload_expected_withdrawals) - - # Verify the gas_limit - assert committed_bid.gas_limit == payload.gas_limit - # Verify the block hash - assert committed_bid.block_hash == payload.block_hash - # Verify consistency of the parent hash with respect to the previous execution payload - assert payload.parent_hash == state.latest_block_hash - # Verify timestamp - assert payload.timestamp == compute_time_at_slot(state, state.slot) - # 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 - assert execution_engine.verify_and_notify_new_payload( - NewPayloadRequest( - execution_payload=payload, - versioned_hashes=versioned_hashes, - parent_beacon_block_root=state.latest_block_header.parent_root, - execution_requests=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 - 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) -``` diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index d9fab6dff7..2e6562cf53 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -102,14 +102,17 @@ proposer what it promised whether it submits the payload or not. Builders can broadcast a payload bid for the current or the next slot's proposer to include. They produce a `SignedExecutionPayloadBid` as follows. -01. Set `bid.parent_block_hash` to the current head of the execution chain (this - can be obtained from the beacon state as `state.latest_block_hash`). -02. Set `bid.parent_block_root` to be the head of the consensus chain; this can +01. Set `bid.parent_block_hash` to the current head of the execution chain. Let + `parent_root = hash_tree_root(state.latest_block_header)`. This is + `state.latest_execution_payload_bid.block_hash` if + `should_extend_payload(store, parent_root)` is true, otherwise + `state.latest_execution_payload_bid.parent_block_hash`. +02. Set `bid.parent_block_root` to be the head of the consensus chain. This can be obtained from the beacon state as `hash_tree_root(state.latest_block_header)`. The `parent_block_root` and `parent_block_hash` must be compatible, in the sense that they both should - come from the same `state` by the method described in this and the previous - point. + come from the same `state` and `store` by the method described in this and + the previous point. 03. Construct an execution payload. This can be performed with an external execution engine via a call to `engine_getPayloadV5`. 04. Set `bid.block_hash` to be the block hash of the constructed payload, that @@ -134,6 +137,9 @@ to include. They produce a `SignedExecutionPayloadBid` as follows. be broadcast to the `execution_payload_bid` gossip topic. 12. Set `bid.blob_kzg_commitments` to be the `blobsbundle.commitments` field returned by `engine_getPayloadV5`. +13. Set `bid.execution_requests_root` to `hash_tree_root(execution_requests)`, + where `execution_requests` is the `ExecutionRequests` field returned by + `engine_getPayloadV5`. After building the `bid`, the builder obtains a `signature` of the bid by using: @@ -240,15 +246,7 @@ alias `bid` to be the committed `ExecutionPayloadBid` in 4. Set `envelope.beacon_block_root` to be `hash_tree_root(block)`. 5. Set `envelope.slot` to be `block.slot`. -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)`. - -After preparing the `envelope` the builder should sign the envelope using: +After preparing the `envelope` the builder signs it using: ```python def get_execution_payload_envelope_signature( diff --git a/specs/gloas/fork-choice.md b/specs/gloas/fork-choice.md index 0c332fab81..6311e9dfb9 100644 --- a/specs/gloas/fork-choice.md +++ b/specs/gloas/fork-choice.md @@ -14,6 +14,7 @@ - [Modified `Store`](#modified-store) - [Modified `get_forkchoice_store`](#modified-get_forkchoice_store) - [New `notify_ptc_messages`](#new-notify_ptc_messages) + - [New `is_payload_verified`](#new-is_payload_verified) - [New `is_payload_timely`](#new-is_payload_timely) - [New `is_payload_data_available`](#new-is_payload_data_available) - [New `get_parent_payload_status`](#new-get_parent_payload_status) @@ -39,6 +40,8 @@ - [Modified `get_sync_message_due_ms`](#modified-get_sync_message_due_ms) - [Modified `get_contribution_due_ms`](#modified-get_contribution_due_ms) - [New `get_payload_attestation_due_ms`](#new-get_payload_attestation_due_ms) + - [New `verify_execution_payload_envelope_signature`](#new-verify_execution_payload_envelope_signature) + - [New `verify_execution_payload_envelope`](#new-verify_execution_payload_envelope) - [Handlers](#handlers) - [Modified `on_block`](#modified-on_block) - [Modified `is_data_available`](#modified-is_data_available) @@ -122,10 +125,6 @@ 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. - ```python @dataclass class Store(object): @@ -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: Dict[Root, ExecutionPayloadEnvelope] = field(default_factory=dict) # [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={}, # [New in Gloas:EIP7732] payload_timeliness_vote={ anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE)) @@ -219,6 +218,18 @@ def notify_ptc_messages( ) ``` +### New `is_payload_verified` + +```python +def is_payload_verified(store: Store, root: Root) -> bool: + """ + Return whether the execution payload envelope for the beacon block with + root ``root`` has been locally delivered and verified via + ``on_execution_payload_envelope``. + """ + return root in store.payloads +``` + ### New `is_payload_timely` ```python @@ -232,7 +243,7 @@ def is_payload_timely(store: Store, root: Root) -> bool: # If the payload is not locally available, the payload # is not considered available regardless of the PTC vote - if root not in store.payload_states: + if not is_payload_verified(store, root): return False return sum(store.payload_timeliness_vote[root]) > PAYLOAD_TIMELY_THRESHOLD @@ -251,7 +262,7 @@ def is_payload_data_available(store: Store, root: Root) -> bool: # If the payload is not locally available, the blob data # is not considered available regardless of the PTC vote - if root not in store.payload_states: + if not is_payload_verified(store, root): return False return sum(store.payload_data_availability_vote[root]) > DATA_AVAILABILITY_TIMELY_THRESHOLD @@ -351,6 +362,8 @@ extending the payload. ```python def should_extend_payload(store: Store, root: Root) -> bool: + if not is_payload_verified(store, root): + return False proposer_root = store.proposer_boost_root return ( (is_payload_timely(store, root) and is_payload_data_available(store, root)) @@ -488,7 +501,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 is_payload_verified(store, node.root): children.append(ForkChoiceNode(root=node.root, payload_status=PAYLOAD_STATUS_FULL)) return children else: @@ -504,8 +517,8 @@ def get_node_children( ### Modified `get_head` -*Note*: `get_head` is a modified to use the new `get_weight` function. It -returns the `ForkChoiceNode` object corresponding to the head block. +*Note*: `get_head` is modified to use the new `get_weight` function. It returns +the `ForkChoiceNode` object corresponding to the head block. ```python def get_head(store: Store) -> ForkChoiceNode: @@ -604,7 +617,7 @@ def validate_on_attestation(store: Store, attestation: Attestation, is_from_bloc # [New in Gloas:EIP7732] # If attesting for a full node, the payload must be known if attestation.data.index == 1: - assert attestation.data.beacon_block_root in store.payload_states + assert is_payload_verified(store, attestation.data.beacon_block_root) # LMD vote must be consistent with FFG vote target assert target.root == get_checkpoint_block( @@ -713,14 +726,78 @@ def get_payload_attestation_due_ms() -> uint64: return get_slot_component_duration_ms(PAYLOAD_ATTESTATION_DUE_BPS) ``` +### New `verify_execution_payload_envelope_signature` + +```python +def verify_execution_payload_envelope_signature( + state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope +) -> bool: + builder_index = signed_envelope.message.builder_index + if builder_index == BUILDER_INDEX_SELF_BUILD: + validator_index = state.latest_block_header.proposer_index + pubkey = state.validators[validator_index].pubkey + else: + pubkey = state.builders[builder_index].pubkey + + signing_root = compute_signing_root( + signed_envelope.message, get_domain(state, DOMAIN_BEACON_BUILDER) + ) + return bls.Verify(pubkey, signing_root, signed_envelope.signature) +``` + +### New `verify_execution_payload_envelope` + +```python +def verify_execution_payload_envelope( + state: BeaconState, + signed_envelope: SignedExecutionPayloadEnvelope, + execution_engine: ExecutionEngine, +) -> None: + envelope = signed_envelope.message + payload = envelope.payload + + # Verify signature + assert verify_execution_payload_envelope_signature(state, signed_envelope) + + # Verify consistency with the beacon block + header = copy(state.latest_block_header) + header.state_root = hash_tree_root(state) + assert envelope.beacon_block_root == hash_tree_root(header) + assert envelope.slot == state.slot + + # Verify consistency with the committed bid + bid = state.latest_execution_payload_bid + assert envelope.builder_index == bid.builder_index + assert payload.prev_randao == bid.prev_randao + assert payload.gas_limit == bid.gas_limit + assert payload.block_hash == bid.block_hash + assert hash_tree_root(envelope.execution_requests) == bid.execution_requests_root + + # Verify the execution payload is valid + assert payload.parent_hash == state.latest_block_hash + assert payload.timestamp == compute_time_at_slot(state, state.slot) + assert hash_tree_root(payload.withdrawals) == hash_tree_root(state.payload_expected_withdrawals) + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=[ + kzg_commitment_to_versioned_hash(commitment) + for commitment in bid.blob_kzg_commitments + ], + parent_beacon_block_root=state.latest_block_header.parent_root, + execution_requests=envelope.execution_requests, + ) + ) +``` + ## Handlers ### 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 assert that the parent payload has +been verified (`is_payload_verified`) when the block builds on a full parent. 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: @@ -731,17 +808,10 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Parent block must be known assert block.parent_root in store.block_states - # Check if this blocks builds on empty or full parent block - 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 + # If this block builds on the parent's full payload, that payload must + # have been verified by on_execution_payload_envelope if is_parent_node_full(store, block): - assert block.parent_root in store.payload_states - state = copy(store.payload_states[block.parent_root]) - else: - assert bid.parent_block_hash == parent_bid.parent_block_hash - state = copy(store.block_states[block.parent_root]) + assert is_payload_verified(store, 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) @@ -758,6 +828,9 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: ) assert store.finalized_checkpoint.root == finalized_checkpoint_block + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[block.parent_root]) + # Check the block is valid and compute the post-state block_root = hash_tree_root(block) state_transition(state, signed_block, True) @@ -823,14 +896,13 @@ def on_execution_payload_envelope( # 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 - state = copy(store.block_states[envelope.beacon_block_root]) + state = store.block_states[envelope.beacon_block_root] - # Process the execution payload - process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) + # Verify the execution payload envelope + verify_execution_payload_envelope(state, signed_envelope, EXECUTION_ENGINE) - # Add new state for this payload to the store - store.payload_states[envelope.beacon_block_root] = state + # Add execution payload envelope to the store + store.payloads[envelope.beacon_block_root] = envelope ``` ### New `on_payload_attestation_message` diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 87cc0f088f..c1182d89b8 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -182,6 +182,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()), ), # [New in Gloas:EIP7732] payload_expected_withdrawals=[], diff --git a/specs/gloas/p2p-interface.md b/specs/gloas/p2p-interface.md index a4d7f7821a..55ef5f5050 100644 --- a/specs/gloas/p2p-interface.md +++ b/specs/gloas/p2p-interface.md @@ -317,6 +317,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 01e431ca67..ac0257fd6b 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) @@ -193,7 +194,10 @@ top of a `state` MUST take the following actions in order to construct the `bid.value` MUST be zero. - The builder balance can cover the `bid.value`. - The `bid.slot` is for the proposal block slot. - - The `bid.parent_block_hash` equals the state's `latest_block_hash`. + - The `bid.parent_block_hash` equals + `state.latest_execution_payload_bid.block_hash` if + `should_extend_payload(store, block.parent_root)` is true, otherwise + `state.latest_execution_payload_bid.parent_block_hash`. - The `bid.parent_block_root` equals the current block's `parent_root`. - Select one bid and set `block.body.signed_execution_payload_bid = signed_execution_payload_bid`. @@ -219,10 +223,32 @@ 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 `should_extend_payload(store, block.parent_root)` is true (the proposer is + building on the parent's full payload), set `parent_execution_requests` to + `store.payloads[block.parent_root].execution_requests`. +- Otherwise (the proposer is building on the parent's empty variant), set + `parent_execution_requests` to an empty `ExecutionRequests()`. + ##### ExecutionPayload +*Note*: `prepare_execution_payload` is modified in Gloas to take `store` as an +additional parameter. It consults `should_extend_payload` to decide whether to +build on the parent's full payload or its empty variant, selecting both the +withdrawals source and the execution head for the new payload. When building on +a full parent, `apply_parent_execution_payload` is called so that withdrawals +are computed against the post-processing state. + ```python def prepare_execution_payload( + # [New in Gloas:EIP7732] + store: Store, state: BeaconState, safe_block_hash: Hash32, finalized_block_hash: Hash32, @@ -230,10 +256,19 @@ def prepare_execution_payload( execution_engine: ExecutionEngine, ) -> Optional[PayloadId]: # [New in Gloas:EIP7732] - if is_parent_block_full(state): + parent_bid = state.latest_execution_payload_bid + parent_root = hash_tree_root(state.latest_block_header) + if should_extend_payload(store, parent_root): + envelope = store.payloads[parent_root] + # Make a copy of the state to avoid mutability issues + state = copy(state) + # Apply parent payload before computing withdrawals + apply_parent_execution_payload(state, parent_bid, envelope.execution_requests) withdrawals = get_expected_withdrawals(state).withdrawals + head_block_hash = parent_bid.block_hash else: withdrawals = state.payload_expected_withdrawals + head_block_hash = parent_bid.parent_block_hash # Set the forkchoice head and initiate the payload build process payload_attributes = PayloadAttributes( @@ -246,7 +281,7 @@ def prepare_execution_payload( ) return execution_engine.notify_forkchoice_updated( # [Modified in Gloas:EIP7732] - head_block_hash=state.latest_block_hash, + head_block_hash=head_block_hash, safe_block_hash=safe_block_hash, finalized_block_hash=finalized_block_hash, payload_attributes=payload_attributes, diff --git a/specs/heze/beacon-chain.md b/specs/heze/beacon-chain.md index b4ecec8dcb..2589bafd0c 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 9654575488..e4909bd3d4 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: Dict[Root, ExecutionPayloadEnvelope] = field(default_factory=dict) 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={}, 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 not is_payload_verified(store, root): return False return store.payload_inclusion_list_satisfaction[root] @@ -301,11 +301,10 @@ def on_execution_payload_envelope( # 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 - state = copy(store.block_states[envelope.beacon_block_root]) + state = store.block_states[envelope.beacon_block_root] - # Process the execution payload - process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) + # Verify the execution payload envelope + verify_execution_payload_envelope(state, signed_envelope, EXECUTION_ENGINE) # [New in Heze:EIP7805] # Check if this payload satisfies the inclusion list constraints @@ -314,6 +313,6 @@ def on_execution_payload_envelope( 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 + # Add execution payload envelope to the store + store.payloads[envelope.beacon_block_root] = envelope ``` diff --git a/specs/heze/fork.md b/specs/heze/fork.md index e3ada1c15d..a3d41571f0 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/specs/heze/validator.md b/specs/heze/validator.md index adf39e928e..6c2ce0bcbf 100644 --- a/specs/heze/validator.md +++ b/specs/heze/validator.md @@ -142,18 +142,33 @@ inclusion list constraints with respect to the inclusion lists gathered up to ```python def prepare_execution_payload( + store: Store, state: BeaconState, safe_block_hash: Hash32, finalized_block_hash: Hash32, suggested_fee_recipient: ExecutionAddress, execution_engine: ExecutionEngine, ) -> Optional[PayloadId]: + parent_bid = state.latest_execution_payload_bid + parent_root = hash_tree_root(state.latest_block_header) + if should_extend_payload(store, parent_root): + envelope = store.payloads[parent_root] + # Make a copy of the state to avoid mutability issues + state = copy(state) + # Apply parent payload before computing withdrawals + apply_parent_execution_payload(state, parent_bid, envelope.execution_requests) + withdrawals = get_expected_withdrawals(state).withdrawals + head_block_hash = parent_bid.block_hash + else: + withdrawals = state.payload_expected_withdrawals + head_block_hash = parent_bid.parent_block_hash + # Set the forkchoice head and initiate the payload build process payload_attributes = PayloadAttributes( timestamp=compute_time_at_slot(state, state.slot), prev_randao=get_randao_mix(state, get_current_epoch(state)), suggested_fee_recipient=suggested_fee_recipient, - withdrawals=get_expected_withdrawals(state).withdrawals, + withdrawals=withdrawals, parent_beacon_block_root=hash_tree_root(state.latest_block_header), # [New in Heze:EIP7805] inclusion_list_transactions=get_inclusion_list_transactions( @@ -161,7 +176,7 @@ def prepare_execution_payload( ), ) return execution_engine.notify_forkchoice_updated( - head_block_hash=state.latest_block_hash, + head_block_hash=head_block_hash, safe_block_hash=safe_block_hash, finalized_block_hash=finalized_block_hash, payload_attributes=payload_attributes, 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 9a5444516c..6c17881603 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 @@ -4,7 +4,6 @@ expect_assertion_error, spec_state_test, with_all_phases_from_to, - with_bellatrix_and_later, with_phases, ) from eth_consensus_specs.test.helpers.constants import ( @@ -19,8 +18,7 @@ compute_el_block_hash, get_execution_payload_header, ) -from eth_consensus_specs.test.helpers.forks import is_post_eip8025, is_post_gloas -from eth_consensus_specs.test.helpers.keys import builder_privkeys, privkeys +from eth_consensus_specs.test.helpers.forks import is_post_eip8025 from eth_consensus_specs.test.helpers.state import next_slot @@ -36,32 +34,8 @@ def run_execution_payload_processing( If ``valid == False``, run expecting ``AssertionError`` """ # 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): - 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: - privkey = builder_privkeys[envelope.builder_index] - signature = spec.get_execution_payload_envelope_signature( - state, - envelope, - privkey, - ) - signed_envelope = spec.SignedExecutionPayloadEnvelope( - message=envelope, - signature=signature, - ) - yield "signed_envelope", signed_envelope - else: - body = spec.BeaconBlockBody(execution_payload=execution_payload) - yield "body", body + body = spec.BeaconBlockBody(execution_payload=execution_payload) + yield "body", body yield "pre", state yield "execution", {"execution_valid": execution_valid} @@ -77,9 +51,7 @@ def verify_and_notify_new_payload(self, new_payload_request) -> bool: def call_process_execution_payload(): engine = TestEngine() - if is_post_gloas(spec): - spec.process_execution_payload(state, signed_envelope, engine) - elif is_post_eip8025(spec): + if is_post_eip8025(spec): spec.process_execution_payload(state, body, engine, spec.PROOF_ENGINE) else: spec.process_execution_payload(state, body, engine) @@ -96,12 +68,9 @@ def call_process_execution_payload(): yield "post", state - if is_post_gloas(spec): - assert state.latest_block_hash == execution_payload.block_hash - else: - assert state.latest_execution_payload_header == get_execution_payload_header( - spec, state, body.execution_payload - ) + assert state.latest_execution_payload_header == get_execution_payload_header( + spec, state, body.execution_payload + ) def run_success_test(spec, state): @@ -186,7 +155,7 @@ def test_bad_parent_hash_first_payload(spec, state): yield from run_execution_payload_processing(spec, state, execution_payload) -@with_bellatrix_and_later +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_invalid_bad_parent_hash_regular_payload(spec, state): state = build_state_with_complete_transition(spec, state) @@ -209,14 +178,14 @@ def run_bad_prev_randao_test(spec, state): yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) -@with_bellatrix_and_later +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_invalid_bad_prev_randao_first_payload(spec, state): state = build_state_with_incomplete_transition(spec, state) yield from run_bad_prev_randao_test(spec, state) -@with_bellatrix_and_later +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_invalid_bad_pre_randao_regular_payload(spec, state): state = build_state_with_complete_transition(spec, state) @@ -235,14 +204,14 @@ def run_bad_everything_test(spec, state): yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) -@with_bellatrix_and_later +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_invalid_bad_everything_first_payload(spec, state): state = build_state_with_incomplete_transition(spec, state) yield from run_bad_everything_test(spec, state) -@with_bellatrix_and_later +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_invalid_bad_everything_regular_payload(spec, state): state = build_state_with_complete_transition(spec, state) @@ -264,28 +233,28 @@ def run_bad_timestamp_test(spec, state, is_future): yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) -@with_bellatrix_and_later +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_invalid_future_timestamp_first_payload(spec, state): state = build_state_with_incomplete_transition(spec, state) yield from run_bad_timestamp_test(spec, state, is_future=True) -@with_bellatrix_and_later +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_invalid_future_timestamp_regular_payload(spec, state): state = build_state_with_complete_transition(spec, state) yield from run_bad_timestamp_test(spec, state, is_future=True) -@with_bellatrix_and_later +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_invalid_past_timestamp_first_payload(spec, state): state = build_state_with_incomplete_transition(spec, state) yield from run_bad_timestamp_test(spec, state, is_future=False) -@with_bellatrix_and_later +@with_all_phases_from_to(BELLATRIX, GLOAS) @spec_state_test def test_invalid_past_timestamp_regular_payload(spec, state): state = build_state_with_complete_transition(spec, state) @@ -329,11 +298,10 @@ def run_non_empty_transactions_test(spec, state): yield from run_execution_payload_processing(spec, state, execution_payload) - if not is_post_gloas(spec): - assert ( - state.latest_execution_payload_header.transactions_root - == execution_payload.transactions.hash_tree_root() - ) + assert ( + state.latest_execution_payload_header.transactions_root + == execution_payload.transactions.hash_tree_root() + ) @with_all_phases_from_to(BELLATRIX, GLOAS) @@ -360,11 +328,10 @@ def run_zero_length_transaction_test(spec, state): yield from run_execution_payload_processing(spec, state, execution_payload) - if not is_post_gloas(spec): - assert ( - state.latest_execution_payload_header.transactions_root - == execution_payload.transactions.hash_tree_root() - ) + assert ( + state.latest_execution_payload_header.transactions_root + == execution_payload.transactions.hash_tree_root() + ) @with_all_phases_from_to(BELLATRIX, GLOAS) @@ -385,11 +352,6 @@ def run_randomized_non_validated_execution_fields_test(spec, state, rng, executi next_slot(spec, state) execution_payload = build_randomized_execution_payload(spec, state, rng) - if is_post_gloas(spec): - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_block_hash = execution_payload.parent_hash - yield from run_execution_payload_processing( spec, state, execution_payload, valid=execution_valid, execution_valid=execution_valid ) diff --git a/tests/core/pyspec/eth_consensus_specs/test/capella/block_processing/test_process_execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/capella/block_processing/test_process_execution_payload.py index e0d5c99b97..6ffb4aacc0 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/capella/block_processing/test_process_execution_payload.py +++ b/tests/core/pyspec/eth_consensus_specs/test/capella/block_processing/test_process_execution_payload.py @@ -3,8 +3,9 @@ ) from eth_consensus_specs.test.context import ( spec_state_test, - with_capella_and_later, + with_all_phases_from_to, ) +from eth_consensus_specs.test.helpers.constants import CAPELLA, GLOAS from eth_consensus_specs.test.helpers.execution_payload import ( build_empty_execution_payload, build_state_with_incomplete_transition, @@ -13,7 +14,7 @@ from eth_consensus_specs.test.helpers.state import next_slot -@with_capella_and_later +@with_all_phases_from_to(CAPELLA, GLOAS) @spec_state_test def test_invalid_bad_parent_hash_first_payload(spec, state): state = build_state_with_incomplete_transition(spec, state) 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..36b6ab0107 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 @@ -3,18 +3,18 @@ from eth_consensus_specs.test.context import ( expect_assertion_error, spec_state_test, - with_deneb_and_later, + with_all_phases_from_to, ) from eth_consensus_specs.test.helpers.blob import ( get_sample_blob_tx, ) +from eth_consensus_specs.test.helpers.constants import DENEB, GLOAS from eth_consensus_specs.test.helpers.execution_payload import ( build_empty_execution_payload, compute_el_block_hash, get_execution_payload_header, ) -from eth_consensus_specs.test.helpers.forks import is_post_eip8025, is_post_gloas -from eth_consensus_specs.test.helpers.keys import builder_privkeys, privkeys +from eth_consensus_specs.test.helpers.forks import is_post_eip8025 def run_execution_payload_processing( @@ -29,61 +29,11 @@ def run_execution_payload_processing( If ``valid == False``, run expecting ``AssertionError`` """ - # After Gloas the execution payload is no longer in the body - if is_post_gloas(spec): - envelope = spec.ExecutionPayloadEnvelope( - payload=execution_payload, - slot=state.slot, - builder_index=spec.BUILDER_INDEX_SELF_BUILD, - ) - kzg_list = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]( - blob_kzg_commitments - ) - # In Gloas, blob_kzg_commitments is stored in latest_execution_payload_bid, not latest_execution_payload_header - state.latest_execution_payload_bid.blob_kzg_commitments = kzg_list - state.latest_execution_payload_bid.builder_index = envelope.builder_index - # 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() - if envelope.builder_index == spec.BUILDER_INDEX_SELF_BUILD: - privkey = privkeys[state.latest_block_header.proposer_index] - else: - privkey = builder_privkeys[envelope.builder_index] - signature = spec.get_execution_payload_envelope_signature( - state, - envelope, - privkey, - ) - signed_envelope = spec.SignedExecutionPayloadEnvelope( - message=envelope, - signature=signature, - ) - yield "signed_envelope", signed_envelope - else: - body = spec.BeaconBlockBody( - blob_kzg_commitments=blob_kzg_commitments, - execution_payload=execution_payload, - ) - yield "body", body + body = spec.BeaconBlockBody( + blob_kzg_commitments=blob_kzg_commitments, + execution_payload=execution_payload, + ) + yield "body", body yield "pre", state yield "execution", {"execution_valid": execution_valid} @@ -99,9 +49,7 @@ def verify_and_notify_new_payload(self, new_payload_request) -> bool: def call_process_execution_payload(): engine = TestEngine() - if is_post_gloas(spec): - spec.process_execution_payload(state, signed_envelope, engine) - elif is_post_eip8025(spec): + if is_post_eip8025(spec): spec.process_execution_payload(state, body, engine, spec.PROOF_ENGINE) else: spec.process_execution_payload(state, body, engine) @@ -118,15 +66,9 @@ 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: - assert state.latest_execution_payload_header == get_execution_payload_header( - spec, state, execution_payload - ) + assert state.latest_execution_payload_header == get_execution_payload_header( + spec, state, execution_payload + ) """ @@ -136,7 +78,7 @@ def call_process_execution_payload(): """ -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_incorrect_blob_tx_type(spec, state): """ @@ -150,18 +92,12 @@ def test_incorrect_blob_tx_type(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash - yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_incorrect_transaction_length_1_extra_byte(spec, state): """ @@ -175,17 +111,12 @@ def test_incorrect_transaction_length_1_extra_byte(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_incorrect_transaction_length_1_byte_short(spec, state): """ @@ -199,17 +130,12 @@ def test_incorrect_transaction_length_1_byte_short(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_incorrect_transaction_length_empty(spec, state): """ @@ -223,17 +149,12 @@ def test_incorrect_transaction_length_empty(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_incorrect_transaction_length_32_extra_bytes(spec, state): """ @@ -247,17 +168,12 @@ def test_incorrect_transaction_length_32_extra_bytes(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_no_transactions_with_commitments(spec, state): """ @@ -270,17 +186,12 @@ def test_no_transactions_with_commitments(spec, state): execution_payload.transactions = [] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_incorrect_commitment(spec, state): """ @@ -294,17 +205,12 @@ def test_incorrect_commitment(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_no_commitments_for_transactions(spec, state): """ @@ -318,15 +224,12 @@ def test_no_commitments_for_transactions(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_incorrect_commitments_order(spec, state): """ @@ -340,17 +243,12 @@ def test_incorrect_commitments_order(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_incorrect_transaction_no_blobs_but_with_commitments(spec, state): """ @@ -366,16 +264,13 @@ def test_incorrect_transaction_no_blobs_but_with_commitments(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - # the transaction doesn't contain any blob, but commitments are provided yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_incorrect_block_hash(spec, state): """ @@ -389,17 +284,12 @@ def test_incorrect_block_hash(spec, state): execution_payload.block_hash = b"\x12" * 32 # incorrect block hash # CL itself doesn't verify EL block hash - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_zeroed_commitment(spec, state): """ @@ -415,17 +305,12 @@ def test_zeroed_commitment(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments ) -@with_deneb_and_later +@with_all_phases_from_to(DENEB, GLOAS) @spec_state_test def test_invalid_correct_input__execution_invalid(spec, state): """ @@ -438,11 +323,6 @@ def test_invalid_correct_input__execution_invalid(spec, state): execution_payload.transactions = [opaque_tx] execution_payload.block_hash = compute_el_block_hash(spec, execution_payload, state) - # Make the parent block full in Gloas and set up bid to match payload - if is_post_gloas(spec): - state.latest_block_hash = execution_payload.parent_hash - state.latest_execution_payload_bid.gas_limit = execution_payload.gas_limit - state.latest_execution_payload_bid.block_hash = execution_payload.block_hash yield from run_execution_payload_processing( spec, state, execution_payload, blob_kzg_commitments, valid=False, execution_valid=False ) 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 deleted file mode 100644 index 89962068c0..0000000000 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_execution_payload.py +++ /dev/null @@ -1,918 +0,0 @@ -from eth_consensus_specs.test.context import ( - always_bls, - expect_assertion_error, - spec_state_test, - with_gloas_and_later, -) -from eth_consensus_specs.test.helpers.deposits import ( - make_withdrawal_credentials, - prepare_deposit_request, -) -from eth_consensus_specs.test.helpers.execution_payload import ( - build_empty_execution_payload, -) -from eth_consensus_specs.test.helpers.keys import builder_privkeys, privkeys - - -def run_execution_payload_processing( - spec, state, signed_envelope, valid=True, execution_valid=True -): - """ - Run ``process_execution_payload``, yielding: - - pre-state ('pre') - - signed_envelope ('signed_envelope') - - execution details ('execution.yml') - - post-state ('post'). - If ``valid == False``, run expecting ``AssertionError`` - """ - yield "pre", state - yield "signed_envelope", signed_envelope - yield "execution", {"execution_valid": execution_valid} - - called_new_payload = False - - class TestEngine(spec.NoopExecutionEngine): - def verify_and_notify_new_payload(self, new_payload_request) -> bool: - nonlocal called_new_payload - called_new_payload = True - assert new_payload_request.execution_payload == signed_envelope.message.payload - return execution_valid - - if not valid: - expect_assertion_error( - lambda: spec.process_execution_payload( - state, signed_envelope, TestEngine(), verify=True - ) - ) - yield "post", None - return - - # Use full verification including state root - spec.process_execution_payload(state, signed_envelope, TestEngine(), verify=True) - - # Make sure we called the engine - assert called_new_payload - - yield "post", state - - -def prepare_execution_payload_envelope( - spec, - state, - builder_index=None, - slot=None, - beacon_block_root=None, - state_root=None, - execution_payload=None, - execution_requests=None, - valid_signature=True, -): - """ - Helper to create a signed execution payload envelope with customizable parameters. - Note: This should be called AFTER setting up the state with the committed bid. - """ - if builder_index is None: - builder_index = spec.BUILDER_INDEX_SELF_BUILD - - if slot is None: - slot = state.slot - - if beacon_block_root is None: - # Cache latest block header state root if not already set - if state.latest_block_header.state_root == spec.Root(): - state.latest_block_header.state_root = state.hash_tree_root() - beacon_block_root = state.latest_block_header.hash_tree_root() - - if execution_payload is None: - execution_payload = build_empty_execution_payload(spec, state) - - if execution_requests is None: - execution_requests = spec.ExecutionRequests( - deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD](), - withdrawals=spec.List[ - spec.WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD - ](), - consolidations=spec.List[ - spec.ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD - ](), - ) - - # 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: - if envelope.builder_index == spec.BUILDER_INDEX_SELF_BUILD: - privkey = privkeys[state.latest_block_header.proposer_index] - else: - privkey = builder_privkeys[envelope.builder_index] - signature = spec.get_execution_payload_envelope_signature( - state, - envelope, - privkey, - ) - else: - # Invalid signature - signature = spec.BLSSignature() - - return spec.SignedExecutionPayloadEnvelope( - message=envelope, - signature=signature, - ) - - -def setup_state_with_payload_bid( - spec, state, builder_index=None, value=None, prev_randao=None, blob_kzg_commitments=None -): - """ - Helper to setup state with a committed execution payload bid. - This simulates the state after process_execution_payload_bid has run. - """ - if builder_index is None: - builder_index = spec.BUILDER_INDEX_SELF_BUILD - - if value is None: - value = spec.Gwei(0) - - if prev_randao is None: - prev_randao = spec.get_randao_mix(state, spec.get_current_epoch(state)) - - if blob_kzg_commitments is None: - blob_kzg_commitments = spec.List[spec.KZGCommitment, spec.MAX_BLOB_COMMITMENTS_PER_BLOCK]() - - # Create and set the latest execution payload bid - bid = spec.ExecutionPayloadBid( - parent_block_hash=state.latest_block_hash, - parent_block_root=state.latest_block_header.hash_tree_root(), - block_hash=spec.Hash32(), - prev_randao=prev_randao, - fee_recipient=spec.ExecutionAddress(), - gas_limit=spec.uint64(60000000), - builder_index=builder_index, - slot=state.slot, - value=value, - blob_kzg_commitments=blob_kzg_commitments, - ) - state.latest_execution_payload_bid = bid - - # Setup withdrawals root - state.payload_expected_withdrawals = spec.List[ - spec.Withdrawal, spec.MAX_WITHDRAWALS_PER_PAYLOAD - ]() - - # Add pending payment if value > 0 - if value > 0: - pending_payment = spec.BuilderPendingPayment( - weight=0, - withdrawal=spec.BuilderPendingWithdrawal( - fee_recipient=bid.fee_recipient, - amount=value, - builder_index=builder_index, - ), - ) - state.builder_pending_payments[spec.SLOTS_PER_EPOCH + state.slot % spec.SLOTS_PER_EPOCH] = ( - pending_payment - ) - - -# -# Valid cases -# - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_valid(spec, state): - """ - Test valid execution payload processing with separate builder and non-zero payment - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(50000000)) - - # Create execution payload that matches the committed bid - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - 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 -@always_bls -def test_process_execution_payload_self_build_zero_value(spec, state): - """ - Test valid self-building scenario (zero value) - """ - # Setup state with committed bid (self-build, zero value) - setup_state_with_payload_bid(spec, state, spec.BUILDER_INDEX_SELF_BUILD, spec.Gwei(0)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, - state, - builder_index=spec.BUILDER_INDEX_SELF_BUILD, - 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 -@always_bls -def test_process_execution_payload_large_payment_churn_impact(spec, state): - """ - Test execution payload processing with large payment that impacts exit churn state - """ - builder_index = 0 - - # Use a very large payment (500 ETH) to ensure it impacts churn tracking - large_payment_amount = spec.Gwei(500000000000) - setup_state_with_payload_bid(spec, state, builder_index, large_payment_amount) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, - state, - builder_index=builder_index, - 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 -@always_bls -def test_process_execution_payload_with_blob_commitments(spec, state): - """ - Test execution payload processing with blob KZG commitments and separate builder - """ - builder_index = 0 - - # Create bid with blob commitments - setup_state_with_payload_bid( - spec, - state, - builder_index, - spec.Gwei(3000000), - blob_kzg_commitments=[spec.KZGCommitment(b"\x42" * 48), spec.KZGCommitment(b"\x43" * 48)], - ) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, - state, - builder_index=builder_index, - 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 -@always_bls -def test_process_execution_payload_with_execution_requests(spec, state): - """ - Test execution payload processing with execution requests and separate builder - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(4000000)) - - # Create execution requests - execution_requests = spec.ExecutionRequests( - deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]( - [ - spec.DepositRequest( - pubkey=spec.BLSPubkey(b"\x01" * 48), - withdrawal_credentials=spec.Bytes32(b"\x02" * 32), - amount=spec.Gwei(32000000000), # 32 ETH - signature=spec.BLSSignature(b"\x03" * 96), - index=spec.uint64(0), - ) - ] - ), - withdrawals=spec.List[spec.WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD]( - [ - spec.WithdrawalRequest( - source_address=spec.ExecutionAddress(b"\x04" * 20), - validator_pubkey=spec.BLSPubkey(b"\x05" * 48), - amount=spec.Gwei(16000000000), # 16 ETH - ) - ] - ), - consolidations=spec.List[ - spec.ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD - ]( - [ - spec.ConsolidationRequest( - source_address=spec.ExecutionAddress(b"\x06" * 20), - source_pubkey=spec.BLSPubkey(b"\x07" * 48), - target_pubkey=spec.BLSPubkey(b"\x08" * 48), - ) - ] - ), - ) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, - state, - builder_index=builder_index, - execution_payload=execution_payload, - 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 -@always_bls -def test_process_execution_payload_with_builder_deposit_after_pending_validator(spec, state): - """ - Test that a builder deposit cannot claim a pubkey that is already a pending validator earlier in the same envelope - """ - builder_index = 0 - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(0)) - - # Use a fresh pubkey that is neither a validator nor a builder - new_validator_index = len(state.validators) - amount = spec.MIN_DEPOSIT_AMOUNT - - # First deposit: regular validator credentials with valid signature. - # Since no validator/builder/pending deposit exists for this pubkey, it is queued as a pending validator. - 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, - ) - - # Second deposit: builder credentials for the same pubkey. - # `is_pending_validator` must see the first deposit (just queued) and route this one to the pending queue - # instead of the builder registry, preventing a builder from claiming a pubkey already in the validator queue. - 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 = spec.ExecutionRequests( - deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]( - [deposit_request_1, deposit_request_2] - ), - withdrawals=spec.List[spec.WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD](), - consolidations=spec.List[ - spec.ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD - ](), - ) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, - state, - builder_index=builder_index, - execution_payload=execution_payload, - 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 -# - - -@with_gloas_and_later -@spec_state_test -def test_process_execution_payload_invalid_signature(spec, state): - """ - Test invalid signature fails with separate builder and non-zero payment - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(2000000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, - state, - builder_index=builder_index, - execution_payload=execution_payload, - valid_signature=False, - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_wrong_beacon_block_root(spec, state): - """ - Test wrong beacon block root fails with separate builder - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(1500000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - wrong_beacon_block_root = spec.Root(b"\x42" * 32) - signed_envelope = prepare_execution_payload_envelope( - spec, - state, - builder_index=builder_index, - execution_payload=execution_payload, - beacon_block_root=wrong_beacon_block_root, - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_wrong_slot(spec, state): - """ - Test wrong slot fails with separate builder - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(2500000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, - state, - builder_index=builder_index, - execution_payload=execution_payload, - slot=state.slot + 1, # Wrong slot - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_wrong_builder_index(spec, state): - """ - Test wrong builder index fails with separate builders - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(3500000)) - - # Use different builder index in envelope - other_builder_index = 1 - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, - state, - builder_index=other_builder_index, # Wrong builder - execution_payload=execution_payload, - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_missing_expected_withdrawal(spec, state): - """ - Verify payload rejected when it omits a withdrawal expected by the state. - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(2600000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - withdrawal = spec.Withdrawal( - index=0, - validator_index=0, - address=b"\x22" * 20, - amount=spec.Gwei(1), - ) - state.payload_expected_withdrawals = spec.List[ - spec.Withdrawal, spec.MAX_WITHDRAWALS_PER_PAYLOAD - ]([withdrawal]) - execution_payload.withdrawals = spec.List[spec.Withdrawal, spec.MAX_WITHDRAWALS_PER_PAYLOAD]() - - signed_envelope = prepare_execution_payload_envelope( - spec, state, builder_index=builder_index, execution_payload=execution_payload - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_wrong_gas_limit(spec, state): - """ - Test wrong gas limit fails with separate builder - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(1800000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = ( - state.latest_execution_payload_bid.gas_limit + 1 - ) # Wrong gas limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, state, builder_index=builder_index, execution_payload=execution_payload - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_wrong_block_hash(spec, state): - """ - Test wrong block hash fails with separate builder - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(2200000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = spec.Hash32(b"\x42" * 32) # Wrong block hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, state, builder_index=builder_index, execution_payload=execution_payload - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_wrong_parent_hash(spec, state): - """ - Test wrong parent hash fails with separate builder - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(1600000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = spec.Hash32(b"\x42" * 32) # Wrong parent hash - - signed_envelope = prepare_execution_payload_envelope( - spec, state, builder_index=builder_index, execution_payload=execution_payload - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_wrong_prev_randao(spec, state): - """ - Test wrong prev_randao fails with separate builder - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(2100000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - execution_payload.prev_randao = spec.Bytes32(b"\x42" * 32) # Wrong prev_randao - - signed_envelope = prepare_execution_payload_envelope( - spec, state, builder_index=builder_index, execution_payload=execution_payload - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_bid_prev_randao_mismatch(spec, state): - """ - Test that committed_bid.prev_randao must equal payload.prev_randao - """ - builder_index = 0 - - # Setup bid with one prev_randao value - bid_prev_randao = spec.Bytes32(b"\x11" * 32) - setup_state_with_payload_bid( - spec, state, builder_index, spec.Gwei(2300000), prev_randao=bid_prev_randao - ) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - # Set payload with a different prev_randao value - execution_payload.prev_randao = spec.Bytes32(b"\x22" * 32) - - signed_envelope = prepare_execution_payload_envelope( - spec, state, builder_index=builder_index, execution_payload=execution_payload - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_wrong_timestamp(spec, state): - """ - Test wrong timestamp fails with separate builder - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(1900000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - execution_payload.timestamp = execution_payload.timestamp + 1 # Wrong timestamp - - signed_envelope = prepare_execution_payload_envelope( - spec, state, builder_index=builder_index, execution_payload=execution_payload - ) - - yield from run_execution_payload_processing(spec, state, signed_envelope, valid=False) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_execution_payload_execution_engine_invalid(spec, state): - """ - Test execution engine returns invalid with separate builder - """ - builder_index = 0 - - setup_state_with_payload_bid(spec, state, builder_index, spec.Gwei(3200000)) - - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.block_hash = state.latest_execution_payload_bid.block_hash - execution_payload.gas_limit = state.latest_execution_payload_bid.gas_limit - execution_payload.parent_hash = state.latest_block_hash - - signed_envelope = prepare_execution_payload_envelope( - spec, state, builder_index=builder_index, execution_payload=execution_payload - ) - - yield from run_execution_payload_processing( - spec, state, signed_envelope, valid=False, execution_valid=False - ) 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..a6e99e1209 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py @@ -0,0 +1,98 @@ +from eth_consensus_specs.test.context import ( + expect_assertion_error, + spec_state_test, + with_gloas_and_later, +) +from eth_consensus_specs.test.helpers.block import build_empty_block_for_next_slot +from eth_consensus_specs.test.helpers.execution_requests import ( + get_non_empty_execution_requests, +) +from tests.infra.helpers.withdrawals import set_parent_block_full + + +def run_parent_execution_payload_processing(spec, state, block, valid=True): + """ + Run ``process_parent_execution_payload`` against a prepared pre-state. + """ + 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 +@spec_state_test +def test_process_parent_execution_payload__empty_parent(spec, state): + """ + Test that process_parent_execution_payload returns early when the parent + block was empty (payload not delivered). + """ + block = build_empty_block_for_next_slot(spec, state) + + is_parent_block_full = ( + block.body.signed_execution_payload_bid.message.parent_block_hash + == state.latest_execution_payload_bid.block_hash + ) + assert not is_parent_block_full + + pre_latest_block_hash = state.latest_block_hash + parent_slot = state.latest_execution_payload_bid.slot + pre_availability = state.execution_payload_availability[ + parent_slot % spec.SLOTS_PER_HISTORICAL_ROOT + ] + + spec.process_slots(state, block.slot) + yield from run_parent_execution_payload_processing(spec, state, block) + + assert state.latest_block_hash == pre_latest_block_hash + assert ( + state.execution_payload_availability[parent_slot % spec.SLOTS_PER_HISTORICAL_ROOT] + == pre_availability + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_parent_execution_payload__full_parent(spec, state): + """ + Test that process_parent_execution_payload processes the parent's execution + requests and updates state when the parent block was full. + """ + set_parent_block_full(spec, state) + block = build_empty_block_for_next_slot(spec, state) + + parent_bid = state.latest_execution_payload_bid.copy() + parent_slot_index = parent_bid.slot % spec.SLOTS_PER_HISTORICAL_ROOT + state.execution_payload_availability[parent_slot_index] = 0b0 + + spec.process_slots(state, block.slot) + yield from run_parent_execution_payload_processing(spec, state, block) + + assert state.latest_block_hash == parent_bid.block_hash + assert state.execution_payload_availability[parent_slot_index] == 0b1 + + +@with_gloas_and_later +@spec_state_test +def test_process_parent_execution_payload__empty_parent_requires_empty_requests(spec, state): + """ + Test that when parent is empty, parent_execution_requests must be empty. + """ + block = build_empty_block_for_next_slot(spec, state) + + is_parent_block_full = ( + block.body.signed_execution_payload_bid.message.parent_block_hash + == state.latest_execution_payload_bid.block_hash + ) + assert not is_parent_block_full + + block.body.parent_execution_requests = get_non_empty_execution_requests(spec) + + spec.process_slots(state, block.slot) + yield from run_parent_execution_payload_processing(spec, state, block, valid=False) diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_withdrawals.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_withdrawals.py index 2bf438afb9..d5428ae194 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_withdrawals.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_withdrawals.py @@ -1065,6 +1065,10 @@ def test_full_builder_payload_reserves_sweep_slot(spec, state): if validator.withdrawal_credentials[0:1] == spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX: state.balances[i] = min(state.balances[i], spec.MAX_EFFECTIVE_BALANCE) + # Setup: Simulate parent being FULL so process_withdrawals runs (deferred + # processing otherwise returns early when parent was EMPTY). + state.latest_block_hash = state.latest_execution_payload_bid.block_hash + # Verify setup: One slot reserved for sweep, so only MAX - 1 builder withdrawals expected_result = spec.get_expected_withdrawals(state) expected_builder_withdrawals = spec.MAX_WITHDRAWALS_PER_PAYLOAD - 1 diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/fork_choice/test_on_execution_payload_envelope.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/fork_choice/test_on_execution_payload_envelope.py index 4b9841e91b..acb6e6e51d 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/fork_choice/test_on_execution_payload_envelope.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/fork_choice/test_on_execution_payload_envelope.py @@ -6,6 +6,7 @@ build_empty_block_for_next_slot, ) from eth_consensus_specs.test.helpers.execution_payload import ( + build_empty_execution_payload, build_signed_execution_payload_envelope, ) from eth_consensus_specs.test.helpers.fork_choice import ( @@ -16,14 +17,71 @@ on_tick_and_append_step, tick_and_add_block, ) +from eth_consensus_specs.test.helpers.keys import builder_privkeys, privkeys from eth_consensus_specs.test.helpers.state import ( state_transition_and_sign_block, ) +def _add_block_and_get_root(spec, state, store, test_steps): + """Add a block to the store and return (signed_block, block_root).""" + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + block_root = signed_block.message.hash_tree_root() + return signed_block, block_root + + +def _build_invalid_envelope(spec, state, block_root, signed_block, **overrides): + """Build a signed envelope with optional field overrides to make it invalid.""" + builder_index = signed_block.message.body.signed_execution_payload_bid.message.builder_index + bid = state.latest_execution_payload_bid + + payload = build_empty_execution_payload(spec, state) + payload.block_hash = bid.block_hash + payload.gas_limit = bid.gas_limit + payload.parent_hash = state.latest_block_hash + + # Apply payload-level overrides + for key in ( + "block_hash", + "gas_limit", + "parent_hash", + "prev_randao", + "timestamp", + "withdrawals", + ): + if key in overrides: + setattr(payload, key, overrides.pop(key)) + + envelope = spec.ExecutionPayloadEnvelope( + beacon_block_root=overrides.pop("beacon_block_root", block_root), + payload=payload, + execution_requests=overrides.pop("execution_requests", spec.ExecutionRequests()), + builder_index=overrides.pop("builder_index", builder_index), + slot=overrides.pop("slot", signed_block.message.slot), + ) + + if overrides.pop("valid_signature", True): + if envelope.builder_index == spec.BUILDER_INDEX_SELF_BUILD: + privkey = privkeys[signed_block.message.proposer_index] + else: + privkey = builder_privkeys[envelope.builder_index] + signature = spec.get_execution_payload_envelope_signature(state, envelope, privkey) + else: + signature = spec.BLSSignature() + + return spec.SignedExecutionPayloadEnvelope(message=envelope, signature=signature) + + +# +# Valid cases +# + + @with_gloas_and_later @spec_state_test -def test_on_execution_payload_envelope(spec, state): +def test_on_execution_payload_envelope__valid(spec, state): test_steps = [] # Initialization @@ -37,17 +95,14 @@ def test_on_execution_payload_envelope(spec, state): anchor_root = get_anchor_root(spec, state) check_head_against_root(spec, store, anchor_root) - # Genesis head has FULL payload status + # Genesis head has EMPTY payload status (no envelope in store.payloads) head = spec.get_head(store) - assert head.payload_status == spec.PAYLOAD_STATUS_FULL + assert head.payload_status == spec.PAYLOAD_STATUS_EMPTY # On receiving a block of `GENESIS_SLOT + 1` slot - block = build_empty_block_for_next_slot(spec, state) - signed_block = state_transition_and_sign_block(spec, state, block) - yield from tick_and_add_block(spec, store, signed_block, test_steps) + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) # Verify block was added to store - block_root = signed_block.message.hash_tree_root() assert block_root in store.blocks assert block_root in store.block_states assert block_root in store.payload_timeliness_vote @@ -61,17 +116,13 @@ def test_on_execution_payload_envelope(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 # On receiving a block of next slot, chain continues after payload reveal - block_2 = build_empty_block_for_next_slot(spec, state) - signed_block_2 = state_transition_and_sign_block(spec, state, block_2) - yield from tick_and_add_block(spec, store, signed_block_2, test_steps) - - block_2_root = signed_block_2.message.hash_tree_root() + _, block_2_root = yield from _add_block_and_get_root(spec, state, store, test_steps) check_head_against_root(spec, store, block_2_root) # Head moved to block 2 with EMPTY status @@ -79,3 +130,334 @@ def test_on_execution_payload_envelope(spec, state): assert head.payload_status == spec.PAYLOAD_STATUS_EMPTY yield "steps", test_steps + + +# +# Invalid cases — ordered to match asserts in verify_execution_payload_envelope +# + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_signature(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + valid_signature=False, + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_beacon_block_root(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + beacon_block_root=spec.Root(b"\x42" * 32), + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_slot(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + slot=state.slot + 1, + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_builder_index(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + builder_index=1, + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_prev_randao(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + prev_randao=spec.Bytes32(b"\x42" * 32), + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_execution_requests_root(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + # Build envelope with non-empty requests but bid commits to empty requests + non_empty_requests = spec.ExecutionRequests( + deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]( + [ + spec.DepositRequest( + pubkey=spec.BLSPubkey(b"\x01" * 48), + withdrawal_credentials=spec.Bytes32(b"\x02" * 32), + amount=spec.Gwei(32000000000), + signature=spec.BLSSignature(b"\x03" * 96), + index=spec.uint64(0), + ) + ] + ), + withdrawals=spec.List[spec.WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD](), + consolidations=spec.List[ + spec.ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD + ](), + ) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + execution_requests=non_empty_requests, + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_withdrawals(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + # Inject an expected withdrawal into the store's block state so the + # envelope's empty withdrawals list causes a mismatch + block_state = store.block_states[block_root] + withdrawal = spec.Withdrawal( + index=0, validator_index=0, address=b"\x22" * 20, amount=spec.Gwei(1) + ) + block_state.payload_expected_withdrawals = spec.List[ + spec.Withdrawal, spec.MAX_WITHDRAWALS_PER_PAYLOAD + ]([withdrawal]) + + # Build a normal envelope (empty withdrawals won't match the expected one) + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_gas_limit(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + gas_limit=state.latest_execution_payload_bid.gas_limit + 1, + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_block_hash(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + block_hash=spec.Hash32(b"\x42" * 32), + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_parent_hash(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + parent_hash=spec.Hash32(b"\x42" * 32), + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps + + +@with_gloas_and_later +@spec_state_test +def test_on_execution_payload_envelope__wrong_timestamp(spec, state): + test_steps = [] + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield "anchor_state", state + yield "anchor_block", anchor_block + + current_time = state.slot * (spec.config.SLOT_DURATION_MS // 1000) + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + signed_block, block_root = yield from _add_block_and_get_root(spec, state, store, test_steps) + + envelope = _build_invalid_envelope( + spec, + state, + block_root, + signed_block, + timestamp=spec.compute_time_at_slot(state, state.slot) + 1, + ) + yield from add_execution_payload(spec, store, envelope, test_steps, valid=False) + + assert block_root not in store.payloads + + yield "steps", test_steps 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..0432135ff7 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 @@ -3,9 +3,15 @@ with_gloas_and_later, ) from eth_consensus_specs.test.helpers.block import ( + build_empty_block, build_empty_block_for_next_slot, ) +from eth_consensus_specs.test.helpers.execution_requests import ( + get_non_empty_execution_requests, +) +from eth_consensus_specs.test.helpers.keys import builder_privkeys, privkeys from eth_consensus_specs.test.helpers.state import ( + next_epoch_with_full_participation, state_transition_and_sign_block, ) from eth_consensus_specs.test.helpers.withdrawals import ( @@ -58,16 +64,17 @@ def _setup_missed_payload_with_withdrawals(spec, state, num_withdrawal_validator assert len(block_1_withdrawals) > 0 # Payload for Block 1 was not delivered, so parent is empty for Block 2 - assert not spec.is_parent_block_full(state) + is_parent_block_full = state.latest_block_hash == state.latest_execution_payload_bid.block_hash + assert not is_parent_block_full return pre_state, signed_block_1, block_1_withdrawals def _attempt_payload_with_withdrawals(spec, state, withdrawals): """ - Attempt to process a payload for the current slot with the given withdrawals. - Uses verify=False to skip signature checks (we only care about withdrawal matching). + Attempt to verify a payload for the current slot with the given withdrawals. Operates on a copy to avoid mutating the test state. + BLS is disabled in tests by default, so signature verification passes. Returns True if accepted, False if rejected. """ @@ -85,28 +92,32 @@ def _attempt_payload_with_withdrawals(spec, state, withdrawals): ) # Cache state root for beacon_block_root computation - # (matches what process_execution_payload does internally) - if test_state.latest_block_header.state_root == spec.Root(): - test_state.latest_block_header.state_root = test_state.hash_tree_root() + header = test_state.latest_block_header.copy() + header.state_root = test_state.hash_tree_root() envelope = spec.ExecutionPayloadEnvelope( payload=payload, execution_requests=spec.ExecutionRequests(), builder_index=committed_bid.builder_index, - beacon_block_root=test_state.latest_block_header.hash_tree_root(), + beacon_block_root=header.hash_tree_root(), slot=test_state.slot, - state_root=spec.Root(), ) + if envelope.builder_index == spec.BUILDER_INDEX_SELF_BUILD: + privkey = privkeys[test_state.latest_block_header.proposer_index] + else: + privkey = builder_privkeys[envelope.builder_index] + signature = spec.get_execution_payload_envelope_signature(test_state, envelope, privkey) + signed_envelope = spec.SignedExecutionPayloadEnvelope( message=envelope, - signature=spec.BLSSignature(), + signature=signature, ) engine = spec.NoopExecutionEngine() try: - spec.process_execution_payload(test_state, signed_envelope, engine, verify=False) + spec.verify_execution_payload_envelope(test_state, signed_envelope, engine) return True except AssertionError: return False @@ -230,3 +241,101 @@ def test_missed_payload_next_block_without_withdrawals_unsatisfying_payload(spec # An empty payload is rejected — it must include W_1 empty_withdrawals = spec.List[spec.Withdrawal, spec.MAX_WITHDRAWALS_PER_PAYLOAD]() assert not _attempt_payload_with_withdrawals(spec, state, empty_withdrawals) + + +@with_gloas_and_later +@spec_state_test +def test_process_parent_execution_payload__wrong_execution_requests_root(spec, state): + """ + Test that process_parent_execution_payload rejects a block whose + parent_execution_requests do not match parent_bid.execution_requests_root + when the parent block was full. + """ + set_parent_block_full(spec, state) + + # Build a valid block, then tamper with parent_execution_requests + block = build_empty_block_for_next_slot(spec, state) + + # Inject a non-empty deposit so the hash diverges from the committed root + block.body.parent_execution_requests = get_non_empty_execution_requests(spec) + + yield "pre", state + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield "blocks", [signed_block] + yield "post", None + + +@with_gloas_and_later +@spec_state_test +def test_builder_payment_after_missed_epochs(spec, state): + """ + Test that a builder is correctly charged when their canonical payload + is processed after 2+ epochs of missed blocks. + """ + # Advance to get finalization + for _ in range(4): + next_epoch_with_full_participation(spec, state) + assert state.finalized_checkpoint.epoch == 2 + + # Build Block 1 with a non-zero value bid from a builder + block_1 = build_empty_block_for_next_slot(spec, state) + builder_index = 0 + value = spec.Gwei(1000000) # 0.001 ETH + fee_recipient = b"\xab" * 20 + + bid = block_1.body.signed_execution_payload_bid.message + bid.builder_index = builder_index + bid.value = value + bid.fee_recipient = fee_recipient + bid.execution_requests_root = spec.hash_tree_root(spec.ExecutionRequests()) + + # Sign the bid with the builder's private key + signature = spec.get_execution_payload_bid_signature( + state, bid, builder_privkeys[builder_index] + ) + block_1.body.signed_execution_payload_bid = spec.SignedExecutionPayloadBid( + message=bid, + signature=signature, + ) + + # Ensure builder can cover the bid + state.builders[builder_index].balance = spec.MIN_DEPOSIT_AMOUNT + value + + yield "pre", state + + # Process Block 1 — creates a pending payment for the builder + signed_block_1 = state_transition_and_sign_block(spec, state, block_1) + + # Verify pending payment was created + payment_idx = spec.SLOTS_PER_EPOCH + block_1.slot % spec.SLOTS_PER_EPOCH + payment = state.builder_pending_payments[payment_idx] + assert payment.withdrawal.amount == value + assert payment.withdrawal.builder_index == builder_index + assert payment.weight == 0 + + # Builder delivers their payload — parent block becomes FULL + set_parent_block_full(spec, state) + pre_builder_balance = state.builders[builder_index].balance + + # Build Block 2 with 2+ epochs of missed slots. During the slot advancement, + # process_builder_pending_payments runs at each epoch boundary: + # 1st boundary: shifts payment from second half to first half + # 2nd boundary: checks quorum on first half — weight 0 < quorum → evicted + # When Block 2 is processed, parent is FULL so apply_parent_execution_payload + # runs. Since parent_epoch is older than previous_epoch, payment_index is None. + # The fix creates the withdrawal directly from the bid in this case. + block_1_epoch = spec.compute_epoch_at_slot(block_1.slot) + block_2_slot = (block_1_epoch + 2) * spec.SLOTS_PER_EPOCH + 1 + block_2 = build_empty_block(spec, state, slot=block_2_slot) + signed_block_2 = state_transition_and_sign_block(spec, state, block_2) + + yield "blocks", [signed_block_1, signed_block_2] + yield "post", state + + # Verify apply_parent_execution_payload actually ran (parent was FULL) + parent_slot_index = bid.slot % spec.SLOTS_PER_HISTORICAL_ROOT + assert state.execution_payload_availability[parent_slot_index] == 0b1 + + # Verify the builder was charged — balance decreased by the bid value + assert state.builders[builder_index].balance == pre_builder_balance - value 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 b097fa8c74..f44662afb0 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 @@ -330,6 +330,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=spec.hash_tree_root(spec.ExecutionRequests()), ) @@ -482,36 +483,6 @@ 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 envelope_message = spec.ExecutionPayloadEnvelope( beacon_block_root=block_root, @@ -519,7 +490,6 @@ def build_signed_execution_payload_envelope(spec, state, block_root, signed_bloc 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/execution_requests.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/execution_requests.py new file mode 100644 index 0000000000..95500506dd --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/execution_requests.py @@ -0,0 +1,21 @@ +from eth_consensus_specs.test.helpers.deposits import prepare_deposit_request + + +def get_non_empty_execution_requests(spec): + deposit_request = prepare_deposit_request( + spec, + validator_index=0, + amount=spec.Gwei(32000000000), + index=spec.uint64(0), + signed=False, + ) + + return spec.ExecutionRequests( + deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]( + [deposit_request] + ), + withdrawals=spec.List[spec.WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD](), + consolidations=spec.List[ + spec.ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD + ](), + ) 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 abcc97493a..dad88f557f 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 @@ -252,8 +252,14 @@ def get_genesis_forkchoice_store_and_block(spec, genesis_state): assert genesis_state.slot == spec.GENESIS_SLOT genesis_block = spec.BeaconBlock(state_root=genesis_state.hash_tree_root()) if is_post_gloas(spec): + # Match the genesis block body bid to what ``genesis.py`` set on the + # state's committed bid; this keeps ``genesis_block`` consistent with + # ``genesis_state.latest_block_header`` (body_root). genesis_block.body.signed_execution_payload_bid.message.block_hash = ( - genesis_state.latest_block_hash + genesis_state.latest_execution_payload_bid.block_hash + ) + genesis_block.body.signed_execution_payload_bid.message.execution_requests_root = ( + genesis_state.latest_execution_payload_bid.execution_requests_root ) store = spec.get_forkchoice_store(genesis_state, genesis_block) return store, genesis_block @@ -407,7 +413,7 @@ def run_on_execution_payload_envelope(spec, store, signed_envelope, valid=True): # Verify the envelope was processed, block should now have FULL state envelope_root = signed_envelope.message.beacon_block_root - assert envelope_root in store.payload_states + assert envelope_root in store.payloads def get_execution_payload_envelope_file_name(signed_envelope): 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..3628d8478a 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/genesis.py @@ -197,8 +197,23 @@ 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 + # Initialize the latest_execution_payload_bid (match fork upgrade in fork.md). + # Genesis payload is EMPTY: ``latest_block_hash`` stays at default zero while + # ``bid.block_hash`` is set to the eth1 block hash, so the parent of any + # first post-genesis block is (correctly) treated as empty. + empty_requests_root = spec.hash_tree_root(spec.ExecutionRequests()) + state.latest_execution_payload_bid = spec.ExecutionPayloadBid( + block_hash=spec.Hash32(eth1_block_hash), + execution_requests_root=empty_requests_root, + ) genesis_block_body.signed_execution_payload_bid.message.block_hash = eth1_block_hash + genesis_block_body.signed_execution_payload_bid.message.execution_requests_root = ( + empty_requests_root + ) + # Recompute body_root after modifying the genesis block body + state.latest_block_header = spec.BeaconBlockHeader( + body_root=spec.hash_tree_root(genesis_block_body) + ) 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..a6e7b46795 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 not in store.payloads # genesis payload is EMPTY assert anchor_root in store.payload_timeliness_vote # Check PTC vote initialization @@ -452,7 +452,10 @@ def test_discard_equivocations_slashed_validator_censoring(spec, state): anchor_block = spec.BeaconBlock(state_root=anchor_state.hash_tree_root()) if is_post_gloas(spec): anchor_block.body.signed_execution_payload_bid.message.block_hash = ( - anchor_state.latest_block_hash + anchor_state.latest_execution_payload_bid.block_hash + ) + anchor_block.body.signed_execution_payload_bid.message.execution_requests_root = ( + anchor_state.latest_execution_payload_bid.execution_requests_root ) yield "anchor_state", anchor_state yield "anchor_block", anchor_block diff --git a/tests/infra/helpers/test_withdrawals.py b/tests/infra/helpers/test_withdrawals.py index 5bba8a3591..20ae154d1e 100644 --- a/tests/infra/helpers/test_withdrawals.py +++ b/tests/infra/helpers/test_withdrawals.py @@ -1,3 +1,4 @@ +from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest @@ -19,17 +20,19 @@ def test_basic_withdrawal_verification_success(self): spec.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP = 16384 # Mock states - pre_state = MagicMock() - pre_state.next_withdrawal_index = 100 - pre_state.next_withdrawal_validator_index = 50 - pre_state.validators = [MagicMock() for _ in range(1000)] - - post_state = MagicMock() - post_state.next_withdrawal_index = 102 - # Calculate expected next_withdrawal_validator_index: (50 + 16384) % 1000 = 434 - post_state.next_withdrawal_validator_index = (50 + 16384) % 1000 # 434 - post_state.validators = pre_state.validators - post_state.balances = [32 * 10**9] * 1000 # 32 ETH per validator + pre_state = SimpleNamespace( + next_withdrawal_index=100, + next_withdrawal_validator_index=50, + validators=[MagicMock() for _ in range(1000)], + ) + + post_state = SimpleNamespace( + next_withdrawal_index=102, + # Calculate expected next_withdrawal_validator_index: (50 + 16384) % 1000 = 434 + next_withdrawal_validator_index=(50 + 16384) % 1000, + validators=pre_state.validators, + balances=[32 * 10**9] * 1000, # 32 ETH per validator + ) # Mock execution payload execution_payload = MagicMock() @@ -65,12 +68,13 @@ def test_post_gloas_parent_block_not_full(self): """Test post-gloas behavior when parent block is not full""" # Mock spec spec = MagicMock() - spec.is_parent_block_full.return_value = False - # Mock states with same withdrawal indices + # Mock states with same withdrawal indices but different block hashes (parent not full) pre_state = MagicMock() pre_state.next_withdrawal_index = 100 pre_state.next_withdrawal_validator_index = 50 + pre_state.latest_block_hash = b"\x01" * 32 + pre_state.latest_execution_payload_bid.block_hash = b"\x02" * 32 post_state = MagicMock() post_state.next_withdrawal_index = 100 # Should remain unchanged diff --git a/tests/infra/helpers/withdrawals.py b/tests/infra/helpers/withdrawals.py index 6de6b87f9a..6863e545b8 100644 --- a/tests/infra/helpers/withdrawals.py +++ b/tests/infra/helpers/withdrawals.py @@ -597,13 +597,19 @@ def assert_process_withdrawals_pre_gloas( Verifies the correctness of the post-state after processing withdrawals. """ - # Since gloas, if parent block was not full, no withdrawals processed, indices unchanged - if is_post_gloas(spec) and not spec.is_parent_block_full(pre_state): - assert post_state.next_withdrawal_index == pre_state.next_withdrawal_index - assert ( - post_state.next_withdrawal_validator_index == pre_state.next_withdrawal_validator_index + # Since Gloas, if parent block was not full, no withdrawals are processed + # and the withdrawal indices remain unchanged. + if is_post_gloas(spec): + is_parent_block_full = ( + pre_state.latest_block_hash == pre_state.latest_execution_payload_bid.block_hash ) - return + if not is_parent_block_full: + assert post_state.next_withdrawal_index == pre_state.next_withdrawal_index + assert ( + post_state.next_withdrawal_validator_index + == pre_state.next_withdrawal_validator_index + ) + return _verify_withdrawals_next_withdrawal_index(spec, pre_state, post_state, expected_withdrawals)