diff --git a/specs/electra/beacon-chain.md b/specs/electra/beacon-chain.md index 0c56491907..940bba7b8b 100644 --- a/specs/electra/beacon-chain.md +++ b/specs/electra/beacon-chain.md @@ -12,6 +12,7 @@ - [Constants](#constants) - [Misc](#misc) - [Withdrawal prefixes](#withdrawal-prefixes) + - [Execution layer triggered requests](#execution-layer-triggered-requests) - [Preset](#preset) - [Gwei values](#gwei-values) - [Rewards and penalties](#rewards-and-penalties) @@ -137,6 +138,14 @@ The following values are (non-configurable) constants used throughout the specif | - | - | | `COMPOUNDING_WITHDRAWAL_PREFIX` | `Bytes1('0x02')` | +### Execution layer triggered requests + +| Name | Value | +| - | - | +| `DEPOSIT_REQUEST_TYPE` | `Bytes1('0x00')` | +| `WITHDRAWAL_REQUEST_TYPE` | `Bytes1('0x01')` | +| `CONSOLIDATION_REQUEST_TYPE` | `Bytes1('0x02')` | + ## Preset ### Gwei values @@ -1146,11 +1155,17 @@ def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None: ```python def get_execution_requests_list(execution_requests: ExecutionRequests) -> Sequence[bytes]: - deposit_bytes = ssz_serialize(execution_requests.deposits) - withdrawal_bytes = ssz_serialize(execution_requests.withdrawals) - consolidation_bytes = ssz_serialize(execution_requests.consolidations) - - return [deposit_bytes, withdrawal_bytes, consolidation_bytes] + requests = [ + (DEPOSIT_REQUEST_TYPE, execution_requests.deposits), + (WITHDRAWAL_REQUEST_TYPE, execution_requests.withdrawals), + (CONSOLIDATION_REQUEST_TYPE, execution_requests.consolidations), + ] + + return [ + request_type + ssz_serialize(request_data) + for request_type, request_data in requests + if len(request_data) != 0 + ] ``` ##### Modified `process_execution_payload` diff --git a/specs/electra/validator.md b/specs/electra/validator.md index 553eeaa702..ca92a1d955 100644 --- a/specs/electra/validator.md +++ b/specs/electra/validator.md @@ -189,18 +189,55 @@ def prepare_execution_payload(state: BeaconState, *[New in Electra]* -1. The execution payload is obtained from the execution engine as defined above using `payload_id`. The response also includes a `execution_requests` entry containing a list of bytes. Each element on the list corresponds to one SSZ list of requests as defined -in [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685). The index of each element in the array determines the type of request. +1. The execution payload is obtained from the execution engine as defined above using `payload_id`. The response also includes a `execution_requests` entry containing a list of bytes. Each element on the list corresponds to one SSZ list of requests as defined in [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685). The first byte of each request is used to determine the request type. Requests must be ordered by request type in ascending order. As a result, there can only be at most one instance of each request type. 2. Set `block.body.execution_requests = get_execution_requests(execution_requests)`, where: ```python -def get_execution_requests(execution_requests: Sequence[bytes]) -> ExecutionRequests: - deposits = ssz_deserialize(List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD], execution_requests[0]) - withdrawals = ssz_deserialize(List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD], execution_requests[1]) - consolidations = ssz_deserialize(List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD], - execution_requests[2]) - - return ExecutionRequests(deposits, withdrawals, consolidations) +def get_execution_requests(execution_requests_list: Sequence[bytes]) -> ExecutionRequests: + deposits = [] + withdrawals = [] + consolidations = [] + + request_types = [ + DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, + CONSOLIDATION_REQUEST_TYPE, + ] + + prev_request_type = None + for request in execution_requests_list: + request_type, request_data = request[0:1], request[1:] + + # Check that the request type is valid + assert request_type in request_types + # Check that the request data is not empty + assert len(request_data) != 0 + # Check that requests are in strictly ascending order + # Each successive type must be greater than the last with no duplicates + assert prev_request_type is None or prev_request_type < request_type + prev_request_type = request_type + + if request_type == DEPOSIT_REQUEST_TYPE: + deposits = ssz_deserialize( + List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD], + request_data + ) + elif request_type == WITHDRAWAL_REQUEST_TYPE: + withdrawals = ssz_deserialize( + List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD], + request_data + ) + elif request_type == CONSOLIDATION_REQUEST_TYPE: + consolidations = ssz_deserialize( + List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD], + request_data + ) + + return ExecutionRequests( + deposits=deposits, + withdrawals=withdrawals, + consolidations=consolidations, + ) ``` ## Attesting diff --git a/tests/core/pyspec/eth2spec/test/electra/sanity/blocks/test_blocks.py b/tests/core/pyspec/eth2spec/test/electra/sanity/blocks/test_blocks.py index c3d2284610..5a4b98c3c8 100644 --- a/tests/core/pyspec/eth2spec/test/electra/sanity/blocks/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/electra/sanity/blocks/test_blocks.py @@ -9,7 +9,7 @@ get_signed_address_change, ) from eth2spec.test.helpers.execution_payload import ( - compute_el_block_hash, + compute_el_block_hash_for_block, ) from eth2spec.test.helpers.voluntary_exits import ( prepare_signed_exits, @@ -42,7 +42,7 @@ def test_basic_el_withdrawal_request(spec, state): ) block = build_empty_block_for_next_slot(spec, state) block.body.execution_requests.withdrawals = [withdrawal_request] - block.body.execution_payload.block_hash = compute_el_block_hash(spec, block.body.execution_payload, state) + block.body.execution_payload.block_hash = compute_el_block_hash_for_block(spec, block) signed_block = state_transition_and_sign_block(spec, state, block) yield 'blocks', [signed_block] @@ -80,7 +80,7 @@ def test_basic_btec_and_el_withdrawal_request_in_same_block(spec, state): ) block.body.execution_requests.withdrawals = [withdrawal_request] - block.body.execution_payload.block_hash = compute_el_block_hash(spec, block.body.execution_payload, state) + block.body.execution_payload.block_hash = compute_el_block_hash_for_block(spec, block) signed_block = state_transition_and_sign_block(spec, state, block) yield 'blocks', [signed_block] @@ -132,7 +132,7 @@ def test_basic_btec_before_el_withdrawal_request(spec, state): ) block_2 = build_empty_block_for_next_slot(spec, state) block_2.body.execution_requests.withdrawals = [withdrawal_request] - block_2.body.execution_payload.block_hash = compute_el_block_hash(spec, block_2.body.execution_payload, state) + block_2.body.execution_payload.block_hash = compute_el_block_hash_for_block(spec, block_2) signed_block_2 = state_transition_and_sign_block(spec, state, block_2) yield 'blocks', [signed_block_1, signed_block_2] @@ -165,7 +165,7 @@ def test_cl_exit_and_el_withdrawal_request_in_same_block(spec, state): block = build_empty_block_for_next_slot(spec, state) block.body.voluntary_exits = signed_voluntary_exits block.body.execution_requests.withdrawals = [withdrawal_request] - block.body.execution_payload.block_hash = compute_el_block_hash(spec, block.body.execution_payload, state) + block.body.execution_payload.block_hash = compute_el_block_hash_for_block(spec, block) signed_block = state_transition_and_sign_block(spec, state, block) yield 'blocks', [signed_block] diff --git a/tests/core/pyspec/eth2spec/test/electra/sanity/blocks/test_deposit_transition.py b/tests/core/pyspec/eth2spec/test/electra/sanity/blocks/test_deposit_transition.py index 9749c89ffd..a9c2c62814 100644 --- a/tests/core/pyspec/eth2spec/test/electra/sanity/blocks/test_deposit_transition.py +++ b/tests/core/pyspec/eth2spec/test/electra/sanity/blocks/test_deposit_transition.py @@ -12,7 +12,7 @@ prepare_deposit_request, ) from eth2spec.test.helpers.execution_payload import ( - compute_el_block_hash, + compute_el_block_hash_for_block, ) from eth2spec.test.helpers.keys import privkeys, pubkeys from eth2spec.test.helpers.state import ( @@ -134,7 +134,7 @@ def prepare_state_and_block(spec, # Assign deposits and deposit requests block.body.deposits = deposits block.body.execution_requests.deposits = deposit_requests - block.body.execution_payload.block_hash = compute_el_block_hash(spec, block.body.execution_payload, state) + block.body.execution_payload.block_hash = compute_el_block_hash_for_block(spec, block) return state, block @@ -251,7 +251,7 @@ def test_deposit_transition__deposit_and_top_up_same_block(spec, state): # Artificially assign deposit's pubkey to a deposit request of the same block top_up_keys = [block.body.deposits[0].data.pubkey] block.body.execution_requests.deposits[0].pubkey = top_up_keys[0] - block.body.execution_payload.block_hash = compute_el_block_hash(spec, block.body.execution_payload, state) + block.body.execution_payload.block_hash = compute_el_block_hash_for_block(spec, block) pre_pending_deposits = len(state.pending_deposits) diff --git a/tests/core/pyspec/eth2spec/test/electra/unittests/test_execution_requests.py b/tests/core/pyspec/eth2spec/test/electra/unittests/test_execution_requests.py new file mode 100644 index 0000000000..d57e724312 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/electra/unittests/test_execution_requests.py @@ -0,0 +1,119 @@ +from eth2spec.test.context import ( + single_phase, + spec_test, + with_electra_and_later, +) + + +@with_electra_and_later +@spec_test +@single_phase +def test_requests_serialization_round_trip__empty(spec): + execution_requests = spec.ExecutionRequests() + serialized_execution_requests = spec.get_execution_requests_list(execution_requests) + deserialized_execution_requests = spec.get_execution_requests(serialized_execution_requests) + assert deserialized_execution_requests == execution_requests + + +@with_electra_and_later +@spec_test +@single_phase +def test_requests_serialization_round_trip__one_request(spec): + execution_requests = spec.ExecutionRequests( + deposits=[spec.DepositRequest()], + ) + serialized_execution_requests = spec.get_execution_requests_list(execution_requests) + deserialized_execution_requests = spec.get_execution_requests(serialized_execution_requests) + assert deserialized_execution_requests == execution_requests + + +@with_electra_and_later +@spec_test +@single_phase +def test_requests_serialization_round_trip__multiple_requests(spec): + execution_requests = spec.ExecutionRequests( + deposits=[spec.DepositRequest()], + withdrawals=[spec.WithdrawalRequest()], + consolidations=[spec.ConsolidationRequest()], + ) + serialized_execution_requests = spec.get_execution_requests_list(execution_requests) + deserialized_execution_requests = spec.get_execution_requests(serialized_execution_requests) + assert deserialized_execution_requests == execution_requests + + +@with_electra_and_later +@spec_test +@single_phase +def test_requests_serialization_round_trip__one_request_with_real_data(spec): + execution_requests = spec.ExecutionRequests( + deposits=[ + spec.DepositRequest( + pubkey=spec.BLSPubkey(48 * "aa"), + withdrawal_credentials=spec.Bytes32(32 * "bb"), + amount=spec.Gwei(11111111), + signature=spec.BLSSignature(96 * "cc"), + index=spec.uint64(22222222), + ), + ] + ) + serialized_execution_requests = spec.get_execution_requests_list(execution_requests) + deserialized_execution_requests = spec.get_execution_requests(serialized_execution_requests) + assert deserialized_execution_requests == execution_requests + + +@with_electra_and_later +@spec_test +@single_phase +def test_requests_deserialize__reject_duplicate_request(spec): + serialized_withdrawal = 76 * b"\x0a" + serialized_execution_requests = [ + spec.WITHDRAWAL_REQUEST_TYPE + serialized_withdrawal, + spec.WITHDRAWAL_REQUEST_TYPE + serialized_withdrawal, + ] + try: + spec.get_execution_requests(serialized_execution_requests) + assert False, "expected exception" + except Exception: + pass + + +@with_electra_and_later +@spec_test +@single_phase +def test_requests_deserialize__reject_out_of_order_requests(spec): + serialized_execution_requests = [ + spec.WITHDRAWAL_REQUEST_TYPE + 76 * b"\x0a", + spec.DEPOSIT_REQUEST_TYPE + 192 * b"\x0b", + ] + assert int(serialized_execution_requests[0][0]) > int(serialized_execution_requests[1][0]) + try: + spec.get_execution_requests(serialized_execution_requests) + assert False, "expected exception" + except Exception: + pass + + +@with_electra_and_later +@spec_test +@single_phase +def test_requests_deserialize__reject_empty_request(spec): + serialized_execution_requests = [b"\x01"] + try: + spec.get_execution_requests(serialized_execution_requests) + assert False, "expected exception" + except Exception: + pass + + +@with_electra_and_later +@spec_test +@single_phase +def test_requests_deserialize__reject_unexpected_request_type(spec): + serialized_execution_requests = [ + b"\x03\xff\xff\xff", + ] + try: + spec.get_execution_requests(serialized_execution_requests) + assert False, "expected exception" + except Exception: + pass diff --git a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py index 0766008b84..80684b9e6e 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py @@ -1,4 +1,5 @@ from eth_hash.auto import keccak +from hashlib import sha256 from trie import HexaryTrie from rlp import encode from rlp.sedes import big_endian_int, Binary, List @@ -7,7 +8,12 @@ from eth2spec.utils.ssz.ssz_impl import hash_tree_root from eth2spec.debug.random_value import get_random_bytes_list from eth2spec.test.helpers.withdrawals import get_expected_withdrawals -from eth2spec.test.helpers.forks import is_post_capella, is_post_deneb, is_post_eip7732 +from eth2spec.test.helpers.forks import ( + is_post_capella, + is_post_deneb, + is_post_electra, + is_post_eip7732, +) def get_execution_payload_header(spec, execution_payload): @@ -59,13 +65,23 @@ def compute_trie_root_from_indexed_data(data): return t.root_hash +# https://eips.ethereum.org/EIPS/eip-7685 +def compute_requests_hash(block_requests): + m = sha256() + for r in block_requests: + if len(r) > 1: + m.update(sha256(r).digest()) + return m.digest() + + # https://eips.ethereum.org/EIPS/eip-4895 # https://eips.ethereum.org/EIPS/eip-4844 def compute_el_header_block_hash(spec, payload_header, transactions_trie_root, withdrawals_trie_root=None, - parent_beacon_block_root=None): + parent_beacon_block_root=None, + requests_hash=None): """ Computes the RLP execution block hash described by an `ExecutionPayloadHeader`. """ @@ -116,6 +132,9 @@ def compute_el_header_block_hash(spec, execution_payload_header_rlp.append((big_endian_int, payload_header.excess_blob_gas)) # parent_beacon_root execution_payload_header_rlp.append((Binary(32, 32), parent_beacon_block_root)) + if is_post_electra(spec): + # requests_hash + execution_payload_header_rlp.append((Binary(32, 32), requests_hash)) sedes = List([schema for schema, _ in execution_payload_header_rlp]) values = [value for _, value in execution_payload_header_rlp] @@ -191,7 +210,7 @@ def get_consolidation_request_rlp_bytes(consolidation_request): return b"\x02" + encode(values, sedes) -def compute_el_block_hash_with_parent_root(spec, payload, parent_beacon_block_root): +def compute_el_block_hash_with_new_fields(spec, payload, parent_beacon_block_root, requests_hash): if payload == spec.ExecutionPayload(): return spec.Hash32() @@ -213,25 +232,35 @@ def compute_el_block_hash_with_parent_root(spec, payload, parent_beacon_block_ro transactions_trie_root, withdrawals_trie_root, parent_beacon_block_root, + requests_hash, ) def compute_el_block_hash(spec, payload, pre_state): parent_beacon_block_root = None + requests_hash = None if is_post_deneb(spec): previous_block_header = pre_state.latest_block_header.copy() if previous_block_header.state_root == spec.Root(): previous_block_header.state_root = pre_state.hash_tree_root() parent_beacon_block_root = previous_block_header.hash_tree_root() + if is_post_electra(spec): + requests_hash = compute_requests_hash([]) - return compute_el_block_hash_with_parent_root( - spec, payload, parent_beacon_block_root) + return compute_el_block_hash_with_new_fields( + spec, payload, parent_beacon_block_root, requests_hash) def compute_el_block_hash_for_block(spec, block): - return compute_el_block_hash_with_parent_root( - spec, block.body.execution_payload, block.parent_root) + requests_hash = None + + if is_post_electra(spec): + requests_list = spec.get_execution_requests_list(block.body.execution_requests) + requests_hash = compute_requests_hash(requests_list) + + return compute_el_block_hash_with_new_fields( + spec, block.body.execution_payload, block.parent_root, requests_hash) def build_empty_post_eip7732_execution_payload_header(spec, state): diff --git a/tests/core/pyspec/eth2spec/test/helpers/genesis.py b/tests/core/pyspec/eth2spec/test/helpers/genesis.py index bd4e5d3bf3..9c43676a41 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/genesis.py +++ b/tests/core/pyspec/eth2spec/test/helpers/genesis.py @@ -1,3 +1,4 @@ +from hashlib import sha256 from eth2spec.test.helpers.constants import ( PHASE0, PREVIOUS_FORK_OF, @@ -66,11 +67,14 @@ def get_sample_genesis_execution_payload_header(spec, transactions_trie_root = bytes.fromhex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") withdrawals_trie_root = None parent_beacon_block_root = None + requests_hash = None if is_post_capella(spec): withdrawals_trie_root = bytes.fromhex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") if is_post_deneb(spec): parent_beacon_block_root = bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000000") + if is_post_electra(spec): + requests_hash = sha256(b"").digest() payload_header.block_hash = compute_el_header_block_hash( spec, @@ -78,6 +82,7 @@ def get_sample_genesis_execution_payload_header(spec, transactions_trie_root, withdrawals_trie_root, parent_beacon_block_root, + requests_hash, ) return payload_header