diff --git a/setup.py b/setup.py index d509e8779..7620020e6 100644 --- a/setup.py +++ b/setup.py @@ -522,16 +522,23 @@ def get_pow_chain_head() -> PowBlock: class NoopExecutionEngine(ExecutionEngine): - def on_payload(self, execution_payload: ExecutionPayload) -> bool: + def execute_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: return True - def set_head(self, block_hash: Hash32) -> bool: - return True + def notify_consensus_validated(self: ExecutionEngine, block_hash: Hash32, valid: bool) -> None: + pass - def finalize_block(self, block_hash: Hash32) -> bool: - return True + def notify_forkchoice_updated(self: ExecutionEngine, head_block_hash: Hash32, finalized_block_hash: Hash32) -> None: + pass - def assemble_block(self, block_hash: Hash32, timestamp: uint64, random: Bytes32) -> ExecutionPayload: + def prepare_payload(self: ExecutionEngine, + parent_hash: Hash32, + timestamp: uint64, + random: Bytes32, + feeRecipient: ExecutionAddress) -> PayloadId: + raise NotImplementedError("no default block production") + + def get_payload(self: ExecutionEngine, payload_id: PayloadId) -> ExecutionPayload: raise NotImplementedError("no default block production") diff --git a/specs/merge/beacon-chain.md b/specs/merge/beacon-chain.md index 39f457252..053c829f1 100644 --- a/specs/merge/beacon-chain.md +++ b/specs/merge/beacon-chain.md @@ -31,7 +31,8 @@ - [`compute_timestamp_at_slot`](#compute_timestamp_at_slot) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - [Execution engine](#execution-engine) - - [`on_payload`](#on_payload) + - [`execute_payload`](#execute_payload) + - [`notify_consensus_validated`](#notify_consensus_validated) - [Block processing](#block-processing) - [Execution payload processing](#execution-payload-processing) - [`is_valid_gas_limit`](#is_valid_gas_limit) @@ -53,6 +54,7 @@ This patch adds transaction execution to the beacon chain as part of the Merge f | - | - | - | | `OpaqueTransaction` | `ByteList[MAX_BYTES_PER_OPAQUE_TRANSACTION]` | a [typed transaction envelope](https://eips.ethereum.org/EIPS/eip-2718#opaque-byte-array-rather-than-an-rlp-array) structured as `TransactionType \|\| TransactionPayload` | | `Transaction` | `Union[OpaqueTransaction]` | a transaction | +| `ExecutionAddress` | `Bytes20` | Address of account on the execution layer | ## Constants @@ -158,7 +160,7 @@ class BeaconState(Container): class ExecutionPayload(Container): # Execution block header fields parent_hash: Hash32 - coinbase: Bytes20 # 'beneficiary' in the yellow paper + coinbase: ExecutionAddress # 'beneficiary' in the yellow paper state_root: Bytes32 receipt_root: Bytes32 # 'receipts root' in the yellow paper logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] @@ -180,7 +182,7 @@ class ExecutionPayload(Container): class ExecutionPayloadHeader(Container): # Execution block header fields parent_hash: Hash32 - coinbase: Bytes20 + coinbase: ExecutionAddress state_root: Bytes32 receipt_root: Bytes32 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] @@ -240,19 +242,38 @@ def compute_timestamp_at_slot(state: BeaconState, slot: Slot) -> uint64: The implementation-dependent `ExecutionEngine` protocol encapsulates the execution sub-system logic via: * a state object `self.execution_state` of type `ExecutionState` -* a state transition function `self.on_payload` which mutates `self.execution_state` +* a state transition function `self.execute_payload` which applies changes to the `self.execution_state` +* a function `self.notify_consensus_validated` which signals that the beacon block containing the execution payload +is valid with respect to the consensus rule set -#### `on_payload` +*Note*: `execute_payload` and `notify_consensus_validated` are functions accessed through the `EXECUTION_ENGINE` module which instantiates the `ExecutionEngine` protocol. + +The body of each of these functions is implementation dependent. +The Engine API may be used to implement them with an external execution engine. + +#### `execute_payload` ```python -def on_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: +def execute_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: """ Returns ``True`` iff ``execution_payload`` is valid with respect to ``self.execution_state``. """ ... ``` -The above function is accessed through the `EXECUTION_ENGINE` module which instantiates the `ExecutionEngine` protocol. +#### `notify_consensus_validated` + +```python +def notify_consensus_validated(self: ExecutionEngine, block_hash: Hash32, valid: bool) -> None: + ... +``` + +The inputs to this function depend on the result of the state transition. A call to `notify_consensus_validated` must be made after the [`state_transition`](../phase0/beacon-chain.md#beacon-chain-state-transition-function) function finishes. The value of the `valid` parameter must be set as follows: + +* `True` if `state_transition` function call succeeds +* `False` if `state_transition` function call fails + +*Note*: The call of the `notify_consensus_validated` function with `valid = True` maps on the `POS_CONSENSUS_VALIDATED` event defined in the [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#definitions). ### Block processing @@ -309,7 +330,7 @@ def process_execution_payload(state: BeaconState, payload: ExecutionPayload, exe # Verify timestamp assert payload.timestamp == compute_timestamp_at_slot(state, state.slot) # Verify the execution payload is valid - assert execution_engine.on_payload(payload) + assert execution_engine.execute_payload(payload) # Cache execution payload header state.latest_execution_payload_header = ExecutionPayloadHeader( parent_hash=payload.parent_hash, diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index 8051ab3eb..4cf413b10 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -10,8 +10,7 @@ - [Introduction](#introduction) - [Protocols](#protocols) - [`ExecutionEngine`](#executionengine) - - [`set_head`](#set_head) - - [`finalize_block`](#finalize_block) + - [`notify_forkchoice_updated`](#notify_forkchoice_updated) - [Helpers](#helpers) - [`PowBlock`](#powblock) - [`get_pow_block`](#get_pow_block) @@ -32,38 +31,24 @@ This is the modification of the fork choice according to the executable beacon c ### `ExecutionEngine` -The following methods are added to the `ExecutionEngine` protocol for use in the fork choice: - -#### `set_head` - -Re-organizes the execution payload chain and corresponding state to make `block_hash` the head. +*Note*: The `notify_forkchoice_updated` function is added to the `ExecutionEngine` protocol to signal the fork choice updates. The body of this function is implementation dependent. -The Consensus API may be used to implement this with an external execution engine. +The Engine API may be used to implement it with an external execution engine. + +#### `notify_forkchoice_updated` + +This function performs two actions *atomically*: +* Re-organizes the execution payload chain and corresponding state to make `head_block_hash` the head. +* Applies finality to the execution state: it irreversibly persists the chain of all execution payloads +and corresponding state, up to and including `finalized_block_hash`. ```python -def set_head(self: ExecutionEngine, block_hash: Hash32) -> bool: - """ - Returns True if the ``block_hash`` was successfully set as head of the execution payload chain. - """ +def notify_forkchoice_updated(self: ExecutionEngine, head_block_hash: Hash32, finalized_block_hash: Hash32) -> None: ... ``` -#### `finalize_block` - -Applies finality to the execution state: it irreversibly persists the chain of all execution payloads -and corresponding state, up to and including `block_hash`. - -The body of this function is implementation dependent. -The Consensus API may be used to implement this with an external execution engine. - -```python -def finalize_block(self: ExecutionEngine, block_hash: Hash32) -> bool: - """ - Returns True if the data up to and including ``block_hash`` was successfully finalized. - """ - ... -``` +*Note*: The call of the `notify_forkchoice_updated` function maps on the `POS_FORKCHOICE_UPDATED` event defined in the [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#definitions). ## Helpers diff --git a/specs/merge/validator.md b/specs/merge/validator.md index 645e1967b..1bc36b02d 100644 --- a/specs/merge/validator.md +++ b/specs/merge/validator.md @@ -10,13 +10,15 @@ - [Introduction](#introduction) - [Prerequisites](#prerequisites) +- [Custom types](#custom-types) - [Protocols](#protocols) - [`ExecutionEngine`](#executionengine) - - [`assemble_block`](#assemble_block) + - [`prepare_payload`](#prepare_payload) + - [`get_payload`](#get_payload) - [Beacon chain responsibilities](#beacon-chain-responsibilities) - [Block proposal](#block-proposal) - [Constructing the `BeaconBlockBody`](#constructing-the-beaconblockbody) - - [Execution Payload](#execution-payload) + - [ExecutionPayload](#executionpayload) @@ -33,22 +35,48 @@ All behaviors and definitions defined in this document, and documents it extends All terminology, constants, functions, and protocol mechanics defined in the updated Beacon Chain doc of [The Merge](./beacon-chain.md) are requisite for this document and used throughout. Please see related Beacon Chain doc before continuing and use them as a reference throughout. +## Custom types + +| Name | SSZ equivalent | Description | +| - | - | - | +| `PayloadId` | `uint64` | Identifier of a payload building process | + ## Protocols ### `ExecutionEngine` -The following methods are added to the `ExecutionEngine` protocol for use as a validator: +*Note*: `prepare_payload` and `get_payload` functions are added to the `ExecutionEngine` protocol for use as a validator. -#### `assemble_block` +The body of each of these functions is implementation dependent. +The Engine API may be used to implement them with an external execution engine. -Produces a new instance of an execution payload, with the specified `timestamp`, -on top of the execution payload chain tip identified by `block_hash`. +#### `prepare_payload` -The body of this function is implementation dependent. -The Consensus API may be used to implement this with an external execution engine. +Given the set of execution payload attributes, `prepare_payload` initiates a process of building an execution payload +on top of the execution chain tip identified by `parent_hash`. ```python -def assemble_block(self: ExecutionEngine, block_hash: Hash32, timestamp: uint64, random: Bytes32) -> ExecutionPayload: +def prepare_payload(self: ExecutionEngine, + parent_hash: Hash32, + timestamp: uint64, + random: Bytes32, + fee_recipient: ExecutionAddress) -> PayloadId: + """ + Return ``payload_id`` that is used to obtain the execution payload in a subsequent ``get_payload`` call. + """ + ... +``` + +#### `get_payload` + +Given the `payload_id`, `get_payload` returns the most recent version of the execution payload that +has been built since the corresponding call to `prepare_payload` method. + +```python +def get_payload(self: ExecutionEngine, payload_id: PayloadId) -> ExecutionPayload: + """ + Return ``execution_payload`` object. + """ ... ``` @@ -60,9 +88,14 @@ All validator responsibilities remain unchanged other than those noted below. Na #### Constructing the `BeaconBlockBody` -##### Execution Payload +##### ExecutionPayload -* Set `block.body.execution_payload = get_execution_payload(state, execution_engine, pow_chain)` where: +To obtain an execution payload, a block proposer building a block on top of a `state` must take the following actions: + +1. Set `payload_id = prepare_execution_payload(state, pow_chain, fee_recipient, execution_engine)`, where: + * `state` is the state object after applying `process_slots(state, slot)` transition to the resulting state of the parent block processing + * `pow_chain` is a list that abstractly represents all blocks in the PoW chain + * `fee_recipient` is the value suggested to be used for the `coinbase` field of the execution payload ```python def get_pow_block_at_total_difficulty(total_difficulty: uint256, pow_chain: Sequence[PowBlock]) -> Optional[PowBlock]: @@ -75,27 +108,37 @@ def get_pow_block_at_total_difficulty(total_difficulty: uint256, pow_chain: Sequ return None -def produce_execution_payload(state: BeaconState, - parent_hash: Hash32, - execution_engine: ExecutionEngine) -> ExecutionPayload: - timestamp = compute_timestamp_at_slot(state, state.slot) - randao_mix = get_randao_mix(state, get_current_epoch(state)) - return execution_engine.assemble_block(parent_hash, timestamp, randao_mix) - - -def get_execution_payload(state: BeaconState, - execution_engine: ExecutionEngine, - pow_chain: Sequence[PowBlock]) -> ExecutionPayload: +def prepare_execution_payload(state: BeaconState, + pow_chain: Sequence[PowBlock], + fee_recipient: ExecutionAddress, + execution_engine: ExecutionEngine) -> Optional[PayloadId]: if not is_merge_complete(state): terminal_pow_block = get_pow_block_at_total_difficulty(TERMINAL_TOTAL_DIFFICULTY, pow_chain) if terminal_pow_block is None: - # Pre-merge, empty payload - return ExecutionPayload() + # Pre-merge, no prepare payload call is needed + return None else: # Signify merge via producing on top of the last PoW block - return produce_execution_payload(state, terminal_pow_block.block_hash, execution_engine) + parent_hash = terminal_pow_block.block_hash + else: + # Post-merge, normal payload + parent_hash = state.latest_execution_payload_header.block_hash - # Post-merge, normal payload - parent_hash = state.latest_execution_payload_header.block_hash - return produce_execution_payload(state, parent_hash, execution_engine) + timestamp = compute_timestamp_at_slot(state, state.slot) + random = get_randao_mix(state, get_current_epoch(state)) + return execution_engine.prepare_payload(parent_hash, timestamp, random, fee_recipient) ``` + +2. Set `block.body.execution_payload = get_execution_payload(payload_id, execution_engine)`, where: + +```python +def get_execution_payload(payload_id: Optional[PayloadId], execution_engine: ExecutionEngine) -> ExecutionPayload: + if payload_id is None: + # Pre-merge, empty payload + return ExecutionPayload() + else: + return execution_engine.get_payload(payload_id) +``` + +*Note*: It is recommended for a validator to call `prepare_execution_payload` as soon as input parameters become known, +and make subsequent calls to this function when any of these parameters gets updated. diff --git a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py index 6126346a9..0d03447a7 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py @@ -11,7 +11,7 @@ def build_empty_execution_payload(spec, state, randao_mix=None): payload = spec.ExecutionPayload( parent_hash=latest.block_hash, - coinbase=spec.Bytes20(), + coinbase=spec.ExecutionAddress(), state_root=latest.state_root, # no changes to the state receipt_root=b"no receipts here" + b"\x00" * 16, # TODO: root of empty MPT may be better. logs_bloom=spec.ByteVector[spec.BYTES_PER_LOGS_BLOOM](), # TODO: zeroed logs bloom for empty logs ok? diff --git a/tests/core/pyspec/eth2spec/test/merge/block_processing/test_process_execution_payload.py b/tests/core/pyspec/eth2spec/test/merge/block_processing/test_process_execution_payload.py index 9c5ed6712..d44bad58c 100644 --- a/tests/core/pyspec/eth2spec/test/merge/block_processing/test_process_execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/merge/block_processing/test_process_execution_payload.py @@ -25,7 +25,7 @@ def run_execution_payload_processing(spec, state, execution_payload, valid=True, called_new_block = False class TestEngine(spec.NoopExecutionEngine): - def on_payload(self, payload) -> bool: + def execute_payload(self, payload) -> bool: nonlocal called_new_block, execution_valid called_new_block = True assert payload == execution_payload