diff --git a/setup.py b/setup.py index be8be6d90..f95f85284 100644 --- a/setup.py +++ b/setup.py @@ -660,6 +660,7 @@ def get_empty_list_result(fn): # type: ignore process_withdrawals = no_op(process_withdrawals) process_bls_to_execution_change = no_op(process_bls_to_execution_change) get_expected_withdrawals = get_empty_list_result(get_expected_withdrawals) +process_historical_summaries_update = no_op(process_historical_summaries_update) # diff --git a/specs/capella/beacon-chain.md b/specs/capella/beacon-chain.md index 20f4a5dd4..bd7a2c50c 100644 --- a/specs/capella/beacon-chain.md +++ b/specs/capella/beacon-chain.md @@ -18,6 +18,7 @@ - [`Withdrawal`](#withdrawal) - [`BLSToExecutionChange`](#blstoexecutionchange) - [`SignedBLSToExecutionChange`](#signedblstoexecutionchange) + - [`HistoricalSummary`](#historicalsummary) - [Extended Containers](#extended-containers) - [`ExecutionPayload`](#executionpayload) - [`ExecutionPayloadHeader`](#executionpayloadheader) @@ -29,6 +30,8 @@ - [`is_fully_withdrawable_validator`](#is_fully_withdrawable_validator) - [`is_partially_withdrawable_validator`](#is_partially_withdrawable_validator) - [Beacon chain state transition function](#beacon-chain-state-transition-function) + - [Epoch processing](#epoch-processing) + - [Historical summaries updates](#historical-summaries-updates) - [Block processing](#block-processing) - [New `get_expected_withdrawals`](#new-get_expected_withdrawals) - [New `process_withdrawals`](#new-process_withdrawals) @@ -115,6 +118,18 @@ class SignedBLSToExecutionChange(Container): signature: BLSSignature ``` +#### `HistoricalSummary` + +```python +class HistoricalSummary(Container): + """ + `HistoricalSummary` matches the components of the phase0 `HistoricalBatch` + making the two hash_tree_root-compatible. + """ + block_summary_root: Root + state_summary_root: Root +``` + ### Extended Containers #### `ExecutionPayload` @@ -196,7 +211,7 @@ class BeaconState(Container): latest_block_header: BeaconBlockHeader block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] - historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] # Frozen in Capella, replaced by historical_summaries # Eth1 eth1_data: Eth1Data eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] @@ -226,6 +241,8 @@ class BeaconState(Container): # Withdrawals next_withdrawal_index: WithdrawalIndex # [New in Capella] next_withdrawal_validator_index: ValidatorIndex # [New in Capella] + # Deep history valid from Capella onwards + historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] # [New in Capella] ``` ## Helpers @@ -270,6 +287,40 @@ def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> ## Beacon chain state transition function +### Epoch processing + +*Note*: The function `process_historical_summaries_update` replaces `process_historical_roots_update` in Bellatrix. + +```python +def process_epoch(state: BeaconState) -> None: + process_justification_and_finalization(state) + process_inactivity_updates(state) + process_rewards_and_penalties(state) + process_registry_updates(state) + process_slashings(state) + process_eth1_data_reset(state) + process_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) + process_historical_summaries_update(state) # [Modified in Capella] + process_participation_flag_updates(state) + process_sync_committee_updates(state) +``` + +#### Historical summaries updates + +```python +def process_historical_summaries_update(state: BeaconState) -> None: + # Set historical block root accumulator. + next_epoch = Epoch(get_current_epoch(state) + 1) + if next_epoch % (SLOTS_PER_HISTORICAL_ROOT // SLOTS_PER_EPOCH) == 0: + historical_summary = HistoricalSummary( + block_summary_root=hash_tree_root(state.block_roots), + state_summary_root=hash_tree_root(state.state_roots), + ) + state.historical_summaries.append(historical_summary) +``` + ### Block processing ```python diff --git a/tests/core/pyspec/eth2spec/test/capella/epoch_processing/__init__.py b/tests/core/pyspec/eth2spec/test/capella/epoch_processing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_historical_batches_update.py b/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_historical_batches_update.py new file mode 100644 index 000000000..f1f8292eb --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_historical_batches_update.py @@ -0,0 +1,27 @@ +from eth2spec.test.context import ( + CAPELLA, + spec_state_test, + with_phases, +) +from eth2spec.test.helpers.epoch_processing import ( + run_epoch_processing_with +) + + +def run_process_historical_summaries_update(spec, state): + yield from run_epoch_processing_with(spec, state, 'process_historical_summaries_update') + + +@with_phases([CAPELLA]) +@spec_state_test +def test_historical_summaries_accumulator(spec, state): + # skip ahead to near the end of the historical batch period (excl block before epoch processing) + state.slot = spec.SLOTS_PER_HISTORICAL_ROOT - 1 + pre_historical_summaries = state.historical_summaries.copy() + + yield from run_process_historical_summaries_update(spec, state) + + assert len(state.historical_summaries) == len(pre_historical_summaries) + 1 + summary = state.historical_summaries[len(state.historical_summaries) - 1] + assert summary.block_summary_root == state.block_roots.hash_tree_root() + assert summary.state_summary_root == state.state_roots.hash_tree_root() diff --git a/tests/core/pyspec/eth2spec/test/eip4844/epoch_processing/__init__.py b/tests/core/pyspec/eth2spec/test/eip4844/epoch_processing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/eip4844/epoch_processing/test_process_historical_batches_update.py b/tests/core/pyspec/eth2spec/test/eip4844/epoch_processing/test_process_historical_batches_update.py new file mode 100644 index 000000000..21865ae38 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/eip4844/epoch_processing/test_process_historical_batches_update.py @@ -0,0 +1,23 @@ +from eth2spec.test.context import ( + spec_state_test, + with_eip4844_and_later, +) +from eth2spec.test.helpers.epoch_processing import ( + run_epoch_processing_with +) + + +def run_process_historical_summaries_update(spec, state): + yield from run_epoch_processing_with(spec, state, 'process_historical_summaries_update') + + +@with_eip4844_and_later +@spec_state_test +def test_no_op(spec, state): + # skip ahead to near the end of the historical batch period (excl block before epoch processing) + state.slot = spec.SLOTS_PER_HISTORICAL_ROOT - 1 + historical_summaries_len = len(state.historical_summaries) + + yield from run_process_historical_summaries_update(spec, state) + + assert len(state.historical_summaries) == historical_summaries_len diff --git a/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py b/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py index ed61f8bdb..44b42aff9 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py +++ b/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py @@ -1,5 +1,8 @@ -from eth2spec.test.helpers.forks import is_post_altair +from eth2spec.test.helpers.forks import ( + is_post_altair, + is_post_capella, +) def get_process_calls(spec): @@ -22,7 +25,10 @@ def get_process_calls(spec): 'process_effective_balance_updates', 'process_slashings_reset', 'process_randao_mixes_reset', - 'process_historical_roots_update', + # Capella replaced `process_historical_roots_update` with `process_historical_summaries_update` + 'process_historical_summaries_update' if is_post_capella(spec) else ( + 'process_historical_roots_update' + ), # Altair replaced `process_participation_record_updates` with `process_participation_flag_updates` 'process_participation_flag_updates' if is_post_altair(spec) else ( 'process_participation_record_updates' diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_historical_roots_update.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_historical_roots_update.py index 02ce7ccba..f03d0bd98 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_historical_roots_update.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_historical_roots_update.py @@ -1,4 +1,8 @@ -from eth2spec.test.context import spec_state_test, with_all_phases +from eth2spec.test.context import ( + PHASE0, ALTAIR, BELLATRIX, + spec_state_test, + with_phases, +) from eth2spec.test.helpers.epoch_processing import ( run_epoch_processing_with ) @@ -8,7 +12,7 @@ def run_process_historical_roots_update(spec, state): yield from run_epoch_processing_with(spec, state, 'process_historical_roots_update') -@with_all_phases +@with_phases([PHASE0, ALTAIR, BELLATRIX]) @spec_state_test def test_historical_root_accumulator(spec, state): # skip ahead to near the end of the historical roots period (excl block before epoch processing) diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py index 1e9dd2d48..71c7798a1 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py @@ -29,8 +29,8 @@ from eth2spec.test.helpers.sync_committee import ( compute_committee_indices, compute_sync_committee_participant_reward_and_penalty, ) -from eth2spec.test.helpers.constants import PHASE0, MINIMAL -from eth2spec.test.helpers.forks import is_post_altair, is_post_bellatrix +from eth2spec.test.helpers.constants import PHASE0, EIP4844, MINIMAL +from eth2spec.test.helpers.forks import is_post_altair, is_post_bellatrix, is_post_capella from eth2spec.test.context import ( spec_test, spec_state_test, dump_skipping_message, with_phases, with_all_phases, single_phase, @@ -1026,7 +1026,10 @@ def test_balance_driven_status_transitions(spec, state): @always_bls def test_historical_batch(spec, state): state.slot += spec.SLOTS_PER_HISTORICAL_ROOT - (state.slot % spec.SLOTS_PER_HISTORICAL_ROOT) - 1 - pre_historical_roots_len = len(state.historical_roots) + pre_historical_roots = state.historical_roots.copy() + + if is_post_capella(spec): + pre_historical_summaries = state.historical_summaries.copy() yield 'pre', state @@ -1038,7 +1041,18 @@ def test_historical_batch(spec, state): assert state.slot == block.slot assert spec.get_current_epoch(state) % (spec.SLOTS_PER_HISTORICAL_ROOT // spec.SLOTS_PER_EPOCH) == 0 - assert len(state.historical_roots) == pre_historical_roots_len + 1 + + # check history update + if is_post_capella(spec): + # Frozen `historical_roots` + assert state.historical_roots == pre_historical_roots + if spec.fork == EIP4844: + # TODO: no-op for now in EIP4844 testnet + assert state.historical_summaries == pre_historical_summaries + else: + assert len(state.historical_summaries) == len(pre_historical_summaries) + 1 + else: + assert len(state.historical_roots) == len(pre_historical_roots) + 1 @with_all_phases diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_slots.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_slots.py index 3fa57f0f1..7b860159a 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_slots.py +++ b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_slots.py @@ -1,3 +1,9 @@ +from eth2spec.test.helpers.constants import ( + EIP4844, +) +from eth2spec.test.helpers.forks import ( + is_post_capella, +) from eth2spec.test.helpers.state import get_state_root from eth2spec.test.context import ( spec_state_test, @@ -61,3 +67,30 @@ def test_over_epoch_boundary(spec, state): yield 'slots', int(slots) spec.process_slots(state, state.slot + slots) yield 'post', state + + +@with_all_phases +@spec_state_test +def test_historical_accumulator(spec, state): + pre_historical_roots = state.historical_roots.copy() + + if is_post_capella(spec): + pre_historical_summaries = state.historical_summaries.copy() + + yield 'pre', state + slots = spec.SLOTS_PER_HISTORICAL_ROOT + yield 'slots', int(slots) + spec.process_slots(state, state.slot + slots) + yield 'post', state + + # check history update + if is_post_capella(spec): + # Frozen `historical_roots` + assert state.historical_roots == pre_historical_roots + if spec.fork == EIP4844: + # TODO: no-op for now in EIP4844 testnet + assert state.historical_summaries == pre_historical_summaries + else: + assert len(state.historical_summaries) == len(pre_historical_summaries) + 1 + else: + assert len(state.historical_roots) == len(pre_historical_roots) + 1 diff --git a/tests/generators/epoch_processing/main.py b/tests/generators/epoch_processing/main.py index 84421e749..58beb0fd2 100644 --- a/tests/generators/epoch_processing/main.py +++ b/tests/generators/epoch_processing/main.py @@ -27,13 +27,15 @@ if __name__ == "__main__": # so no additional tests required. bellatrix_mods = altair_mods - # No epoch-processing changes in Capella and previous testing repeats with new types, - # so no additional tests required. - capella_mods = bellatrix_mods + _new_capella_mods = {key: 'eth2spec.test.capella.epoch_processing.test_process_' + key for key in [ + 'historical_summaries_update', + ]} + capella_mods = combine_mods(_new_capella_mods, bellatrix_mods) - # No epoch-processing changes in EIP4844 and previous testing repeats with new types, - # so no additional tests required. - eip4844_mods = capella_mods + _new_eip4844_mods = {key: 'eth2spec.test.eip4844.epoch_processing.test_process_' + key for key in [ + 'historical_summaries_update', + ]} + eip4844_mods = combine_mods(_new_eip4844_mods, capella_mods) # TODO Custody Game testgen is disabled for now # custody_game_mods = {**{key: 'eth2spec.test.custody_game.epoch_processing.test_process_' + key for key in [