diff --git a/specs/fulu/beacon-chain.md b/specs/fulu/beacon-chain.md index d730eacac6..b36932478b 100644 --- a/specs/fulu/beacon-chain.md +++ b/specs/fulu/beacon-chain.md @@ -6,6 +6,7 @@ - [Introduction](#introduction) - [Configuration](#configuration) + - [Blob schedule](#blob-schedule) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - [Block processing](#block-processing) - [Execution payload](#execution-payload) @@ -15,6 +16,9 @@ - [`BeaconState`](#beaconstate) - [Helper functions](#helper-functions) - [Misc](#misc) + - [New `BlobParameters`](#new-blobparameters) + - [New `get_blob_parameters`](#new-get_blob_parameters) + - [Modified `compute_fork_digest`](#modified-compute_fork_digest) - [New `compute_proposer_indices`](#new-compute_proposer_indices) - [Beacon state accessors](#beacon-state-accessors) - [Modified `get_beacon_proposer_index`](#modified-get_beacon_proposer_index) @@ -32,6 +36,23 @@ and is under active development. ## Configuration +### Blob schedule + +*[New in Fulu:EIP7892]* This schedule defines the maximum blobs per block limit +for a given epoch. + +There MUST NOT exist multiple blob schedule entries with the same epoch value. +The maximum blobs per block limit for blob schedules entries MUST be less than +or equal to `MAX_BLOB_COMMITMENTS_PER_BLOCK`. The blob schedule entries SHOULD +be sorted by epoch in ascending order. The blob schedule MAY be empty. + +*Note*: The blob schedule is to be determined. + + + +| Epoch | Max Blobs Per Block | Description | +| ----- | ------------------- | ----------- | + ## Beacon chain state transition function ### Block processing @@ -53,7 +74,10 @@ def process_execution_payload( # Verify timestamp assert payload.timestamp == compute_timestamp_at_slot(state, state.slot) # [Modified in Fulu:EIP7892] Verify commitments are under limit - assert len(body.blob_kzg_commitments) <= get_max_blobs_per_block(get_current_epoch(state)) + assert ( + len(body.blob_kzg_commitments) + <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block + ) # Verify the execution payload is valid versioned_hashes = [ kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments @@ -151,6 +175,64 @@ class BeaconState(Container): ### Misc +#### New `BlobParameters` + +```python +@dataclass +class BlobParameters: + epoch: Epoch + max_blobs_per_block: uint64 +``` + +#### New `get_blob_parameters` + +```python +def get_blob_parameters(epoch: Epoch) -> BlobParameters: + """ + Return the blob parameters at a given epoch. + """ + for entry in sorted(BLOB_SCHEDULE, key=lambda e: e["EPOCH"], reverse=True): + if epoch >= entry["EPOCH"]: + return BlobParameters(entry["EPOCH"], entry["MAX_BLOBS_PER_BLOCK"]) + return BlobParameters(ELECTRA_FORK_EPOCH, MAX_BLOBS_PER_BLOCK_ELECTRA) +``` + +#### Modified `compute_fork_digest` + +*Note:* The `compute_fork_digest` helper is updated to account for +Blob-Parameters-Only forks. Also, the `fork_version` parameter has been removed +and is now computed for the given epoch with `compute_fork_version`. + +```python +def compute_fork_digest( + genesis_validators_root: Root, + epoch: Epoch, # [New in Fulu:EIP7892] +) -> ForkDigest: + """ + Return the 4-byte fork digest for the ``version`` and ``genesis_validators_root`` + XOR'd with the hash of the blob parameters for ``epoch``. + + This is a digest primarily used for domain separation on the p2p layer. + 4-bytes suffices for practical separation of forks/chains. + """ + fork_version = compute_fork_version(epoch) + base_digest = compute_fork_data_root(fork_version, genesis_validators_root) + blob_parameters = get_blob_parameters(epoch) + + # Bitmask digest with hash of blob parameters + return ForkDigest( + bytes( + xor( + base_digest, + hash( + uint_to_bytes(uint64(blob_parameters.epoch)) + + uint_to_bytes(uint64(blob_parameters.max_blobs_per_block)) + ), + ) + )[:4] + ) +``` + #### New `compute_proposer_indices` ```python diff --git a/specs/fulu/das-core.md b/specs/fulu/das-core.md index c534b70aff..d85b55ea96 100644 --- a/specs/fulu/das-core.md +++ b/specs/fulu/das-core.md @@ -10,13 +10,11 @@ - [Configuration](#configuration) - [Data size](#data-size) - [Custody setting](#custody-setting) - - [Blob schedule](#blob-schedule) - [Containers](#containers) - [`DataColumnSidecar`](#datacolumnsidecar) - [`MatrixEntry`](#matrixentry) - [Helper functions](#helper-functions) - [`get_custody_groups`](#get_custody_groups) - - [`get_max_blobs_per_block`](#get_max_blobs_per_block) - [`compute_columns_for_custody_group`](#compute_columns_for_custody_group) - [`compute_matrix`](#compute_matrix) - [`recover_matrix`](#recover_matrix) @@ -70,23 +68,6 @@ specification. | `NUMBER_OF_CUSTODY_GROUPS` | `128` | Number of custody groups available for nodes to custody | | `CUSTODY_REQUIREMENT` | `4` | Minimum number of custody groups an honest node custodies and serves samples from | -### Blob schedule - -*[New in EIP7892]* This schedule defines the maximum blobs per block limit for a -given epoch. - -There MUST NOT exist multiple blob schedule entries with the same epoch value. -The maximum blobs per block limit for blob schedules entries MUST be less than -or equal to `MAX_BLOB_COMMITMENTS_PER_BLOCK`. The blob schedule entries SHOULD -be sorted by epoch in ascending order. The blob schedule MAY be empty. - -*Note*: The blob schedule is to be determined. - - - -| Epoch | Max Blobs Per Block | Description | -| ----- | ------------------- | ----------- | - ### Containers #### `DataColumnSidecar` @@ -137,19 +118,6 @@ def get_custody_groups(node_id: NodeID, custody_group_count: uint64) -> Sequence return sorted(custody_groups) ``` -### `get_max_blobs_per_block` - -```python -def get_max_blobs_per_block(epoch: Epoch) -> uint64: - """ - Return the maximum number of blobs that can be included in a block for a given epoch. - """ - for entry in sorted(BLOB_SCHEDULE, key=lambda e: e["EPOCH"], reverse=True): - if epoch >= entry["EPOCH"]: - return entry["MAX_BLOBS_PER_BLOCK"] - return MAX_BLOBS_PER_BLOCK_ELECTRA -``` - ### `compute_columns_for_custody_group` ```python diff --git a/specs/fulu/p2p-interface.md b/specs/fulu/p2p-interface.md index 0582b31e5f..59e9160b13 100644 --- a/specs/fulu/p2p-interface.md +++ b/specs/fulu/p2p-interface.md @@ -34,6 +34,7 @@ - [The discovery domain: discv5](#the-discovery-domain-discv5) - [ENR structure](#enr-structure) - [Custody group count](#custody-group-count) + - [Next fork digest](#next-fork-digest) @@ -304,13 +305,13 @@ During the deprecation transition period: **Protocol ID:** `/eth2/beacon_chain/req/data_column_sidecars_by_range/1/` The `` field is calculated as -`context = compute_fork_digest(fork_version, genesis_validators_root)`: +`context = compute_fork_digest(genesis_validators_root, epoch)`: -| `fork_version` | Chunk SSZ type | -| ------------------- | ------------------------ | -| `FULU_FORK_VERSION` | `fulu.DataColumnSidecar` | +| `epoch` | Chunk SSZ type | +| -------------------- | ------------------------ | +| >= `FULU_FORK_EPOCH` | `fulu.DataColumnSidecar` | Request Content: @@ -409,13 +410,13 @@ the request. *[New in Fulu:EIP7594]* The `` field is calculated as -`context = compute_fork_digest(fork_version, genesis_validators_root)`: +`context = compute_fork_digest(genesis_validators_root, epoch)`: -| `fork_version` | Chunk SSZ type | -| ------------------- | ------------------------ | -| `FULU_FORK_VERSION` | `fulu.DataColumnSidecar` | +| `epoch` | Chunk SSZ type | +| -------------------- | ------------------------ | +| >= `FULU_FORK_EPOCH` | `fulu.DataColumnSidecar` | Request Content: @@ -494,3 +495,29 @@ column discovery. | Key | Value | | ----- | ----------------------------------------------------------------------------------------------------------------- | | `cgc` | Custody group count, `uint64` big endian integer with no leading zero bytes (`0` is encoded as empty byte string) | + +##### Next fork digest + +A new entry is added to the ENR under the key `nfd`, short for _next fork +digest_. This entry communicates the digest of the next scheduled fork, +regardless of whether it is a regular or a Blob-Parameters-Only fork. + +If no next fork is scheduled, the `nfd` entry contains the default value for the +type (i.e., the SSZ representation of a zero-filled array). + +| Key | Value | +| :---- | :---------------------- | +| `nfd` | SSZ Bytes4 `ForkDigest` | + +Furthermore, the existing `next_fork_epoch` field under the `eth2` entry MUST be +set to the epoch of the next fork, whether a regular fork, _or a BPO fork_. + +When discovering and interfacing with peers, nodes MUST evaluate `nfd` alongside +their existing consideration of the `ENRForkID::next_*` fields under the `eth2` +key, to form a more accurate view of the peer's intended next fork for the +purposes of sustained peering. A mismatch indicates that the node MUST +disconnect from such peers at the fork boundary, but not sooner. + +Nodes unprepared to follow the Fulu fork will be unaware of `nfd` entries. +However, their existing comparison of `eth2` entries (concretely +`next_fork_epoch`) is sufficient to detect upcoming divergence. diff --git a/tests/core/pyspec/README.md b/tests/core/pyspec/README.md index 1b20176a42..45e6878f39 100644 --- a/tests/core/pyspec/README.md +++ b/tests/core/pyspec/README.md @@ -33,7 +33,7 @@ Or, to run a specific test function specify `k=`: make test k=test_verify_kzg_proof ``` -Or, to run a specific test function under a single fork specify `k=`: +Or, to run all tests under a single fork specify `fork=`: ```shell make test fork=phase0 diff --git a/tests/core/pyspec/eth2spec/test/fulu/merkle_proof/test_single_merkle_proof.py b/tests/core/pyspec/eth2spec/test/fulu/merkle_proof/test_single_merkle_proof.py index c504427e65..966b57ac00 100644 --- a/tests/core/pyspec/eth2spec/test/fulu/merkle_proof/test_single_merkle_proof.py +++ b/tests/core/pyspec/eth2spec/test/fulu/merkle_proof/test_single_merkle_proof.py @@ -85,7 +85,7 @@ def test_blob_kzg_commitments_merkle_proof__random_block_1(spec, state): @with_fulu_and_later @spec_state_test def test_blob_kzg_commitments_merkle_proof__multiple_blobs(spec, state): - blob_count = spec.get_max_blobs_per_block(spec.get_current_epoch(state)) // 2 + blob_count = spec.get_blob_parameters(spec.get_current_epoch(state)).max_blobs_per_block // 2 rng = random.Random(2222) yield from _run_blob_kzg_commitments_merkle_proof_test( spec, state, rng=rng, blob_count=blob_count @@ -96,7 +96,7 @@ def test_blob_kzg_commitments_merkle_proof__multiple_blobs(spec, state): @with_fulu_and_later @spec_state_test def test_blob_kzg_commitments_merkle_proof__max_blobs(spec, state): - max_blobs = spec.get_max_blobs_per_block(spec.get_current_epoch(state)) + max_blobs = spec.get_blob_parameters(spec.get_current_epoch(state)).max_blobs_per_block rng = random.Random(3333) yield from _run_blob_kzg_commitments_merkle_proof_test( spec, state, rng=rng, blob_count=max_blobs diff --git a/tests/core/pyspec/eth2spec/test/fulu/validator/__init__.py b/tests/core/pyspec/eth2spec/test/fulu/validator/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/pyspec/eth2spec/test/fulu/validator/test_compute_fork_digest.py b/tests/core/pyspec/eth2spec/test/fulu/validator/test_compute_fork_digest.py new file mode 100644 index 0000000000..11f81e93b4 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/fulu/validator/test_compute_fork_digest.py @@ -0,0 +1,137 @@ +from eth2spec.test.context import ( + single_phase, + spec_test, + with_config_overrides, + with_fulu_and_later, +) + + +@with_fulu_and_later +@spec_test +@single_phase +@with_config_overrides( + { + "ELECTRA_FORK_EPOCH": 9, + "FULU_FORK_EPOCH": 100, + "BLOB_SCHEDULE": [ + {"EPOCH": 9, "MAX_BLOBS_PER_BLOCK": 9}, + {"EPOCH": 100, "MAX_BLOBS_PER_BLOCK": 100}, + {"EPOCH": 150, "MAX_BLOBS_PER_BLOCK": 175}, + {"EPOCH": 200, "MAX_BLOBS_PER_BLOCK": 200}, + {"EPOCH": 250, "MAX_BLOBS_PER_BLOCK": 275}, + {"EPOCH": 300, "MAX_BLOBS_PER_BLOCK": 300}, + ], + }, + emit=False, +) +def test_compute_fork_digest(spec): + test_cases = [ + # Different epochs and blob limits: + { + "epoch": 9, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x39f8e7c3", + }, + { + "epoch": 10, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x39f8e7c3", + }, + { + "epoch": 11, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x39f8e7c3", + }, + { + "epoch": 99, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x39f8e7c3", + }, + { + "epoch": 100, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x44a571e8", + }, + { + "epoch": 101, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x44a571e8", + }, + { + "epoch": 150, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x1171afca", + }, + { + "epoch": 199, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x1171afca", + }, + { + "epoch": 200, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x427a30ab", + }, + { + "epoch": 201, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x427a30ab", + }, + { + "epoch": 250, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0xd5310ef1", + }, + { + "epoch": 299, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0xd5310ef1", + }, + { + "epoch": 300, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x51d229f7", + }, + { + "epoch": 301, + "genesis_validators_root": b"\x00" * 32, + "expected_fork_digest": "0x51d229f7", + }, + # Different genesis validators roots: + { + "epoch": 9, + "genesis_validators_root": b"\x01" * 32, + "expected_fork_digest": "0xe41615ba", + }, + { + "epoch": 9, + "genesis_validators_root": b"\x02" * 32, + "expected_fork_digest": "0x46790ef9", + }, + { + "epoch": 9, + "genesis_validators_root": b"\x03" * 32, + "expected_fork_digest": "0xa072c2f5", + }, + { + "epoch": 100, + "genesis_validators_root": b"\x01" * 32, + "expected_fork_digest": "0xbfe98545", + }, + { + "epoch": 100, + "genesis_validators_root": b"\x02" * 32, + "expected_fork_digest": "0x9b7e4788", + }, + { + "epoch": 100, + "genesis_validators_root": b"\x03" * 32, + "expected_fork_digest": "0x8b5ce4af", + }, + ] + + for case in test_cases: + # Compute the fork digest given the inputs from the test case + fork_digest = spec.compute_fork_digest(case["genesis_validators_root"], case["epoch"]) + # Check that the computed fork digest matches our expected value + assert f"0x{fork_digest.hex()}" == case["expected_fork_digest"] diff --git a/tests/core/pyspec/eth2spec/test/helpers/blob.py b/tests/core/pyspec/eth2spec/test/helpers/blob.py index af49744ece..2cb37af643 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/blob.py +++ b/tests/core/pyspec/eth2spec/test/helpers/blob.py @@ -121,7 +121,7 @@ def get_sample_blob_tx(spec, blob_count=1, rng=random.Random(5566), is_valid_blo def get_max_blob_count(spec, state): if is_post_fulu(spec): - return spec.get_max_blobs_per_block(spec.get_current_epoch(state)) + return spec.get_blob_parameters(spec.get_current_epoch(state)).max_blobs_per_block elif is_post_electra(spec): return spec.config.MAX_BLOBS_PER_BLOCK_ELECTRA else: diff --git a/tests/core/pyspec/eth2spec/test/phase0/unittests/validator/test_validator_unittest.py b/tests/core/pyspec/eth2spec/test/phase0/unittests/validator/test_validator_unittest.py index c40d823aed..97b175a04f 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/unittests/validator/test_validator_unittest.py +++ b/tests/core/pyspec/eth2spec/test/phase0/unittests/validator/test_validator_unittest.py @@ -6,11 +6,12 @@ spec_state_test, spec_test, with_all_phases, + with_all_phases_from_to, with_phases, ) from eth2spec.test.helpers.attestations import build_attestation_data, get_valid_attestation from eth2spec.test.helpers.block import build_empty_block -from eth2spec.test.helpers.constants import PHASE0 +from eth2spec.test.helpers.constants import FULU, PHASE0 from eth2spec.test.helpers.deposits import prepare_state_and_deposit from eth2spec.test.helpers.keys import privkeys, pubkeys from eth2spec.test.helpers.state import next_epoch @@ -336,7 +337,7 @@ def test_get_block_signature(spec, state): ) -@with_all_phases +@with_all_phases_from_to(from_phase=PHASE0, to_phase=FULU) @spec_state_test def test_compute_fork_digest(spec, state): actual_fork_digest = spec.compute_fork_digest(