diff --git a/.gitignore b/.gitignore index 3e4413e97..3586b356c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ tests/core/pyspec/eth2spec/deneb/ tests/core/pyspec/eth2spec/electra/ tests/core/pyspec/eth2spec/whisk/ tests/core/pyspec/eth2spec/eip7594/ +tests/core/pyspec/eth2spec/eip6800/ # coverage reports .htmlcov diff --git a/Makefile b/Makefile index 8fbedf251..bdf4bdde7 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ MARKDOWN_FILES = $(wildcard $(SPEC_DIR)/*/*.md) \ $(wildcard $(SPEC_DIR)/_features/*/*/*.md) \ $(wildcard $(SSZ_DIR)/*.md) -ALL_EXECUTABLE_SPEC_NAMES = phase0 altair bellatrix capella deneb electra whisk +ALL_EXECUTABLE_SPEC_NAMES = phase0 altair bellatrix capella deneb electra whisk eip6800 # The parameters for commands. Use `foreach` to avoid listing specs again. COVERAGE_SCOPE := $(foreach S,$(ALL_EXECUTABLE_SPEC_NAMES), --cov=eth2spec.$S.$(TEST_PRESET_TYPE)) PYLINT_SCOPE := $(foreach S,$(ALL_EXECUTABLE_SPEC_NAMES), ./eth2spec/$S) diff --git a/presets/mainnet/eip6800.yaml b/presets/mainnet/eip6800.yaml new file mode 100644 index 000000000..d74ee6212 --- /dev/null +++ b/presets/mainnet/eip6800.yaml @@ -0,0 +1,12 @@ +# Mainnet preset - EIP6800 + +# Misc +# --------------------------------------------------------------- +# `uint64(2**16)` (= 65,536) +MAX_STEMS: 65536 +# `uint64(33)` +MAX_COMMITMENTS_PER_STEM: 33 +# `uint64(2**8)` (= 256) +VERKLE_WIDTH: 256 +# `uint64(2**3)` (= 8) +IPA_PROOF_DEPTH: 8 diff --git a/presets/minimal/eip6800.yaml b/presets/minimal/eip6800.yaml new file mode 100644 index 000000000..499721e4a --- /dev/null +++ b/presets/minimal/eip6800.yaml @@ -0,0 +1,12 @@ +# Minimal preset - EIP6800 + +# Execution +# --------------------------------------------------------------- +# `uint64(2**16)` (= 65,536) +MAX_STEMS: 65536 +# `uint64(33)` +MAX_COMMITMENTS_PER_STEM: 33 +# `uint64(2**8)` (= 256) +VERKLE_WIDTH: 256 +# `uint64(2**3)` (= 8) +IPA_PROOF_DEPTH: 8 diff --git a/pysetup/constants.py b/pysetup/constants.py index 0078b24dc..e26efb8e0 100644 --- a/pysetup/constants.py +++ b/pysetup/constants.py @@ -6,10 +6,10 @@ CAPELLA = 'capella' DENEB = 'deneb' ELECTRA = 'electra' EIP7594 = 'eip7594' +EIP6800 = 'eip6800' WHISK = 'whisk' - # The helper functions that are used when defining constants CONSTANT_DEP_SUNDRY_CONSTANTS_FUNCTIONS = ''' def ceillog2(x: int) -> uint64: diff --git a/pysetup/helpers.py b/pysetup/helpers.py index 49c0fcafc..589ae6ab5 100644 --- a/pysetup/helpers.py +++ b/pysetup/helpers.py @@ -178,7 +178,7 @@ def combine_dicts(old_dict: Dict[str, T], new_dict: Dict[str, T]) -> Dict[str, T ignored_dependencies = [ 'bit', 'boolean', 'Vector', 'List', 'Container', 'BLSPubkey', 'BLSSignature', - 'Bytes1', 'Bytes4', 'Bytes8', 'Bytes20', 'Bytes32', 'Bytes48', 'Bytes96', 'Bitlist', 'Bitvector', + 'Bytes1', 'Bytes4', 'Bytes8', 'Bytes20', 'Bytes31', 'Bytes32', 'Bytes48', 'Bytes96', 'Bitlist', 'Bitvector', 'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256', 'bytes', 'byte', 'ByteList', 'ByteVector', 'Dict', 'dict', 'field', 'ceillog2', 'floorlog2', 'Set', diff --git a/pysetup/md_doc_paths.py b/pysetup/md_doc_paths.py index a4a730e5a..28ebc7137 100644 --- a/pysetup/md_doc_paths.py +++ b/pysetup/md_doc_paths.py @@ -9,6 +9,7 @@ from .constants import ( ELECTRA, WHISK, EIP7594, + EIP6800, ) @@ -21,6 +22,7 @@ PREVIOUS_FORK_OF = { ELECTRA: DENEB, WHISK: CAPELLA, EIP7594: DENEB, + EIP6800: DENEB, } ALL_FORKS = list(PREVIOUS_FORK_OF.keys()) diff --git a/pysetup/spec_builders/__init__.py b/pysetup/spec_builders/__init__.py index ea74b50b7..922cee18b 100644 --- a/pysetup/spec_builders/__init__.py +++ b/pysetup/spec_builders/__init__.py @@ -6,12 +6,13 @@ from .deneb import DenebSpecBuilder from .electra import ElectraSpecBuilder from .whisk import WhiskSpecBuilder from .eip7594 import EIP7594SpecBuilder +from .eip6800 import EIP6800SpecBuilder spec_builders = { builder.fork: builder for builder in ( Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder, CapellaSpecBuilder, DenebSpecBuilder, - ElectraSpecBuilder, WhiskSpecBuilder, EIP7594SpecBuilder, + ElectraSpecBuilder, WhiskSpecBuilder, EIP7594SpecBuilder, EIP6800SpecBuilder, ) } diff --git a/pysetup/spec_builders/eip6800.py b/pysetup/spec_builders/eip6800.py new file mode 100644 index 000000000..4ea76d6a2 --- /dev/null +++ b/pysetup/spec_builders/eip6800.py @@ -0,0 +1,21 @@ +from typing import Dict + +from .base import BaseSpecBuilder +from ..constants import EIP6800 + + +class EIP6800SpecBuilder(BaseSpecBuilder): + fork: str = EIP6800 + + @classmethod + def imports(cls, preset_name: str): + return f''' +from eth2spec.deneb import {preset_name} as deneb +from eth2spec.utils.ssz.ssz_typing import Bytes31 +''' + + @classmethod + def hardcoded_custom_type_dep_constants(cls, spec_object) -> str: + return { + 'MAX_STEMS': spec_object.preset_vars['MAX_STEMS'].value, + } diff --git a/setup.py b/setup.py index fe2250f7c..e5c348ada 100644 --- a/setup.py +++ b/setup.py @@ -219,7 +219,13 @@ def get_spec(file_name: Path, preset: Dict[str, str], config: Dict[str, str], pr elif source.startswith("class"): class_name, parent_class = _get_class_info_from_source(source) # check consistency with spec - assert class_name == current_name + try: + assert class_name == current_name + except Exception: + print('class_name', class_name) + print('current_name', current_name) + raise + if parent_class: assert parent_class == "Container" # NOTE: trim whitespace from spec diff --git a/specs/_features/eip6800/beacon-chain.md b/specs/_features/eip6800/beacon-chain.md new file mode 100644 index 000000000..ab935cb87 --- /dev/null +++ b/specs/_features/eip6800/beacon-chain.md @@ -0,0 +1,221 @@ +# EIP6800 -- The Beacon Chain + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Custom types](#custom-types) +- [Preset](#preset) + - [Execution](#execution) +- [Containers](#containers) + - [Extended containers](#extended-containers) + - [`ExecutionPayload`](#executionpayload) + - [`ExecutionPayloadHeader`](#executionpayloadheader) + - [New containers](#new-containers) + - [`SuffixStateDiff`](#suffixstatediff) + - [`StemStateDiff`](#stemstatediff) + - [`IPAProof`](#ipaproof) + - [`VerkleProof`](#verkleproof) + - [`ExecutionWitness`](#executionwitness) +- [Beacon chain state transition function](#beacon-chain-state-transition-function) + - [Block processing](#block-processing) + - [Execution payload](#execution-payload) + - [`process_execution_payload`](#process_execution_payload) +- [Testing](#testing) + + + + +## Introduction + +This upgrade adds transaction execution to the beacon chain as part of the eip6800 upgrade. + +## Custom types + +| Name | SSZ equivalent | Description | +| - | - | - | +| `BanderwagonGroupElement` | `Bytes32` | | +| `BanderwagonFieldElement` | `Bytes32` | | +| `Stem` | `Bytes31` | | + +## Preset + +### Execution + +| Name | Value | +| - | - | +| `MAX_STEMS` | `uint64(2**16)` (= 65,536) | +| `MAX_COMMITMENTS_PER_STEM` | `uint64(33)` | +| `VERKLE_WIDTH` | `uint64(2**8)` (= 256) | +| `IPA_PROOF_DEPTH` | `uint64(2**3)` (= 8) | + +## Containers + +### Extended containers + +#### `ExecutionPayload` + +```python +class ExecutionPayload(Container): + # Execution block header fields + parent_hash: Hash32 + fee_recipient: ExecutionAddress # 'beneficiary' in the yellow paper + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 # 'difficulty' in the yellow paper + block_number: uint64 # 'number' in the yellow paper + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + # Extra payload fields + block_hash: Hash32 # Hash of execution block + transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + blob_gas_used: uint64 + excess_blob_gas: uint64 + execution_witness: ExecutionWitness # [New in EIP6800] +``` + +#### `ExecutionPayloadHeader` + +```python +class ExecutionPayloadHeader(Container): + # Execution block header fields + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + # Extra payload fields + block_hash: Hash32 # Hash of execution block + transactions_root: Root + withdrawals_root: Root + blob_gas_used: uint64 + excess_data_gas: uint64 + execution_witness_root: Root # [New in EIP6800] +``` + +### New containers + +#### `SuffixStateDiff` + +```python +class SuffixStateDiff(Container): + suffix: Bytes1 + # Null means not currently present + current_value: Optional[Bytes32] + # Null means value not updated + new_value: Optional[Bytes32] +``` + +*Note*: on the Kaustinen testnet, `new_value` is omitted from the container. + +#### `StemStateDiff` + +```python +class StemStateDiff(Container): + stem: Stem + # Valid only if list is sorted by suffixes + suffix_diffs: List[SuffixStateDiff, VERKLE_WIDTH] +``` + +#### `IPAProof` + +```python +class IPAProof(Container): + cl: Vector[BanderwagonGroupElement, IPA_PROOF_DEPTH] + cr: Vector[BanderwagonGroupElement, IPA_PROOF_DEPTH] + final_evaluation = BanderwagonFieldElement +``` + +#### `VerkleProof` + +```python +class VerkleProof(Container): + other_stems: List[Bytes31, MAX_STEMS] + depth_extension_present: ByteList[MAX_STEMS] + commitments_by_path: List[BanderwagonGroupElement, MAX_STEMS * MAX_COMMITMENTS_PER_STEM] + d: BanderwagonGroupElement + ipa_proof: IPAProof +``` + +#### `ExecutionWitness` + +```python +class ExecutionWitness(Container): + state_diff: List[StemStateDiff, MAX_STEMS] + verkle_proof: VerkleProof +``` + +## Beacon chain state transition function + +### Block processing + +#### Execution payload + +##### `process_execution_payload` + +```python +def process_execution_payload(state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine) -> None: + payload = body.execution_payload + + # Verify consistency of the parent hash with respect to the previous execution payload header + assert payload.parent_hash == state.latest_execution_payload_header.block_hash + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) + # Verify timestamp + assert payload.timestamp == compute_timestamp_at_slot(state, state.slot) + + # Verify commitments are under limit + assert len(body.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK + + # Verify the execution payload is valid + # Pass `versioned_hashes` to Execution Engine + # Pass `parent_beacon_block_root` to Execution Engine + versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments] + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=versioned_hashes, + parent_beacon_block_root=state.latest_block_header.parent_root, + ) + ) + + # Cache execution payload header + state.latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + withdrawals_root=hash_tree_root(payload.withdrawals), + excess_data_gas=payload.excess_data_gas, + execution_witness_root=hash_tree_root(payload.execution_witness), # [New in EIP6800] + ) +``` + +## Testing + +TBD diff --git a/specs/_features/eip6800/fork.md b/specs/_features/eip6800/fork.md new file mode 100644 index 000000000..74f143f59 --- /dev/null +++ b/specs/_features/eip6800/fork.md @@ -0,0 +1,145 @@ +# EIP-6800 -- Fork Logic + +## Table of contents + + + + +- [Introduction](#introduction) +- [Configuration](#configuration) +- [Helper functions](#helper-functions) + - [Misc](#misc) + - [Modified `compute_fork_version`](#modified-compute_fork_version) +- [Fork to eip6800](#fork-to-eip6800) + - [Fork trigger](#fork-trigger) + - [Upgrading the state](#upgrading-the-state) + + + +## Introduction + +This document describes the process of the eip6800 upgrade. + +## Configuration + +Warning: this configuration is not definitive. + +| Name | Value | +| - | - | +| `EIP6800_FORK_VERSION` | `Version('0x05000000')` | +| `EIP6800_FORK_EPOCH` | `Epoch(18446744073709551615)` **TBD** | + + +## Helper functions + +### Misc + +#### Modified `compute_fork_version` + +```python +def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= EIP6800_FORK_EPOCH: + return EIP6800_FORK_VERSION + if epoch >= DENEB_FORK_EPOCH: + return DENEB_FORK_VERSION + if epoch >= CAPELLA_FORK_EPOCH: + return CAPELLA_FORK_VERSION + if epoch >= BELLATRIX_FORK_EPOCH: + return BELLATRIX_FORK_VERSION + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION +``` + +## Fork to eip6800 + +### Fork trigger + +The fork is triggered at epoch `EIP6800_FORK_EPOCH`. + +Note that for the pure eip6800 networks, we don't apply `upgrade_to_eip6800` since it starts with the eip6800 version logic. + +### Upgrading the state + +If `state.slot % SLOTS_PER_EPOCH == 0` and `compute_epoch_at_slot(state.slot) == EIP6800_FORK_EPOCH`, +an irregular state change is made to upgrade to eip6800. + +The upgrade occurs after the completion of the inner loop of `process_slots` that sets `state.slot` equal to `EIP6800_FORK_EPOCH * SLOTS_PER_EPOCH`. +Care must be taken when transitioning through the fork boundary as implementations will need a modified [state transition function](../phase0/beacon-chain.md#beacon-chain-state-transition-function) that deviates from the Phase 0 document. +In particular, the outer `state_transition` function defined in the Phase 0 document will not expose the precise fork slot to execute the upgrade in the presence of skipped slots at the fork boundary. Instead, the logic must be within `process_slots`. + +```python +def upgrade_to_eip6800(pre: deneb.BeaconState) -> BeaconState: + epoch = capella.get_current_epoch(pre) + latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=pre.latest_execution_payload_header.parent_hash, + fee_recipient=pre.latest_execution_payload_header.fee_recipient, + state_root=pre.latest_execution_payload_header.state_root, + receipts_root=pre.latest_execution_payload_header.receipts_root, + logs_bloom=pre.latest_execution_payload_header.logs_bloom, + prev_randao=pre.latest_execution_payload_header.prev_randao, + block_number=pre.latest_execution_payload_header.block_number, + gas_limit=pre.latest_execution_payload_header.gas_limit, + gas_used=pre.latest_execution_payload_header.gas_used, + timestamp=pre.latest_execution_payload_header.timestamp, + extra_data=pre.latest_execution_payload_header.extra_data, + base_fee_per_gas=pre.latest_execution_payload_header.base_fee_per_gas, + excess_data_gas=uint256(0), + block_hash=pre.latest_execution_payload_header.block_hash, + transactions_root=pre.latest_execution_payload_header.transactions_root, + withdrawals_root=pre.latest_execution_payload_header.withdrawals_root, + execution_witness_root=hash_tree_root(ExecutionWitness([], [])) # New in eip6800 + ) + post = BeaconState( + # Versioning + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + current_version=EIP6800_FORK_VERSION, # [Modified in eip6800] + epoch=epoch, + ), + # History + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + # Eth1 + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + # Registry + validators=pre.validators, + balances=pre.balances, + # Randomness + randao_mixes=pre.randao_mixes, + # Slashings + slashings=pre.slashings, + # Participation + previous_epoch_participation=pre.previous_epoch_participation, + current_epoch_participation=pre.current_epoch_participation, + # Finality + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + # Inactivity + inactivity_scores=pre.inactivity_scores, + # Sync + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + # Execution-layer + latest_execution_payload_header=latest_execution_payload_header, + # Withdrawals + next_withdrawal_index=pre.next_withdrawal_index, + next_withdrawal_validator_index=pre.next_withdrawal_validator_index, + # Deep history valid from Capella onwards + historical_summaries=pre.historical_summaries, + ) + + return post +``` diff --git a/tests/core/pyspec/eth2spec/utils/ssz/ssz_typing.py b/tests/core/pyspec/eth2spec/utils/ssz/ssz_typing.py index 5a1b61d0b..1f3db2fe0 100644 --- a/tests/core/pyspec/eth2spec/utils/ssz/ssz_typing.py +++ b/tests/core/pyspec/eth2spec/utils/ssz/ssz_typing.py @@ -10,3 +10,4 @@ from remerkleable.core import BasicView, View, Path Bytes20 = ByteVector[20] # type: ignore +Bytes31 = ByteVector[31] # type: ignore