diff --git a/specs/bellatrix/beacon-chain.md b/specs/bellatrix/beacon-chain.md index 983ecda23..9285594ab 100644 --- a/specs/bellatrix/beacon-chain.md +++ b/specs/bellatrix/beacon-chain.md @@ -298,8 +298,6 @@ def slash_validator(state: BeaconState, increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward)) ``` - - ## Beacon chain state transition function ### Execution engine diff --git a/specs/bellatrix/fork-choice.md b/specs/bellatrix/fork-choice.md index 60a54da9c..312768e44 100644 --- a/specs/bellatrix/fork-choice.md +++ b/specs/bellatrix/fork-choice.md @@ -165,7 +165,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: assert block.parent_root in store.block_states # Make a copy of the state to avoid mutability issues pre_state = copy(store.block_states[block.parent_root]) - # Blocks cannot be in the future. If they are, their consideration must be delayed until the are in the past. + # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. assert get_current_slot(store) >= block.slot # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor) diff --git a/specs/capella/beacon-chain.md b/specs/capella/beacon-chain.md index 06659349a..40592a9bd 100644 --- a/specs/capella/beacon-chain.md +++ b/specs/capella/beacon-chain.md @@ -269,13 +269,13 @@ class BeaconState(Container): #### `withdraw` ```python -def withdraw_balance(state: BeaconState, index: ValidatorIndex, amount: Gwei) -> None: +def withdraw_balance(state: BeaconState, validator_index: ValidatorIndex, amount: Gwei) -> None: # Decrease the validator's balance - decrease_balance(state, index, amount) + decrease_balance(state, validator_index, amount) # Create a corresponding withdrawal receipt withdrawal = Withdrawal( index=state.next_withdrawal_index, - address=ExecutionAddress(state.validators[index].withdrawal_credentials[12:]), + address=ExecutionAddress(state.validators[validator_index].withdrawal_credentials[12:]), amount=amount, ) state.next_withdrawal_index = WithdrawalIndex(state.next_withdrawal_index + 1) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 1593e07fe..661ad613b 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -406,7 +406,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: assert block.parent_root in store.block_states # Make a copy of the state to avoid mutability issues pre_state = copy(store.block_states[block.parent_root]) - # Blocks cannot be in the future. If they are, their consideration must be delayed until the are in the past. + # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. assert get_current_slot(store) >= block.slot # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor) diff --git a/sync/optimistic.md b/sync/optimistic.md index ea0ed7500..c205773cb 100644 --- a/sync/optimistic.md +++ b/sync/optimistic.md @@ -1,5 +1,37 @@ # Optimistic Sync +## Table of contents + + + + +- [Introduction](#introduction) +- [Constants](#constants) +- [Helpers](#helpers) +- [Mechanisms](#mechanisms) + - [When to optimistically import blocks](#when-to-optimistically-import-blocks) + - [How to optimistically import blocks](#how-to-optimistically-import-blocks) + - [How to apply `latestValidHash` when payload status is `INVALID`](#how-to-apply-latestvalidhash-when-payload-status-is-invalid) + - [Execution Engine Errors](#execution-engine-errors) + - [Assumptions about Execution Engine Behaviour](#assumptions-about-execution-engine-behaviour) + - [Re-Orgs](#re-orgs) +- [Fork Choice](#fork-choice) + - [Fork Choice Poisoning](#fork-choice-poisoning) +- [Checkpoint Sync (Weak Subjectivity Sync)](#checkpoint-sync-weak-subjectivity-sync) +- [Validator assignments](#validator-assignments) + - [Block Production](#block-production) + - [Attesting](#attesting) + - [Participating in Sync Committees](#participating-in-sync-committees) +- [Ethereum Beacon APIs](#ethereum-beacon-apis) +- [Design Decision Rationale](#design-decision-rationale) + - [Why `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`?](#why-safe_slots_to_import_optimistically) + - [Transitioning from VALID -> INVALIDATED or INVALIDATED -> VALID](#transitioning-from-valid---invalidated-or-invalidated---valid) + - [What about Light Clients?](#what-about-light-clients) + - [What if `TERMINAL_BLOCK_HASH` is used?](#what-if-terminal_block_hash-is-used) + + + + ## Introduction In order to provide a syncing execution engine with a partial view of the head @@ -163,6 +195,23 @@ the merge block MUST be treated the same as an `INVALIDATED` block (i.e., it and all its descendants are invalidated and removed from the block tree). +### How to apply `latestValidHash` when payload status is `INVALID` + +Processing an `INVALID` payload status depends on the `latestValidHash` parameter. +The general approach is as follows: +1. Consensus engine MUST identify `invalidBlock` as per definition in the table below. +2. `invalidBlock` and all of its descendants MUST be transitioned from `NOT_VALIDATED` to `INVALIDATED`. + +| `latestValidHash` | `invalidBlock` | +|:- |:- | +| Execution block hash | The *child* of a block with `body.execution_payload.block_hash == latestValidHash` in the chain containing the block with payload in question | +| `0x00..00` (all zeroes) | The first block with `body.execution_payload != ExecutionPayload()` in the chain containing a block with payload in question | +| `null` | Block with payload in question | + +When `latestValidHash` is a meaningful execution block hash but consensus engine +cannot find a block satisfying `body.execution_payload.block_hash == latestValidHash`, +consensus engine SHOULD behave the same as if `latestValidHash` was `null`. + ### Execution Engine Errors When an execution engine returns an error or fails to respond to a payload @@ -350,7 +399,7 @@ specification since it's only possible with a faulty EE. Such a scenario requires manual intervention. -## What about Light Clients? +### What about Light Clients? An alternative to optimistic sync is to run a light client inside/alongside beacon nodes that mitigates the need for optimistic sync by providing @@ -362,7 +411,7 @@ A notable thing about optimistic sync is that it's *optional*. Should an implementation decide to go the light-client route, then they can just ignore optimistic sync all together. -## What if `TERMINAL_BLOCK_HASH` is used? +### What if `TERMINAL_BLOCK_HASH` is used? If the terminal block hash override is used (i.e., `TERMINAL_BLOCK_HASH != Hash32()`), the [`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block) diff --git a/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_execution_payload.py b/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_execution_payload.py index cd7a74259..90125b69c 100644 --- a/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_execution_payload.py @@ -1,3 +1,6 @@ +from random import Random + +from eth2spec.debug.random_value import get_random_bytes_list from eth2spec.test.helpers.execution_payload import ( build_empty_execution_payload, get_execution_payload_header, @@ -46,14 +49,8 @@ def run_execution_payload_processing(spec, state, execution_payload, valid=True, assert state.latest_execution_payload_header == get_execution_payload_header(spec, execution_payload) -@with_bellatrix_and_later -@spec_state_test -def test_success_first_payload(spec, state): - # pre-state - state = build_state_with_incomplete_transition(spec, state) +def run_success_test(spec, state): next_slot(spec, state) - - # execution payload execution_payload = build_empty_execution_payload(spec, state) yield from run_execution_payload_processing(spec, state, execution_payload) @@ -61,12 +58,23 @@ def test_success_first_payload(spec, state): @with_bellatrix_and_later @spec_state_test -def test_success_regular_payload(spec, state): - # pre-state - state = build_state_with_complete_transition(spec, state) - next_slot(spec, state) +def test_success_first_payload(spec, state): + state = build_state_with_incomplete_transition(spec, state) - # execution payload + yield from run_success_test(spec, state) + + +@with_bellatrix_and_later +@spec_state_test +def test_success_regular_payload(spec, state): + state = build_state_with_complete_transition(spec, state) + + yield from run_success_test(spec, state) + + +def run_gap_slot_test(spec, state): + next_slot(spec, state) + next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) yield from run_execution_payload_processing(spec, state, execution_payload) @@ -75,83 +83,66 @@ def test_success_regular_payload(spec, state): @with_bellatrix_and_later @spec_state_test def test_success_first_payload_with_gap_slot(spec, state): - # pre-state state = build_state_with_incomplete_transition(spec, state) - next_slot(spec, state) - next_slot(spec, state) - - # execution payload - execution_payload = build_empty_execution_payload(spec, state) - - yield from run_execution_payload_processing(spec, state, execution_payload) + yield from run_gap_slot_test(spec, state) @with_bellatrix_and_later @spec_state_test def test_success_regular_payload_with_gap_slot(spec, state): - # pre-state state = build_state_with_complete_transition(spec, state) - next_slot(spec, state) - next_slot(spec, state) + yield from run_gap_slot_test(spec, state) - # execution payload + +def run_bad_execution_test(spec, state): + # completely valid payload, but execution itself fails (e.g. block exceeds gas limit) + next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) - yield from run_execution_payload_processing(spec, state, execution_payload) + yield from run_execution_payload_processing(spec, state, execution_payload, valid=False, execution_valid=False) @with_bellatrix_and_later @spec_state_test def test_bad_execution_first_payload(spec, state): - # completely valid payload, but execution itself fails (e.g. block exceeds gas limit) - - # pre-state state = build_state_with_incomplete_transition(spec, state) - next_slot(spec, state) - - # execution payload - execution_payload = build_empty_execution_payload(spec, state) - - yield from run_execution_payload_processing(spec, state, execution_payload, valid=False, execution_valid=False) + yield from run_bad_execution_test(spec, state) @with_bellatrix_and_later @spec_state_test def test_bad_execution_regular_payload(spec, state): - # completely valid payload, but execution itself fails (e.g. block exceeds gas limit) - - # pre-state state = build_state_with_complete_transition(spec, state) + yield from run_bad_execution_test(spec, state) + + +@with_bellatrix_and_later +@spec_state_test +def test_bad_parent_hash_first_payload(spec, state): + state = build_state_with_incomplete_transition(spec, state) next_slot(spec, state) - # execution payload execution_payload = build_empty_execution_payload(spec, state) + execution_payload.parent_hash = b'\x55' * 32 - yield from run_execution_payload_processing(spec, state, execution_payload, valid=False, execution_valid=False) + yield from run_execution_payload_processing(spec, state, execution_payload, valid=True) @with_bellatrix_and_later @spec_state_test def test_bad_parent_hash_regular_payload(spec, state): - # pre-state state = build_state_with_complete_transition(spec, state) next_slot(spec, state) - # execution payload execution_payload = build_empty_execution_payload(spec, state) execution_payload.parent_hash = spec.Hash32() yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) -@with_bellatrix_and_later -@spec_state_test -def test_bad_random_first_payload(spec, state): - # pre-state - state = build_state_with_incomplete_transition(spec, state) +def run_bad_prev_randao_test(spec, state): next_slot(spec, state) - # execution payload execution_payload = build_empty_execution_payload(spec, state) execution_payload.prev_randao = b'\x42' * 32 @@ -160,26 +151,21 @@ def test_bad_random_first_payload(spec, state): @with_bellatrix_and_later @spec_state_test -def test_bad_random_regular_payload(spec, state): - # pre-state - state = build_state_with_complete_transition(spec, state) - next_slot(spec, state) - - # execution payload - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.prev_randao = b'\x04' * 32 - - yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) +def test_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 @spec_state_test -def test_bad_everything_regular_payload(spec, state): - # pre-state +def test_bad_pre_randao_regular_payload(spec, state): state = build_state_with_complete_transition(spec, state) + yield from run_bad_prev_randao_test(spec, state) + + +def run_bad_everything_test(spec, state): next_slot(spec, state) - # execution payload execution_payload = build_empty_execution_payload(spec, state) execution_payload.parent_hash = spec.Hash32() execution_payload.prev_randao = spec.Bytes32() @@ -190,59 +176,198 @@ def test_bad_everything_regular_payload(spec, state): @with_bellatrix_and_later @spec_state_test -def test_bad_timestamp_first_payload(spec, state): - # pre-state +def test_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 +@spec_state_test +def test_bad_everything_regular_payload(spec, state): + state = build_state_with_complete_transition(spec, state) + yield from run_bad_everything_test(spec, state) + + +def run_bad_timestamp_test(spec, state, is_future): next_slot(spec, state) # execution payload execution_payload = build_empty_execution_payload(spec, state) - execution_payload.timestamp = execution_payload.timestamp + 1 + if is_future: + timestamp = execution_payload.timestamp + 1 + else: + timestamp = execution_payload.timestamp - 1 + execution_payload.timestamp = timestamp yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) @with_bellatrix_and_later @spec_state_test -def test_bad_timestamp_regular_payload(spec, state): - # pre-state +def test_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 +@spec_state_test +def test_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 +@spec_state_test +def test_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 +@spec_state_test +def test_past_timestamp_regular_payload(spec, state): + state = build_state_with_complete_transition(spec, state) + yield from run_bad_timestamp_test(spec, state, is_future=False) + + +def run_non_empty_extra_data_test(spec, state): next_slot(spec, state) - # execution payload execution_payload = build_empty_execution_payload(spec, state) - execution_payload.timestamp = execution_payload.timestamp + 1 + execution_payload.extra_data = b'\x45' * 12 - yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) + yield from run_execution_payload_processing(spec, state, execution_payload) + assert state.latest_execution_payload_header.extra_data == execution_payload.extra_data @with_bellatrix_and_later @spec_state_test def test_non_empty_extra_data_first_payload(spec, state): - # pre-state state = build_state_with_incomplete_transition(spec, state) - next_slot(spec, state) - - # execution payload - execution_payload = build_empty_execution_payload(spec, state) - execution_payload.extra_data = b'\x45' * 12 - - yield from run_execution_payload_processing(spec, state, execution_payload) - - assert state.latest_execution_payload_header.extra_data == execution_payload.extra_data + yield from run_non_empty_extra_data_test(spec, state) @with_bellatrix_and_later @spec_state_test def test_non_empty_extra_data_regular_payload(spec, state): - # pre-state state = build_state_with_complete_transition(spec, state) + yield from run_non_empty_extra_data_test(spec, state) + + +def run_non_empty_transactions_test(spec, state): next_slot(spec, state) - # execution payload execution_payload = build_empty_execution_payload(spec, state) - execution_payload.extra_data = b'\x45' * 12 + num_transactions = 2 + execution_payload.transactions = [ + spec.Transaction(b'\x99' * 128) + for _ in range(num_transactions) + ] yield from run_execution_payload_processing(spec, state, execution_payload) + assert state.latest_execution_payload_header.transactions_root == execution_payload.transactions.hash_tree_root() - assert state.latest_execution_payload_header.extra_data == execution_payload.extra_data + +@with_bellatrix_and_later +@spec_state_test +def test_non_empty_transactions_first_payload(spec, state): + state = build_state_with_incomplete_transition(spec, state) + yield from run_non_empty_extra_data_test(spec, state) + + +@with_bellatrix_and_later +@spec_state_test +def test_non_empty_transactions_regular_payload(spec, state): + state = build_state_with_complete_transition(spec, state) + yield from run_non_empty_extra_data_test(spec, state) + + +def run_zero_length_transaction_test(spec, state): + next_slot(spec, state) + + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.transactions = [spec.Transaction(b'')] + assert len(execution_payload.transactions[0]) == 0 + + yield from run_execution_payload_processing(spec, state, execution_payload) + assert state.latest_execution_payload_header.transactions_root == execution_payload.transactions.hash_tree_root() + + +@with_bellatrix_and_later +@spec_state_test +def test_zero_length_transaction_first_payload(spec, state): + state = build_state_with_incomplete_transition(spec, state) + yield from run_zero_length_transaction_test(spec, state) + + +@with_bellatrix_and_later +@spec_state_test +def test_zero_length_transaction_regular_payload(spec, state): + state = build_state_with_complete_transition(spec, state) + yield from run_zero_length_transaction_test(spec, state) + + +def build_randomized_execution_payload(spec, state, rng): + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.fee_recipient = spec.ExecutionAddress(get_random_bytes_list(rng, 20)) + execution_payload.state_root = spec.Bytes32(get_random_bytes_list(rng, 32)) + execution_payload.receipts_root = spec.Bytes32(get_random_bytes_list(rng, 32)) + execution_payload.logs_bloom = spec.ByteVector[spec.BYTES_PER_LOGS_BLOOM]( + get_random_bytes_list(rng, spec.BYTES_PER_LOGS_BLOOM) + ) + execution_payload.block_number = rng.randint(0, 10e10) + execution_payload.gas_limit = rng.randint(0, 10e10) + execution_payload.gas_used = rng.randint(0, 10e10) + extra_data_length = rng.randint(0, spec.MAX_EXTRA_DATA_BYTES) + execution_payload.extra_data = spec.ByteList[spec.MAX_EXTRA_DATA_BYTES]( + get_random_bytes_list(rng, extra_data_length) + ) + execution_payload.base_fee_per_gas = rng.randint(0, 2**256 - 1) + execution_payload.block_hash = spec.Hash32(get_random_bytes_list(rng, 32)) + + num_transactions = rng.randint(0, 100) + execution_payload.transactions = [ + spec.Transaction(get_random_bytes_list(rng, rng.randint(0, 1000))) + for _ in range(num_transactions) + ] + + return execution_payload + + +def run_randomized_non_validated_execution_fields_test(spec, state, execution_valid=True, rng=Random(5555)): + next_slot(spec, state) + execution_payload = build_randomized_execution_payload(spec, state, rng) + + yield from run_execution_payload_processing( + spec, state, + execution_payload, + valid=execution_valid, execution_valid=execution_valid + ) + + +@with_bellatrix_and_later +@spec_state_test +def test_randomized_non_validated_execution_fields_first_payload__valid(spec, state): + state = build_state_with_incomplete_transition(spec, state) + yield from run_randomized_non_validated_execution_fields_test(spec, state) + + +@with_bellatrix_and_later +@spec_state_test +def test_randomized_non_validated_execution_fields_regular_payload__valid(spec, state): + state = build_state_with_complete_transition(spec, state) + yield from run_randomized_non_validated_execution_fields_test(spec, state) + + +@with_bellatrix_and_later +@spec_state_test +def test_randomized_non_validated_execution_fields_first_payload__invalid(spec, state): + state = build_state_with_incomplete_transition(spec, state) + yield from run_randomized_non_validated_execution_fields_test(spec, state, execution_valid=False) + + +@with_bellatrix_and_later +@spec_state_test +def test_randomized_non_validated_execution_fields_regular_payload__invalid(spec, state): + state = build_state_with_complete_transition(spec, state) + yield from run_randomized_non_validated_execution_fields_test(spec, state, execution_valid=False) diff --git a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py index 5225d4efe..cce4c89ed 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py @@ -61,14 +61,20 @@ def get_execution_payload_header(spec, execution_payload): def build_state_with_incomplete_transition(spec, state): - return build_state_with_execution_payload_header(spec, state, spec.ExecutionPayloadHeader()) + state = build_state_with_execution_payload_header(spec, state, spec.ExecutionPayloadHeader()) + assert not spec.is_merge_transition_complete(state) + + return state def build_state_with_complete_transition(spec, state): pre_state_payload = build_empty_execution_payload(spec, state) payload_header = get_execution_payload_header(spec, pre_state_payload) - return build_state_with_execution_payload_header(spec, state, payload_header) + state = build_state_with_execution_payload_header(spec, state, payload_header) + assert spec.is_merge_transition_complete(state) + + return state def build_state_with_execution_payload_header(spec, state, execution_payload_header):