From 878b15df6ab8d56dd4499c45e2ddec4faa570910 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 18 Jun 2021 11:09:30 +0100 Subject: [PATCH] polish merge/beacon-chain.md (#2472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polish `merge/beacon-chain.md` with mostly non-substantive changes. **Non-substantive changes** * rename `MAX_EXECUTION_TRANSACTIONS` to `MAX_TRANSACTIONS_PER_PAYLOAD` - rename "execution transaction" to just "transaction" as per discussion with Danny * rename `compute_time_at_slot` to `compute_timestamp_at_slot` - the function returns a Unix timestamp - "timestamp" matches `execution_payload.timestamp` * be explicit about `ExecutionEngine.execution_state` for clarity * rename `ExecutionPayload.number` to `ExecutionPayload.block_number` - more specific ("number" is pretty vague) - consistent with `ExecutionPayload.block_hash` * rename `new_block` to `on_payload` - the `on_` prefix is consistent with other event handlers (e.g. see `on_tick`, `on_block`, `on_attestation` [here](https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/fork-choice.md#handlers)) - the `_payload` suffix is more to the point given the function accepts an `execution_payload` - avoids conflict with `on_block` which is already used in the fork choice * rework the table of contents for consistency * order `is_execution_enabled` after `is_transition_completed` and `is_transition_block` - `is_execution_enabled` refers to `is_transition_completed` and `is_transition_block` * rename "transition" to "merge" - "transition" is a bit vague—we will have other transitions at future hard forks - there is no need for two words to refer to the same concept * add a bunch of inline comments, e.g. in `process_execution_payload` * make the `process_execution_payload` signature consistent with the other `process_` functions in `process_block` which take as arguments `state` and `block.body` * remove `TRANSITION_TOTAL_DIFFICULTY` - to be put in `merge/fork-choice.md` where it is used * various misc cleanups **Substantive changes** * reorder `ExecutionPayload` fields - for consistency with yellow paper and Eth1 - same for `ExecutionPayloadHeader` - added comments separating out the execution block header fields from the extra fields (cosmetic) --- setup.py | 18 +- specs/merge/beacon-chain.md | 219 ++++++++---------- specs/merge/fork-choice.md | 2 +- specs/merge/validator.md | 2 +- .../test/helpers/execution_payload.py | 18 +- .../test_process_execution_payload.py | 6 +- 6 files changed, 122 insertions(+), 143 deletions(-) diff --git a/setup.py b/setup.py index 9ecf8261fd..4430ccec0d 100644 --- a/setup.py +++ b/setup.py @@ -223,7 +223,7 @@ def get_spec(file_name: Path, preset: Dict[str, str], config: Dict[str, str]) -> if not _is_constant_id(name): # Check for short type declarations - if value.startswith("uint") or value.startswith("Bytes") or value.startswith("ByteList"): + if value.startswith("uint") or value.startswith("Bytes") or value.startswith("ByteList") or value.startswith("Union"): custom_types[name] = value continue @@ -495,7 +495,7 @@ def imports(cls, preset_name: str): return super().imports(preset_name) + f''' from typing import Protocol from eth2spec.phase0 import {preset_name} as phase0 -from eth2spec.utils.ssz.ssz_typing import Bytes20, ByteList, ByteVector, uint256 +from eth2spec.utils.ssz.ssz_typing import Bytes20, ByteList, ByteVector, uint256, Union ''' @classmethod @@ -523,7 +523,7 @@ def get_pow_chain_head() -> PowBlock: class NoopExecutionEngine(ExecutionEngine): - def new_block(self, execution_payload: ExecutionPayload) -> bool: + def on_payload(self, execution_payload: ExecutionPayload) -> bool: return True def set_head(self, block_hash: Hash32) -> bool: @@ -553,6 +553,10 @@ def hardcoded_custom_type_dep_constants(cls) -> str: } +def is_spec_defined_type(value: str) -> bool: + return value.startswith('ByteList') or value.startswith('Union') + + def objects_to_spec(preset_name: str, spec_object: SpecObject, builder: SpecBuilder, @@ -565,15 +569,15 @@ def objects_to_spec(preset_name: str, [ f"class {key}({value}):\n pass\n" for key, value in spec_object.custom_types.items() - if not value.startswith('ByteList') + if not is_spec_defined_type(value) ] ) - + ('\n\n' if len([key for key, value in spec_object.custom_types.items() if value.startswith('ByteList')]) > 0 else '') + + ('\n\n' if len([key for key, value in spec_object.custom_types.items() if is_spec_defined_type(value)]) > 0 else '') + '\n\n'.join( [ f"{key} = {value}\n" for key, value in spec_object.custom_types.items() - if value.startswith('ByteList') + if is_spec_defined_type(value) ] ) ) @@ -1020,7 +1024,7 @@ def run(self): "py_ecc==5.2.0", "milagro_bls_binding==1.6.3", "dataclasses==0.6", - "remerkleable==0.1.20", + "remerkleable==0.1.21", RUAMEL_YAML_VERSION, "lru-dict==1.1.6", MARKO_VERSION, diff --git a/specs/merge/beacon-chain.md b/specs/merge/beacon-chain.md index 697bd0c96b..562adaab4d 100644 --- a/specs/merge/beacon-chain.md +++ b/specs/merge/beacon-chain.md @@ -1,6 +1,6 @@ # Ethereum 2.0 The Merge -**Warning:** This document is currently based on [Phase 0](../phase0/beacon-chain.md) but will be rebased to [Altair](../altair/beacon-chain.md) once the latter is shipped. +**Warning**: This document is currently based on [Phase 0](../phase0/beacon-chain.md) and will be rebased on [Altair](../altair/beacon-chain.md). **Notice**: This document is a work-in-progress for researchers and implementers. @@ -21,35 +21,36 @@ - [New containers](#new-containers) - [`ExecutionPayload`](#executionpayload) - [`ExecutionPayloadHeader`](#executionpayloadheader) -- [Protocols](#protocols) - - [`ExecutionEngine`](#executionengine) - - [`new_block`](#new_block) - [Helper functions](#helper-functions) - - [Misc](#misc) + - [Predicates](#predicates) + - [`is_merge_complete`](#is_merge_complete) + - [`is_merge_block`](#is_merge_block) - [`is_execution_enabled`](#is_execution_enabled) - - [`is_transition_completed`](#is_transition_completed) - - [`is_transition_block`](#is_transition_block) - - [`compute_time_at_slot`](#compute_time_at_slot) + - [Misc](#misc) + - [`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) - [Block processing](#block-processing) - - [Execution payload processing](#execution-payload-processing) - - [`process_execution_payload`](#process_execution_payload) -- [Initialize state for pure Merge testnets and test vectors](#initialize-state-for-pure-merge-testnets-and-test-vectors) + - [Execution payload processing](#execution-payload-processing) + - [`process_execution_payload`](#process_execution_payload) +- [Testing](#testing) ## Introduction -This is a patch implementing the executable beacon chain proposal. -It enshrines transaction execution and validity as a first class citizen at the core of the beacon chain. +This patch adds transaction execution to the beacon chain as part of the Merge fork. ## Custom types -We define the following Python custom types for type hinting and readability: +*Note*: The `Transaction` type is a stub which is not final. | Name | SSZ equivalent | Description | | - | - | - | -| `OpaqueTransaction` | `ByteList[MAX_BYTES_PER_OPAQUE_TRANSACTION]` | a byte-list containing a single [typed transaction envelope](https://eips.ethereum.org/EIPS/eip-2718#opaque-byte-array-rather-than-an-rlp-array) structured as `TransactionType \|\| TransactionPayload` | +| `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 | ## Constants @@ -58,32 +59,26 @@ We define the following Python custom types for type hinting and readability: | Name | Value | | - | - | | `MAX_BYTES_PER_OPAQUE_TRANSACTION` | `uint64(2**20)` (= 1,048,576) | -| `MAX_EXECUTION_TRANSACTIONS` | `uint64(2**14)` (= 16,384) | +| `MAX_TRANSACTIONS_PER_PAYLOAD` | `uint64(2**14)` (= 16,384) | | `BYTES_PER_LOGS_BLOOM` | `uint64(2**8)` (= 256) | ## Containers ### Extended containers -*Note*: Extended SSZ containers inherit all fields from the parent in the original -order and append any additional fields to the end. - #### `BeaconBlockBody` -*Note*: `BeaconBlockBody` fields remain unchanged other than the addition of `execution_payload`. - ```python class BeaconBlockBody(phase0.BeaconBlockBody): + # Execution execution_payload: ExecutionPayload # [New in Merge] ``` #### `BeaconState` -*Note*: `BeaconState` fields remain unchanged other than addition of `latest_execution_payload_header`. - ```python class BeaconState(phase0.BeaconState): - # Execution-layer + # Execution latest_execution_payload_header: ExecutionPayloadHeader # [New in Merge] ``` @@ -91,103 +86,100 @@ class BeaconState(phase0.BeaconState): #### `ExecutionPayload` -The execution payload included in a `BeaconBlockBody`. - ```python class ExecutionPayload(Container): - block_hash: Hash32 # Hash of execution block + # Execution block header fields parent_hash: Hash32 - coinbase: Bytes20 + coinbase: Bytes20 # 'beneficiary' in the yellow paper state_root: Bytes32 - number: uint64 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + receipt_root: Bytes32 # 'receipts root' in the yellow paper + block_number: uint64 # 'number' in the yellow paper gas_limit: uint64 gas_used: uint64 timestamp: uint64 - receipt_root: Bytes32 - logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] - transactions: List[OpaqueTransaction, MAX_EXECUTION_TRANSACTIONS] + # Extra payload fields + block_hash: Hash32 # Hash of execution block + transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] ``` #### `ExecutionPayloadHeader` -The execution payload header included in a `BeaconState`. - -*Note:* Holds execution payload data without transaction bodies. - ```python class ExecutionPayloadHeader(Container): - block_hash: Hash32 # Hash of execution block + # Execution block header fields parent_hash: Hash32 coinbase: Bytes20 state_root: Bytes32 - number: uint64 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + receipt_root: Bytes32 + block_number: uint64 gas_limit: uint64 gas_used: uint64 timestamp: uint64 - receipt_root: Bytes32 - logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + # Extra payload fields + block_hash: Hash32 # Hash of execution block transactions_root: Root ``` -## Protocols - -### `ExecutionEngine` - -The `ExecutionEngine` protocol separates the consensus and execution sub-systems. -The consensus implementation references an instance of this sub-system with `EXECUTION_ENGINE`. - -The following methods are added to the `ExecutionEngine` protocol for use in the state transition: - -#### `new_block` +## Helper functions -Verifies the given `execution_payload` with respect to execution state transition, and persists changes if valid. +### Predicates -The body of this function is implementation dependent. -The Consensus API may be used to implement this with an external execution engine. +#### `is_merge_complete` ```python -def new_block(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: - """ - Returns True if the ``execution_payload`` was verified and processed successfully, False otherwise. - """ - ... +def is_merge_complete(state: BeaconState) -> bool: + return state.latest_execution_payload_header != ExecutionPayloadHeader() ``` -## Helper functions +#### `is_merge_block` -### Misc +```python +def is_merge_block(state: BeaconState, body: BeaconBlockBody) -> bool: + return not is_merge_complete(state) and body.execution_payload != ExecutionPayload() +``` #### `is_execution_enabled` ```python -def is_execution_enabled(state: BeaconState, block: BeaconBlock) -> bool: - return is_transition_completed(state) or is_transition_block(state, block) +def is_execution_enabled(state: BeaconState, body: BeaconBlockBody) -> bool: + return is_merge_block(state, body) or is_merge_complete(state) ``` -#### `is_transition_completed` +### Misc -```python -def is_transition_completed(state: BeaconState) -> bool: - return state.latest_execution_payload_header != ExecutionPayloadHeader() -``` +#### `compute_timestamp_at_slot` -#### `is_transition_block` +*Note*: This function is unsafe with respect to overflows and underflows. ```python -def is_transition_block(state: BeaconState, block: BeaconBlock) -> bool: - return not is_transition_completed(state) and block.body.execution_payload != ExecutionPayload() +def compute_timestamp_at_slot(state: BeaconState, slot: Slot) -> uint64: + slots_since_genesis = slot - GENESIS_SLOT + return uint64(state.genesis_time + slots_since_genesis * SECONDS_PER_SLOT) ``` -#### `compute_time_at_slot` +## Beacon chain state transition function -*Note*: This function is unsafe with respect to overflows and underflows. +### Execution engine + +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` + +#### `on_payload` ```python -def compute_time_at_slot(state: BeaconState, slot: Slot) -> uint64: - slots_since_genesis = slot - GENESIS_SLOT - return uint64(state.genesis_time + slots_since_genesis * SECONDS_PER_SLOT) +def on_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. + ### Block processing ```python @@ -196,50 +188,45 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: process_randao(state, block.body) process_eth1_data(state, block.body) process_operations(state, block.body) - # Pre-merge, skip execution payload processing - if is_execution_enabled(state, block): + if is_execution_enabled(state, block.body): process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [New in Merge] ``` -#### Execution payload processing +### Execution payload processing -##### `process_execution_payload` +#### `process_execution_payload` ```python -def process_execution_payload(state: BeaconState, - execution_payload: ExecutionPayload, - execution_engine: ExecutionEngine) -> None: - """ - Note: This function is designed to be able to be run in parallel with the other `process_block` sub-functions - """ - if is_transition_completed(state): - assert execution_payload.parent_hash == state.latest_execution_payload_header.block_hash - assert execution_payload.number == state.latest_execution_payload_header.number + 1 - - assert execution_payload.timestamp == compute_time_at_slot(state, state.slot) - - assert execution_engine.new_block(execution_payload) - +def process_execution_payload(state: BeaconState, payload: ExecutionPayload, execution_engine: ExecutionEngine) -> None: + # Verify consistency of the parent hash and block number + if is_merge_complete(state): + assert payload.parent_hash == state.latest_execution_payload_header.block_hash + assert payload.block_number == state.latest_execution_payload_header.block_number + uint64(1) + # Verify timestamp + assert payload.timestamp == compute_timestamp_at_slot(state, state.slot) + # Verify the execution payload is valid + assert execution_engine.on_payload(payload) + # Cache execution payload state.latest_execution_payload_header = ExecutionPayloadHeader( - block_hash=execution_payload.block_hash, - parent_hash=execution_payload.parent_hash, - coinbase=execution_payload.coinbase, - state_root=execution_payload.state_root, - number=execution_payload.number, - gas_limit=execution_payload.gas_limit, - gas_used=execution_payload.gas_used, - timestamp=execution_payload.timestamp, - receipt_root=execution_payload.receipt_root, - logs_bloom=execution_payload.logs_bloom, - transactions_root=hash_tree_root(execution_payload.transactions), + parent_hash=payload.parent_hash, + coinbase=payload.coinbase, + state_root=payload.state_root, + logs_bloom=payload.logs_bloom, + receipt_root=payload.receipt_root, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), ) ``` -## Initialize state for pure Merge testnets and test vectors +## Testing -This helper function is only for initializing the state for pure Merge testnets and tests. +*Note*: The function `initialize_beacon_state_from_eth1` is modified for pure Merge testing only. -*Note*: The function `initialize_beacon_state_from_eth1` is modified: (1) using `MERGE_FORK_VERSION` as the current fork version, (2) utilizing the Merge `BeaconBlockBody` when constructing the initial `latest_block_header`, and (3) adding initial `latest_execution_payload_header`. +*Note*: The function `initialize_beacon_state_from_eth1` is modified to use `MERGE_FORK_VERSION` and initialize `latest_execution_payload_header`. ```python def initialize_beacon_state_from_eth1(eth1_block_hash: Bytes32, @@ -276,21 +263,9 @@ def initialize_beacon_state_from_eth1(eth1_block_hash: Bytes32, # Set genesis validators root for domain separation and chain versioning state.genesis_validators_root = hash_tree_root(state.validators) - # [New in Merge] Construct execution payload header - # Note: initialized with zero block height - state.latest_execution_payload_header = ExecutionPayloadHeader( - block_hash=eth1_block_hash, - parent_hash=Hash32(), - coinbase=Bytes20(), - state_root=Bytes32(), - number=uint64(0), - gas_limit=uint64(0), - gas_used=uint64(0), - timestamp=eth1_timestamp, - receipt_root=Bytes32(), - logs_bloom=ByteVector[BYTES_PER_LOGS_BLOOM](), - transactions_root=Root(), - ) + # [New in Merge] Initialize the execution payload header (with block number set to 0) + state.latest_execution_payload_header.block_hash = eth1_block_hash + state.latest_execution_payload_header.timestamp = eth1_timestamp return state ``` diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index 56345dd90e..d0f327137b 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -127,7 +127,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock, transition_store: Tr assert get_ancestor(store, block.parent_root, finalized_slot) == store.finalized_checkpoint.root # [New in Merge] - if (transition_store is not None) and is_transition_block(pre_state, block): + if (transition_store is not None) and is_merge_block(pre_state, block): # Delay consideration of block until PoW block is processed by the PoW node pow_block = get_pow_block(block.body.execution_payload.parent_hash) assert pow_block.is_processed diff --git a/specs/merge/validator.md b/specs/merge/validator.md index c5a7a4c789..21668b327d 100644 --- a/specs/merge/validator.md +++ b/specs/merge/validator.md @@ -73,7 +73,7 @@ Let `get_pow_chain_head() -> PowBlock` be the function that returns the head of def get_execution_payload(state: BeaconState, transition_store: TransitionStore, execution_engine: ExecutionEngine) -> ExecutionPayload: - if not is_transition_completed(state): + if not is_merge_complete(state): pow_block = get_pow_chain_head() if not is_valid_terminal_pow_block(transition_store, pow_block): # Pre-merge, empty payload diff --git a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py index 7774aa4d9f..238e2e00f5 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py @@ -4,19 +4,19 @@ def build_empty_execution_payload(spec, state): """ latest = state.latest_execution_payload_header timestamp = spec.compute_time_at_slot(state, state.slot) - empty_txs = spec.List[spec.OpaqueTransaction, spec.MAX_EXECUTION_TRANSACTIONS]() + empty_txs = spec.List[spec.Transaction, spec.MAX_TRANSACTIONS_PER_PAYLOAD]() payload = spec.ExecutionPayload( - block_hash=spec.Hash32(), parent_hash=latest.block_hash, coinbase=spec.Bytes20(), state_root=latest.state_root, # no changes to the state - number=latest.number + 1, + logs_bloom=spec.ByteVector[spec.BYTES_PER_LOGS_BLOOM](), # TODO: zeroed logs bloom for empty logs ok? + receipt_root=b"no receipts here" + b"\x00" * 16, # TODO: root of empty MPT may be better. + block_number=latest.block_number + 1, gas_limit=latest.gas_limit, # retain same limit gas_used=0, # empty block, 0 gas timestamp=timestamp, - 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? + block_hash=spec.Hash32(), transactions=empty_txs, ) # TODO: real RLP + block hash logic would be nice, requires RLP and keccak256 dependency however. @@ -27,16 +27,16 @@ def build_empty_execution_payload(spec, state): def get_execution_payload_header(spec, execution_payload): return spec.ExecutionPayloadHeader( - block_hash=execution_payload.block_hash, parent_hash=execution_payload.parent_hash, coinbase=execution_payload.coinbase, state_root=execution_payload.state_root, - number=execution_payload.number, + logs_bloom=execution_payload.logs_bloom, + receipt_root=execution_payload.receipt_root, + block_number=execution_payload.block_number, gas_limit=execution_payload.gas_limit, gas_used=execution_payload.gas_used, timestamp=execution_payload.timestamp, - receipt_root=execution_payload.receipt_root, - logs_bloom=execution_payload.logs_bloom, + block_hash=execution_payload.block_hash, transactions_root=spec.hash_tree_root(execution_payload.transactions) ) 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 5edd319603..4c68034d4a 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 new_block(self, payload) -> bool: + def on_payload(self, payload) -> bool: nonlocal called_new_block, execution_valid called_new_block = True assert payload == execution_payload @@ -153,7 +153,7 @@ def test_bad_number_regular_payload(spec, state): # execution payload execution_payload = build_empty_execution_payload(spec, state) - execution_payload.number = execution_payload.number + 1 + execution_payload.block_number = execution_payload.block_number + 1 yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) @@ -168,7 +168,7 @@ def test_bad_everything_regular_payload(spec, state): # execution payload execution_payload = build_empty_execution_payload(spec, state) execution_payload.parent_hash = spec.Hash32() - execution_payload.number = execution_payload.number + 1 + execution_payload.block_number = execution_payload.block_number + 1 yield from run_execution_payload_processing(spec, state, execution_payload, valid=False)