Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TheVerge: spec draft #3230

Merged
merged 15 commits into from
May 28, 2024
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions presets/mainnet/eip6800.yaml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions presets/minimal/eip6800.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion pysetup/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
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:
Expand Down
2 changes: 1 addition & 1 deletion pysetup/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions pysetup/md_doc_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ELECTRA,
WHISK,
EIP7594,
EIP6800,
)


Expand All @@ -21,6 +22,7 @@
ELECTRA: DENEB,
WHISK: CAPELLA,
EIP7594: DENEB,
EIP6800: DENEB,
}

ALL_FORKS = list(PREVIOUS_FORK_OF.keys())
Expand Down
3 changes: 2 additions & 1 deletion pysetup/spec_builders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
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,
)
}
21 changes: 21 additions & 0 deletions pysetup/spec_builders/eip6800.py
Original file line number Diff line number Diff line change
@@ -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,
}
8 changes: 7 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
221 changes: 221 additions & 0 deletions specs/_features/eip6800/beacon-chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# EIP6800 -- The Beacon Chain

## Table of contents

<!-- TOC -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [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)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- /TOC -->

## 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]
Comment on lines +118 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should either be updated for EIP-7495 StableContainer / Profile (on SuffixStateDiff), or unwrapped so that e.g. a zero value is used instead of None. Would like to withdraw EIP-6475 style Optional as the functionality is covered by the more advanced EIP-7495 as well.

SSZ library implementation progress here: https://stabilitynow.box

Copy link
Contributor

@hwwhww hwwhww May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right.

@gballet
The current Optional in pyspec is the Python typing.Optional. Our SSZ lib Remerkleable only implemented SSZ Union, so technically we can use Union[None, Bytes32] here in the specs. However, other SSZ libs didn't implement Union and may go implement the newer SSZ types as @etan-status suggested.

Is it possible to use Bytes32(), the empty 0x00...00, to represent null in this case so that we don't introduce new SSZ typing here?

Copy link
Member Author

@gballet gballet May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no it is not possible to use Bytes32() and I was initially using Union[None, Byte32] and was asked to replace it with Optional. Precisely because Union didn't exist (except in the wonderful gballet/ssz.zig, of course 😇).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to give StableContainer a shot, this is how it would look there:

class StableSuffixStateDiff(StableContainer[8]):  # or [4] if this is not planned to grow in the future
    suffix: Optional[Bytes1]
    # Null means not currently present
    current_value: Optional[Bytes32]
    # Null means value not updated
    new_value: Optional[Bytes32]

class SuffixStateDiff(Profile[StableSuffixStateDiff]):
    suffix: Bytes1
    # Null means not currently present
    current_value: Optional[Bytes32]
    # Null means value not updated
    new_value: Optional[Bytes32]

Serialization of the Profile looks like this:

  1. 1 byte optional_fields, 0b1/0 for current_value presence/absence, 0b1/0 for new_value presence/absence
  2. 1 byte suffix
  3. 32 bytes current_value if present, or nothing otherwise
  4. 32 bytes new_value if present, or nothing otherwise

Notably, no variable length offsets, this is 7-9 bytes more compact than the current EIP-6475 based impl, and requires one fewer hash as well.

Merkleization would look like this, with htr == hash_tree_root and based on 8 capacity. can lower to 4 if sufficient, should reflect theoretical design space, not actual space used today, for future extensibility without breaking merkleization of existing EIP-4788 based verifiers:

  • htr
    • htr
      • htr
        • htr
          • htr(suffix)
          • htr(current_value) if current_value is not None else htr(0)
        • htr
          • htr(new_value) if new_value is not None else htr(0)
          • htr(0)
      • htr
        • htr
          • htr(0)
          • htr(0)
        • htr
          • htr(0)
          • htr(0)
    • active_fields (suffix always 0b1, then 0b0/0b1 for current_value, then 0b0/0b1 for new_value, rest 0)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs custom remerkleable from here:

#3777

```

*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=payload.execution_witness, # [New in EIP6800]
gballet marked this conversation as resolved.
Show resolved Hide resolved
)
```

## Testing

TBD
Loading