diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index 1092ec507d..24788459f2 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -68,6 +68,9 @@ EIP7549_FORK_EPOCH: 18446744073709551615 # WHISK WHISK_FORK_VERSION: 0x08000000 # temporary stub WHISK_FORK_EPOCH: 18446744073709551615 +# EIP7594 +EIP7594_FORK_VERSION: 0x06000001 +EIP7594_FORK_EPOCH: 18446744073709551615 # Time parameters @@ -165,10 +168,10 @@ WHISK_EPOCHS_PER_SHUFFLING_PHASE: 256 WHISK_PROPOSER_SELECTION_GAP: 2 # EIP7594 -EIP7594_FORK_VERSION: 0x06000001 -EIP7594_FORK_EPOCH: 18446744073709551615 +NUMBER_OF_COLUMNS: 128 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 # [New in EIP7251] MIN_PER_EPOCH_CHURN_LIMIT_EIP7251: 128000000000 # 2**7 * 10**9 (= 128,000,000,000) MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000 # 2**8 * 10**9 (= 256,000,000,000) - diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 07f0da4e22..984bb253f5 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -67,7 +67,9 @@ EIP7549_FORK_EPOCH: 18446744073709551615 # WHISK WHISK_FORK_VERSION: 0x08000001 WHISK_FORK_EPOCH: 18446744073709551615 - +# EIP7594 +EIP7594_FORK_VERSION: 0x06000001 +EIP7594_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- @@ -164,8 +166,9 @@ WHISK_EPOCHS_PER_SHUFFLING_PHASE: 4 WHISK_PROPOSER_SELECTION_GAP: 1 # EIP7594 -EIP7594_FORK_VERSION: 0x06000001 -EIP7594_FORK_EPOCH: 18446744073709551615 +NUMBER_OF_COLUMNS: 128 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 # [New in EIP7251] MIN_PER_EPOCH_CHURN_LIMIT_EIP7251: 64000000000 # 2**6 * 10**9 (= 64,000,000,000) diff --git a/presets/mainnet/eip7594.yaml b/presets/mainnet/eip7594.yaml index 6bddce11c8..813febf26d 100644 --- a/presets/mainnet/eip7594.yaml +++ b/presets/mainnet/eip7594.yaml @@ -6,3 +6,5 @@ FIELD_ELEMENTS_PER_CELL: 64 # `uint64(2 * 4096)` (= 8192) FIELD_ELEMENTS_PER_EXT_BLOB: 8192 +# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) +KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 diff --git a/presets/minimal/eip7594.yaml b/presets/minimal/eip7594.yaml index 0abcad3b27..847719a421 100644 --- a/presets/minimal/eip7594.yaml +++ b/presets/minimal/eip7594.yaml @@ -6,3 +6,5 @@ FIELD_ELEMENTS_PER_CELL: 64 # `uint64(2 * 4096)` (= 8192) FIELD_ELEMENTS_PER_EXT_BLOB: 8192 +# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) +KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 diff --git a/pysetup/helpers.py b/pysetup/helpers.py index 77743c8fe3..49c0fcafcb 100644 --- a/pysetup/helpers.py +++ b/pysetup/helpers.py @@ -208,8 +208,7 @@ def dependency_order_class_objects(objects: Dict[str, str], custom_types: Dict[s for item in [dep, key] + key_list[key_list.index(dep)+1:]: objects[item] = objects.pop(item) - -def combine_ssz_objects(old_objects: Dict[str, str], new_objects: Dict[str, str], custom_types) -> Dict[str, str]: +def combine_ssz_objects(old_objects: Dict[str, str], new_objects: Dict[str, str]) -> Dict[str, str]: """ Takes in old spec and new spec ssz objects, combines them, and returns the newer versions of the objects in dependency order. @@ -231,7 +230,7 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject: config_vars = combine_dicts(spec0.config_vars, spec1.config_vars) ssz_dep_constants = combine_dicts(spec0.ssz_dep_constants, spec1.ssz_dep_constants) func_dep_presets = combine_dicts(spec0.func_dep_presets, spec1.func_dep_presets) - ssz_objects = combine_ssz_objects(spec0.ssz_objects, spec1.ssz_objects, custom_types) + ssz_objects = combine_ssz_objects(spec0.ssz_objects, spec1.ssz_objects) dataclasses = combine_dicts(spec0.dataclasses, spec1.dataclasses) return SpecObject( functions=functions, diff --git a/pysetup/md_doc_paths.py b/pysetup/md_doc_paths.py index f79b5f642d..f60f2fb90d 100644 --- a/pysetup/md_doc_paths.py +++ b/pysetup/md_doc_paths.py @@ -39,6 +39,11 @@ BELLATRIX: "sync/optimistic.md" } +DEFAULT_ORDER = ( + "beacon-chain", + "polynomial-commitments", +) + def is_post_fork(a, b) -> bool: """ @@ -66,15 +71,25 @@ def get_fork_directory(fork): raise FileNotFoundError(f"No directory found for fork: {fork}") +def sort_key(s): + for index, key in enumerate(DEFAULT_ORDER): + if key in s: + return (index, s) + return (len(DEFAULT_ORDER), s) + + def get_md_doc_paths(spec_fork: str) -> str: md_doc_paths = "" for fork in ALL_FORKS: if is_post_fork(spec_fork, fork): # Append all files in fork directory recursively - for root, dirs, files in os.walk(get_fork_directory(fork)): + for root, _, files in os.walk(get_fork_directory(fork)): + filepaths = [] for filename in files: filepath = os.path.join(root, filename) + filepaths.append(filepath) + for filepath in sorted(filepaths, key=sort_key): if filepath.endswith('.md') and filepath not in IGNORE_SPEC_FILES: md_doc_paths += filepath + "\n" # Append extra files if any diff --git a/pysetup/spec_builders/eip7594.py b/pysetup/spec_builders/eip7594.py index 6f145affb4..414d73f327 100644 --- a/pysetup/spec_builders/eip7594.py +++ b/pysetup/spec_builders/eip7594.py @@ -14,8 +14,15 @@ def imports(cls, preset_name: str): ''' @classmethod - def hardcoded_custom_type_dep_constants(cls, spec_object) -> Dict[str, str]: + def hardcoded_custom_type_dep_constants(cls, spec_object) -> str: return { 'FIELD_ELEMENTS_PER_CELL': spec_object.preset_vars['FIELD_ELEMENTS_PER_CELL'].value, 'FIELD_ELEMENTS_PER_EXT_BLOB': spec_object.preset_vars['FIELD_ELEMENTS_PER_EXT_BLOB'].value, + 'NUMBER_OF_COLUMNS': spec_object.config_vars['NUMBER_OF_COLUMNS'].value, + } + + @classmethod + def hardcoded_func_dep_presets(cls, spec_object) -> Dict[str, str]: + return { + 'KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH': spec_object.preset_vars['KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH'].value, } diff --git a/specs/_features/eip7594/das-core.md b/specs/_features/eip7594/das-core.md new file mode 100644 index 0000000000..83d47e1856 --- /dev/null +++ b/specs/_features/eip7594/das-core.md @@ -0,0 +1,285 @@ +# EIP-7594 -- Data Availability Sampling Core + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Custom types](#custom-types) +- [Configuration](#configuration) + - [Data size](#data-size) + - [Networking](#networking) + - [Custody setting](#custody-setting) + - [Containers](#containers) + - [`DataColumnSidecar`](#datacolumnsidecar) + - [Helper functions](#helper-functions) + - [`get_custody_columns`](#get_custody_columns) + - [`compute_extended_matrix`](#compute_extended_matrix) + - [`recover_matrix`](#recover_matrix) + - [`get_data_column_sidecars`](#get_data_column_sidecars) +- [Custody](#custody) + - [Custody requirement](#custody-requirement) + - [Public, deterministic selection](#public-deterministic-selection) +- [Peer discovery](#peer-discovery) +- [Extended data](#extended-data) +- [Column gossip](#column-gossip) + - [Parameters](#parameters) +- [Peer sampling](#peer-sampling) +- [Peer scoring](#peer-scoring) +- [Reconstruction and cross-seeding](#reconstruction-and-cross-seeding) +- [DAS providers](#das-providers) +- [A note on fork choice](#a-note-on-fork-choice) +- [FAQs](#faqs) + - [Row (blob) custody](#row-blob-custody) + - [Subnet stability](#subnet-stability) + + + + +## Custom types + +We define the following Python custom types for type hinting and readability: + +| Name | SSZ equivalent | Description | +| - | - | - | +| `DataColumn` | `List[Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK]` | The data of each column in EIP-7594 | +| `ExtendedMatrix` | `List[Cell, MAX_BLOBS_PER_BLOCK * NUMBER_OF_COLUMNS]` | The full data of one-dimensional erasure coding extended blobs (in row major format) | + +## Configuration + +### Data size + +| Name | Value | Description | +| - | - | - | +| `NUMBER_OF_COLUMNS` | `uint64((FIELD_ELEMENTS_PER_BLOB * 2) // FIELD_ELEMENTS_PER_CELL)` (= 128) | Number of columns in the extended data matrix. | + +### Networking + +| Name | Value | Description | +| - | - | - | +| `DATA_COLUMN_SIDECAR_SUBNET_COUNT` | `32` | The number of data column sidecar subnets used in the gossipsub protocol | + +### Custody setting + +| Name | Value | Description | +| - | - | - | +| `SAMPLES_PER_SLOT` | `8` | Number of `DataColumn` random samples a node queries per slot | +| `CUSTODY_REQUIREMENT` | `1` | Minimum number of subnets an honest node custodies and serves samples from | +| `TARGET_NUMBER_OF_PEERS` | `70` | Suggested minimum peer count | + +### Containers + +#### `DataColumnSidecar` + +```python +class DataColumnSidecar(Container): + index: ColumnIndex # Index of column in extended matrix + column: DataColumn + kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] + signed_block_header: SignedBeaconBlockHeader + kzg_commitments_inclusion_proof: Vector[Bytes32, KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH] +``` + +### Helper functions + +#### `get_custody_columns` + +```python +def get_custody_columns(node_id: NodeID, custody_subnet_count: uint64) -> Sequence[ColumnIndex]: + assert custody_subnet_count <= DATA_COLUMN_SIDECAR_SUBNET_COUNT + + subnet_ids = [] + i = 0 + while len(subnet_ids) < custody_subnet_count: + subnet_id = ( + bytes_to_uint64(hash(uint_to_bytes(uint64(node_id + i)))[0:8]) + % DATA_COLUMN_SIDECAR_SUBNET_COUNT + ) + if subnet_id not in subnet_ids: + subnet_ids.append(subnet_id) + i += 1 + assert len(subnet_ids) == len(set(subnet_ids)) + + columns_per_subnet = NUMBER_OF_COLUMNS // DATA_COLUMN_SIDECAR_SUBNET_COUNT + return [ + ColumnIndex(DATA_COLUMN_SIDECAR_SUBNET_COUNT * i + subnet_id) + for i in range(columns_per_subnet) + for subnet_id in subnet_ids + ] +``` + +#### `compute_extended_matrix` + +```python +def compute_extended_matrix(blobs: Sequence[Blob]) -> ExtendedMatrix: + """ + Return the full ``ExtendedMatrix``. + + This helper demonstrates the relationship between blobs and ``ExtendedMatrix``. + The data structure for storing cells is implementation-dependent. + """ + extended_matrix = [] + for blob in blobs: + extended_matrix.extend(compute_cells(blob)) + return ExtendedMatrix(extended_matrix) +``` + +#### `recover_matrix` + +```python +def recover_matrix(cells_dict: Dict[Tuple[BlobIndex, CellID], Cell], blob_count: uint64) -> ExtendedMatrix: + """ + Return the recovered ``ExtendedMatrix``. + + This helper demonstrates how to apply ``recover_polynomial``. + The data structure for storing cells is implementation-dependent. + """ + extended_matrix = [] + for blob_index in range(blob_count): + cell_ids = [cell_id for b_index, cell_id in cells_dict.keys() if b_index == blob_index] + cells = [cells_dict[(blob_index, cell_id)] for cell_id in cell_ids] + cells_bytes = [[bls_field_to_bytes(element) for element in cell] for cell in cells] + + full_polynomial = recover_polynomial(cell_ids, cells_bytes) + cells_from_full_polynomial = [ + full_polynomial[i * FIELD_ELEMENTS_PER_CELL:(i + 1) * FIELD_ELEMENTS_PER_CELL] + for i in range(CELLS_PER_BLOB) + ] + extended_matrix.extend(cells_from_full_polynomial) + return ExtendedMatrix(extended_matrix) +``` + +#### `get_data_column_sidecars` + +```python +def get_data_column_sidecars(signed_block: SignedBeaconBlock, + blobs: Sequence[Blob]) -> Sequence[DataColumnSidecar]: + signed_block_header = compute_signed_block_header(signed_block) + block = signed_block.message + kzg_commitments_inclusion_proof = compute_merkle_proof( + block.body, + get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments'), + ) + cells_and_proofs = [compute_cells_and_proofs(blob) for blob in blobs] + blob_count = len(blobs) + cells = [cells_and_proofs[i][0] for i in range(blob_count)] + proofs = [cells_and_proofs[i][1] for i in range(blob_count)] + sidecars = [] + for column_index in range(NUMBER_OF_COLUMNS): + column = DataColumn([cells[row_index][column_index] + for row_index in range(blob_count)]) + kzg_proof_of_column = [proofs[row_index][column_index] + for row_index in range(blob_count)] + sidecars.append(DataColumnSidecar( + index=column_index, + column=column, + kzg_commitments=block.body.blob_kzg_commitments, + kzg_proofs=kzg_proof_of_column, + signed_block_header=signed_block_header, + kzg_commitments_inclusion_proof=kzg_commitments_inclusion_proof, + )) + return sidecars +``` + +## Custody + +### Custody requirement + +Each node downloads and custodies a minimum of `CUSTODY_REQUIREMENT` subnets per slot. The particular subnets that the node is required to custody are selected pseudo-randomly (more on this below). + +A node *may* choose to custody and serve more than the minimum honesty requirement. Such a node explicitly advertises a number greater than `CUSTODY_REQUIREMENT` via the peer discovery mechanism -- for example, in their ENR (e.g. `custody_subnet_count: 4` if the node custodies `4` subnets each slot) -- up to a `DATA_COLUMN_SIDECAR_SUBNET_COUNT` (i.e. a super-full node). + +A node stores the custodied columns for the duration of the pruning period and responds to peer requests for samples on those columns. + +### Public, deterministic selection + +The particular columns that a node custodies are selected pseudo-randomly as a function (`get_custody_columns`) of the node-id and custody size -- importantly this function can be run by any party as the inputs are all public. + +*Note*: increasing the `custody_size` parameter for a given `node_id` extends the returned list (rather than being an entirely new shuffle) such that if `custody_size` is unknown, the default `CUSTODY_REQUIREMENT` will be correct for a subset of the node's custody. + +## Peer discovery + +At each slot, a node needs to be able to readily sample from *any* set of columns. To this end, a node SHOULD find and maintain a set of diverse and reliable peers that can regularly satisfy their sampling demands. + +A node runs a background peer discovery process, maintaining at least `TARGET_NUMBER_OF_PEERS` of various custody distributions (both `custody_size` and column assignments). The combination of advertised `custody_size` size and public node-id make this readily and publicly accessible. + +`TARGET_NUMBER_OF_PEERS` should be tuned upward in the event of failed sampling. + +*Note*: while high-capacity and super-full nodes are high value with respect to satisfying sampling requirements, a node SHOULD maintain a distribution across node capacities as to not centralize the p2p graph too much (in the extreme becomes hub/spoke) and to distribute sampling load better across all nodes. + +*Note*: A DHT-based peer discovery mechanism is expected to be utilized in the above. The beacon-chain network currently utilizes discv5 in a similar method as described for finding peers of particular distributions of attestation subnets. Additional peer discovery methods are valuable to integrate (e.g., latent peer discovery via libp2p gossipsub) to add a defense in breadth against one of the discovery methods being attacked. + +## Extended data + +In this construction, we extend the blobs using a one-dimensional erasure coding extension. The matrix comprises maximum `MAX_BLOBS_PER_BLOCK` rows and fixed `NUMBER_OF_COLUMNS` columns, with each row containing a `Blob` and its corresponding extension. `compute_extended_matrix` demonstrates the relationship between blobs and custom type `ExtendedMatrix`. + +## Column gossip + +### Parameters + +For each column -- use `data_column_sidecar_{subnet_id}` subnets, where `subnet_id` can be computed with the `compute_subnet_for_data_column_sidecar(column_index: ColumnIndex)` helper. The sidecars can be computed with the `get_data_column_sidecars(signed_block: SignedBeaconBlock, blobs: Sequence[Blob])` helper. + +To custody a particular column, a node joins the respective gossip subnet. Verifiable samples from their respective column are gossiped on the assigned subnet. + +## Peer sampling + +A node SHOULD maintain a diverse set of peers for each column and each slot by verifying responsiveness to sample queries. At each slot, a node makes `SAMPLES_PER_SLOT` queries for samples from their peers via `DataColumnSidecarsByRoot` request. A node utilizes `get_custody_columns` helper to determine which peer(s) to request from. If a node has enough good/honest peers across all rows and columns, this has a high chance of success. + +## Peer scoring + +Due to the deterministic custody functions, a node knows exactly what a peer should be able to respond to. In the event that a peer does not respond to samples of their custodied rows/columns, a node may downscore or disconnect from a peer. + +## Reconstruction and cross-seeding + +If the node obtains 50%+ of all the columns, they can reconstruct the full data matrix via `recover_matrix` helper. + +If a node fails to sample a peer or fails to get a column on the column subnet, a node can utilize the Req/Resp message to query the missing column from other peers. + +Once the node obtain the column, the node SHOULD send the missing columns to the column subnets. + +*Note*: A node always maintains a matrix view of the rows and columns they are following, able to cross-reference and cross-seed in either direction. + +*Note*: There are timing considerations to analyze -- at what point does a node consider samples missing and choose to reconstruct and cross-seed. + +*Note*: There may be anti-DoS and quality-of-service considerations around how to send samples and consider samples -- is each individual sample a message or are they sent in aggregate forms. + +## DAS providers + +A DAS provider is a consistently-available-for-DAS-queries, super-full (or high capacity) node. To the p2p, these look just like other nodes but with high advertised capacity, and they should generally be able to be latently found via normal discovery. + +DAS providers can also be found out-of-band and configured into a node to connect to directly and prioritize. Nodes can add some set of these to their local configuration for persistent connection to bolster their DAS quality of service. + +Such direct peering utilizes a feature supported out of the box today on all nodes and can complement (and reduce attackability and increase quality-of-service) alternative peer discovery mechanisms. + +## A note on fork choice + +*Fork choice spec TBD, but it will just be a replacement of `is_data_available()` call in Deneb with column sampling instead of full download. Note the `is_data_available(slot_N)` will likely do a `-1` follow distance so that you just need to check the availability of slot `N-1` for slot `N` (starting with the block proposer of `N`).* + +The fork choice rule (essentially a DA filter) is *orthogonal to a given DAS design*, other than the efficiency of a particular design impacting it. + +In any DAS design, there are probably a few degrees of freedom around timing, acceptability of short-term re-orgs, etc. + +For example, the fork choice rule might require validators to do successful DAS on slot `N` to be able to include block of slot `N` in its fork choice. That's the tightest DA filter. But trailing filters are also probably acceptable, knowing that there might be some failures/short re-orgs but that they don't hurt the aggregate security. For example, the rule could be — DAS must be completed for slot N-1 for a child block in N to be included in the fork choice. + +Such trailing techniques and their analysis will be valuable for any DAS construction. The question is — can you relax how quickly you need to do DA and in the worst case not confirm unavailable data via attestations/finality, and what impact does it have on short-term re-orgs and fast confirmation rules. + +## FAQs + +### Row (blob) custody + +In the one-dimension construction, a node samples the peers by requesting the whole `DataColumn`. In reconstruction, a node can reconstruct all the blobs by 50% of the columns. Note that nodes can still download the row via `blob_sidecar_{subnet_id}` subnets. + +The potential benefits of having row custody could include: + +1. Allow for more "natural" distribution of data to consumers -- e.g., roll-ups -- but honestly, they won't know a priori which row their blob is going to be included in in the block, so they would either need to listen to all rows or download a particular row after seeing the block. The former looks just like listening to column [0, N) and the latter is req/resp instead of gossiping. +2. Help with some sort of distributed reconstruction. Those with full rows can compute extensions and seed missing samples to the network. This would either need to be able to send individual points on the gossip or would need some sort of req/resp faculty, potentially similar to an `IHAVEPOINTBITFIELD` and `IWANTSAMPLE`. + +However, for simplicity, we don't assign row custody assignments to nodes in the current design. + +### Subnet stability + +To start with a simple, stable backbone, for now, we don't shuffle the subnet assignments via the deterministic custody selection helper `get_custody_columns`. However, staggered rotation likely needs to happen on the order of the pruning period to ensure subnets can be utilized for recovery. For example, introducing an `epoch` argument allows the function to maintain stability over many epochs. diff --git a/specs/_features/eip7594/p2p-interface.md b/specs/_features/eip7594/p2p-interface.md new file mode 100644 index 0000000000..b282abbe49 --- /dev/null +++ b/specs/_features/eip7594/p2p-interface.md @@ -0,0 +1,211 @@ +# EIP-7594 -- Networking + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Modifications in EIP-7594](#modifications-in-eip-7594) + - [Preset](#preset) + - [Configuration](#configuration) + - [Containers](#containers) + - [`DataColumnIdentifier`](#datacolumnidentifier) + - [Helpers](#helpers) + - [`verify_data_column_sidecar_kzg_proofs`](#verify_data_column_sidecar_kzg_proofs) + - [`verify_data_column_sidecar_inclusion_proof`](#verify_data_column_sidecar_inclusion_proof) + - [`compute_subnet_for_data_column_sidecar`](#compute_subnet_for_data_column_sidecar) + - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) + - [Topics and messages](#topics-and-messages) + - [Blob subnets](#blob-subnets) + - [Deprecated `blob_sidecar_{subnet_id}`](#deprecated-blob_sidecar_subnet_id) + - [`data_column_sidecar_{subnet_id}`](#data_column_sidecar_subnet_id) + - [The Req/Resp domain](#the-reqresp-domain) + - [Messages](#messages) + - [DataColumnSidecarsByRoot v1](#datacolumnsidecarsbyroot-v1) + - [The discovery domain: discv5](#the-discovery-domain-discv5) + - [ENR structure](#enr-structure) + - [`custody_subnet_count`](#custody_subnet_count) + + + + +## Modifications in EIP-7594 + +### Preset + +| Name | Value | Description | +| - | - | - | +| `KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH` | `uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')))` (= 4) | Merkle proof index for `blob_kzg_commitments` | + + +### Configuration + +*[New in Deneb:EIP4844]* + +| Name | Value | Description | +|------------------------------------------|-----------------------------------|---------------------------------------------------------------------| +| `MAX_REQUEST_DATA_COLUMN_SIDECARS` | `MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS` | Maximum number of data column sidecars in a single request | +| `MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS` | `2**12` (= 4096 epochs, ~18 days) | The minimum epoch range over which a node must serve data column sidecars | + +### Containers + +#### `DataColumnIdentifier` + +```python +class DataColumnIdentifier(Container): + block_root: Root + index: ColumnIndex +``` + +### Helpers + +##### `verify_data_column_sidecar_kzg_proofs` + +```python +def verify_data_column_sidecar_kzg_proofs(sidecar: DataColumnSidecar) -> bool: + """ + Verify if the proofs are correct + """ + assert sidecar.index < NUMBER_OF_COLUMNS + assert len(sidecar.column) == len(sidecar.kzg_commitments) == len(sidecar.kzg_proofs) + row_ids = [RowIndex(i) for i in range(len(sidecar.column))] + + # KZG batch verifies that the cells match the corresponding commitments and proofs + return verify_cell_proof_batch( + row_commitments=sidecar.kzg_commitments, + row_indices=row_ids, # all rows + column_indices=[sidecar.index], + cells=sidecar.column, + proofs=sidecar.kzg_proofs, + ) +``` + +##### `verify_data_column_sidecar_inclusion_proof` + +```python +def verify_data_column_sidecar_inclusion_proof(sidecar: DataColumnSidecar) -> bool: + """ + Verify if the given KZG commitments included in the given beacon block. + """ + gindex = get_subtree_index(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + return is_valid_merkle_branch( + leaf=hash_tree_root(sidecar.kzg_commitments), + branch=sidecar.kzg_commitments_inclusion_proof, + depth=KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH, + index=gindex, + root=sidecar.signed_block_header.message.body_root, + ) +``` + +##### `compute_subnet_for_data_column_sidecar` + +```python +def compute_subnet_for_data_column_sidecar(column_index: ColumnIndex) -> SubnetID: + return SubnetID(column_index % DATA_COLUMN_SIDECAR_SUBNET_COUNT) +``` + +### The gossip domain: gossipsub + +Some gossip meshes are upgraded in the EIP-7594 fork to support upgraded types. + +#### Topics and messages + +##### Blob subnets + +###### Deprecated `blob_sidecar_{subnet_id}` + +`blob_sidecar_{subnet_id}` is deprecated. + +###### `data_column_sidecar_{subnet_id}` + +This topic is used to propagate column sidecars, where each column maps to some `subnet_id`. + +The *type* of the payload of this topic is `DataColumnSidecar`. + +The following validations MUST pass before forwarding the `sidecar: DataColumnSidecar` on the network, assuming the alias `block_header = sidecar.signed_block_header.message`: + +- _[REJECT]_ The sidecar's index is consistent with `NUMBER_OF_COLUMNS` -- i.e. `sidecar.index < NUMBER_OF_COLUMNS`. +- _[REJECT]_ The sidecar is for the correct subnet -- i.e. `compute_subnet_for_data_column_sidecar(sidecar.index) == subnet_id`. +- _[IGNORE]_ The sidecar is not from a future slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- i.e. validate that `block_header.slot <= current_slot` (a client MAY queue future sidecars for processing at the appropriate slot). +- _[IGNORE]_ The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that `block_header.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)` +- _[REJECT]_ The proposer signature of `sidecar.signed_block_header`, is valid with respect to the `block_header.proposer_index` pubkey. +- _[IGNORE]_ The sidecar's block's parent (defined by `block_header.parent_root`) has been seen (via both gossip and non-gossip sources) (a client MAY queue sidecars for processing once the parent block is retrieved). +- _[REJECT]_ The sidecar's block's parent (defined by `block_header.parent_root`) passes validation. +- _[REJECT]_ The sidecar is from a higher slot than the sidecar's block's parent (defined by `block_header.parent_root`). +- _[REJECT]_ The current finalized_checkpoint is an ancestor of the sidecar's block -- i.e. `get_checkpoint_block(store, block_header.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root`. +- _[REJECT]_ The sidecar's `kzg_commitments` field inclusion proof is valid as verified by `verify_data_column_sidecar_inclusion_proof(sidecar)`. +- _[REJECT]_ The sidecar's column data is valid as verified by `verify_data_column_sidecar_kzg_proofs(sidecar)`. +- _[IGNORE]_ The sidecar is the first sidecar for the tuple `(block_header.slot, block_header.proposer_index, sidecar.index)` with valid header signature, sidecar inclusion proof, and kzg proof. +- _[REJECT]_ The sidecar is proposed by the expected `proposer_index` for the block's slot in the context of the current shuffling (defined by `block_header.parent_root`/`block_header.slot`). + If the `proposer_index` cannot immediately be verified against the expected shuffling, the sidecar MAY be queued for later processing while proposers for the block's branch are calculated -- in such a case _do not_ `REJECT`, instead `IGNORE` this message. + +*Note:* In the `verify_data_column_sidecar_inclusion_proof(sidecar)` check, for all the sidecars of the same block, it verifies against the same set of `kzg_commitments` of the given beacon block. Client can choose to cache the result of the arguments tuple `(sidecar.kzg_commitments, sidecar.kzg_commitments_inclusion_proof, sidecar.signed_block_header)`. + +### The Req/Resp domain + +#### Messages + +##### DataColumnSidecarsByRoot v1 + +**Protocol ID:** `/eth2/beacon_chain/req/data_column_sidecars_by_root/1/` + +*[New in EIP7594]* + +The `` field is calculated as `context = compute_fork_digest(fork_version, genesis_validators_root)`: + +[1]: # (eth2spec: skip) + +| `fork_version` | Chunk SSZ type | +| - | - | +| `EIP7594_FORK_VERSION` | `eip7594.DataColumnSidecar` | + +Request Content: + +``` +( + List[DataColumnIdentifier, MAX_REQUEST_DATA_COLUMN_SIDECARS] +) +``` + +Response Content: + +``` +( + List[DataColumnSidecar, MAX_REQUEST_DATA_COLUMN_SIDECARS] +) +``` + +Requests sidecars by block root and index. +The response is a list of `DataColumnIdentifier` whose length is less than or equal to the number of requests. +It may be less in the case that the responding peer is missing blocks or sidecars. + +Before consuming the next response chunk, the response reader SHOULD verify the data column sidecar is well-formatted, has valid inclusion proof, and is correct w.r.t. the expected KZG commitments through `verify_data_column_sidecar_kzg_proofs`. + +No more than `MAX_REQUEST_DATA_COLUMN_SIDECARS` may be requested at a time. + +The response MUST consist of zero or more `response_chunk`. +Each _successful_ `response_chunk` MUST contain a single `DataColumnSidecar` payload. + +Clients MUST support requesting sidecars since `minimum_request_epoch`, where `minimum_request_epoch = max(finalized_epoch, current_epoch - MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS, EIP7594_FORK_EPOCH)`. If any root in the request content references a block earlier than `minimum_request_epoch`, peers MAY respond with error code `3: ResourceUnavailable` or not include the data column sidecar in the response. + +Clients MUST respond with at least one sidecar, if they have it. +Clients MAY limit the number of blocks and sidecars in the response. + +Clients SHOULD include a sidecar in the response as soon as it passes the gossip validation rules. +Clients SHOULD NOT respond with sidecars related to blocks that fail gossip validation rules. +Clients SHOULD NOT respond with sidecars related to blocks that fail the beacon chain state transition + +### The discovery domain: discv5 + +#### ENR structure + +##### `custody_subnet_count` + +A new field is added to the ENR under the key `custody_subnet_count` to facilitate custody data column discovery. + +| Key | Value | +|:-----------------------|:-------------| +| `custody_subnet_count` | SSZ `uint64` | diff --git a/specs/_features/eip7594/polynomial-commitments-sampling.md b/specs/_features/eip7594/polynomial-commitments-sampling.md index 7fe59c3b64..829e16ebaa 100644 --- a/specs/_features/eip7594/polynomial-commitments-sampling.md +++ b/specs/_features/eip7594/polynomial-commitments-sampling.md @@ -1,4 +1,4 @@ -# Deneb -- Polynomial Commitments +# EIP-7594 -- Polynomial Commitments ## Table of contents @@ -65,6 +65,8 @@ Public functions MUST accept raw bytes as input and perform the required cryptog | `PolynomialCoeff` | `List[BLSFieldElement, FIELD_ELEMENTS_PER_EXT_BLOB]` | A polynomial in coefficient form | | `Cell` | `Vector[BLSFieldElement, FIELD_ELEMENTS_PER_CELL]` | The unit of blob data that can come with their own KZG proofs | | `CellID` | `uint64` | Cell identifier | +| `RowIndex` | `uint64` | Row identifier | +| `ColumnIndex` | `uint64` | Column identifier | ## Constants @@ -442,8 +444,8 @@ def verify_cell_proof(commitment_bytes: Bytes48, ```python def verify_cell_proof_batch(row_commitments_bytes: Sequence[Bytes48], - row_ids: Sequence[uint64], - column_ids: Sequence[uint64], + row_indices: Sequence[RowIndex], + column_indices: Sequence[ColumnIndex], cells_bytes: Sequence[Vector[Bytes32, FIELD_ELEMENTS_PER_CELL]], proofs_bytes: Sequence[Bytes48]) -> bool: """ @@ -460,10 +462,10 @@ def verify_cell_proof_batch(row_commitments_bytes: Sequence[Bytes48], Public method. """ - assert len(cells_bytes) == len(proofs_bytes) == len(row_ids) == len(column_ids) + assert len(cells_bytes) == len(proofs_bytes) == len(row_indices) == len(column_indices) # Get commitments via row IDs - commitments_bytes = [row_commitments_bytes[row_id] for row_id in row_ids] + commitments_bytes = [row_commitments_bytes[row_index] for row_index in row_indices] # Get objects from bytes commitments = [bytes_to_kzg_commitment(commitment_bytes) for commitment_bytes in commitments_bytes] @@ -471,8 +473,8 @@ def verify_cell_proof_batch(row_commitments_bytes: Sequence[Bytes48], proofs = [bytes_to_kzg_proof(proof_bytes) for proof_bytes in proofs_bytes] return all( - verify_kzg_proof_multi_impl(commitment, coset_for_cell(column_id), cell, proof) - for commitment, column_id, cell, proof in zip(commitments, column_ids, cells, proofs) + verify_kzg_proof_multi_impl(commitment, coset_for_cell(column_index), cell, proof) + for commitment, column_index, cell, proof in zip(commitments, column_indices, cells, proofs) ) ``` diff --git a/specs/altair/p2p-interface.md b/specs/altair/p2p-interface.md index fac540fb99..5b0c938df7 100644 --- a/specs/altair/p2p-interface.md +++ b/specs/altair/p2p-interface.md @@ -33,6 +33,8 @@ Altair adds new messages, topics and data to the Req-Resp, Gossip and Discovery - [GetMetaData v2](#getmetadata-v2) - [Transitioning from v1 to v2](#transitioning-from-v1-to-v2) - [The discovery domain: discv5](#the-discovery-domain-discv5) + - [ENR structure](#enr-structure) + - [Sync committee bitfield](#sync-committee-bitfield) @@ -287,10 +289,16 @@ the responder MUST return the **InvalidRequest** response code. ### The discovery domain: discv5 -The `attnets` key of the ENR is used as defined in the Phase 0 document. +#### ENR structure + +##### Sync committee bitfield An additional bitfield is added to the ENR under the key `syncnets` to facilitate sync committee subnet discovery. The length of this bitfield is `SYNC_COMMITTEE_SUBNET_COUNT` where each bit corresponds to a distinct `subnet_id` for a specific sync committee subnet. The `i`th bit is set in this bitfield if the validator is currently subscribed to the `sync_committee_{i}` topic. +| Key | Value | +|:-------------|:-------------------------------------------------| +| `syncnets` | SSZ `Bitvector[SYNC_COMMITTEE_SUBNET_COUNT]` | + See the [validator document](./validator.md#sync-committee-subnet-stability) for further details on how the new bits are used. diff --git a/specs/deneb/polynomial-commitments.md b/specs/deneb/polynomial-commitments.md index 33945d249f..818bee6435 100644 --- a/specs/deneb/polynomial-commitments.md +++ b/specs/deneb/polynomial-commitments.md @@ -20,6 +20,7 @@ - [BLS12-381 helpers](#bls12-381-helpers) - [`hash_to_bls_field`](#hash_to_bls_field) - [`bytes_to_bls_field`](#bytes_to_bls_field) + - [`bls_field_to_bytes`](#bls_field_to_bytes) - [`validate_kzg_g1`](#validate_kzg_g1) - [`bytes_to_kzg_commitment`](#bytes_to_kzg_commitment) - [`bytes_to_kzg_proof`](#bytes_to_kzg_proof) @@ -170,6 +171,12 @@ def bytes_to_bls_field(b: Bytes32) -> BLSFieldElement: return BLSFieldElement(field_element) ``` +#### `bls_field_to_bytes` + +```python +def bls_field_to_bytes(x: BLSFieldElement) -> Bytes32: + return int.to_bytes(x % BLS_MODULUS, 32, KZG_ENDIANNESS) +``` #### `validate_kzg_g1` diff --git a/specs/deneb/validator.md b/specs/deneb/validator.md index c5ad1be911..fa2bfb5d1a 100644 --- a/specs/deneb/validator.md +++ b/specs/deneb/validator.md @@ -61,6 +61,19 @@ class GetPayloadResponse(object): blobs_bundle: BlobsBundle # [New in Deneb:EIP4844] ``` +```python +def compute_signed_block_header(signed_block: SignedBeaconBlock) -> SignedBeaconBlockHeader: + block = signed_block.message + block_header = BeaconBlockHeader( + slot=block.slot, + proposer_index=block.proposer_index, + parent_root=block.parent_root, + state_root=block.state_root, + body_root=hash_tree_root(block.body), + ) + return SignedBeaconBlockHeader(message=block_header, signature=signed_block.signature) +``` + ## Protocol ### `ExecutionEngine` @@ -145,14 +158,7 @@ def get_blob_sidecars(signed_block: SignedBeaconBlock, blobs: Sequence[Blob], blob_kzg_proofs: Sequence[KZGProof]) -> Sequence[BlobSidecar]: block = signed_block.message - block_header = BeaconBlockHeader( - slot=block.slot, - proposer_index=block.proposer_index, - parent_root=block.parent_root, - state_root=block.state_root, - body_root=hash_tree_root(block.body), - ) - signed_block_header = SignedBeaconBlockHeader(message=block_header, signature=signed_block.signature) + signed_block_header = compute_signed_block_header(signed_block) return [ BlobSidecar( index=index, diff --git a/tests/core/pyspec/eth2spec/test/deneb/unittests/polynomial_commitments/test_polynomial_commitments.py b/tests/core/pyspec/eth2spec/test/deneb/unittests/polynomial_commitments/test_polynomial_commitments.py index 7ee7168d9f..1d43d07caf 100644 --- a/tests/core/pyspec/eth2spec/test/deneb/unittests/polynomial_commitments/test_polynomial_commitments.py +++ b/tests/core/pyspec/eth2spec/test/deneb/unittests/polynomial_commitments/test_polynomial_commitments.py @@ -32,10 +32,6 @@ def bls_add_one(x): ) -def field_element_bytes(x): - return int.to_bytes(x % BLS_MODULUS, 32, "big") - - @with_deneb_and_later @spec_test @single_phase @@ -43,7 +39,7 @@ def test_verify_kzg_proof(spec): """ Test the wrapper functions (taking bytes arguments) for computing and verifying KZG proofs. """ - x = field_element_bytes(3) + x = spec.bls_field_to_bytes(3) blob = get_sample_blob(spec) commitment = spec.blob_to_kzg_commitment(blob) proof, y = spec.compute_kzg_proof(blob, x) @@ -58,7 +54,7 @@ def test_verify_kzg_proof_incorrect_proof(spec): """ Test the wrapper function `verify_kzg_proof` fails on an incorrect proof. """ - x = field_element_bytes(3465) + x = spec.bls_field_to_bytes(3465) blob = get_sample_blob(spec) commitment = spec.blob_to_kzg_commitment(blob) proof, y = spec.compute_kzg_proof(blob, x) diff --git a/tests/core/pyspec/eth2spec/test/eip7594/merkle_proof/__init__.py b/tests/core/pyspec/eth2spec/test/eip7594/merkle_proof/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/pyspec/eth2spec/test/eip7594/merkle_proof/test_single_merkle_proof.py b/tests/core/pyspec/eth2spec/test/eip7594/merkle_proof/test_single_merkle_proof.py new file mode 100644 index 0000000000..222f59b775 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/eip7594/merkle_proof/test_single_merkle_proof.py @@ -0,0 +1,74 @@ +import random + +from eth2spec.test.context import ( + spec_state_test, + with_eip7594_and_later, + with_test_suite_name, +) +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, + sign_block, +) +from eth2spec.test.helpers.execution_payload import ( + compute_el_block_hash, +) +from eth2spec.test.helpers.sharding import ( + get_sample_opaque_tx, +) +from eth2spec.debug.random_value import ( + RandomizationMode, + get_random_ssz_object, +) + + +def _run_blob_kzg_commitments_merkle_proof_test(spec, state, rng=None): + opaque_tx, blobs, blob_kzg_commitments, proofs = get_sample_opaque_tx(spec, blob_count=1) + if rng is None: + block = build_empty_block_for_next_slot(spec, state) + else: + block = get_random_ssz_object( + rng, + spec.BeaconBlock, + max_bytes_length=2000, + max_list_length=2000, + mode=RandomizationMode, + chaos=True, + ) + block.body.blob_kzg_commitments = blob_kzg_commitments + block.body.execution_payload.transactions = [opaque_tx] + block.body.execution_payload.block_hash = compute_el_block_hash(spec, block.body.execution_payload) + signed_block = sign_block(spec, state, block, proposer_index=0) + column_sidcars = spec.get_data_column_sidecars(signed_block, blobs) + column_sidcar = column_sidcars[0] + + yield "object", block.body + kzg_commitments_inclusion_proof = column_sidcar.kzg_commitments_inclusion_proof + gindex = spec.get_generalized_index(spec.BeaconBlockBody, 'blob_kzg_commitments') + yield "proof", { + "leaf": "0x" + column_sidcar.kzg_commitments.hash_tree_root().hex(), + "leaf_index": gindex, + "branch": ['0x' + root.hex() for root in kzg_commitments_inclusion_proof] + } + assert spec.is_valid_merkle_branch( + leaf=column_sidcar.kzg_commitments.hash_tree_root(), + branch=column_sidcar.kzg_commitments_inclusion_proof, + depth=spec.floorlog2(gindex), + index=spec.get_subtree_index(gindex), + root=column_sidcar.signed_block_header.message.body_root, + ) + assert spec.verify_data_column_sidecar_inclusion_proof(column_sidcar) + + +@with_test_suite_name("BeaconBlockBody") +@with_eip7594_and_later +@spec_state_test +def test_blob_kzg_commitments_merkle_proof__basic(spec, state): + yield from _run_blob_kzg_commitments_merkle_proof_test(spec, state) + + +@with_test_suite_name("BeaconBlockBody") +@with_eip7594_and_later +@spec_state_test +def test_blob_kzg_commitments_merkle_proof__random_block_1(spec, state): + rng = random.Random(1111) + yield from _run_blob_kzg_commitments_merkle_proof_test(spec, state, rng=rng) diff --git a/tests/core/pyspec/eth2spec/test/eip7594/unittests/das/__init__.py b/tests/core/pyspec/eth2spec/test/eip7594/unittests/das/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/pyspec/eth2spec/test/eip7594/unittests/das/test_das.py b/tests/core/pyspec/eth2spec/test/eip7594/unittests/das/test_das.py new file mode 100644 index 0000000000..24011fcdd7 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/eip7594/unittests/das/test_das.py @@ -0,0 +1,68 @@ +import random +from eth2spec.test.context import ( + spec_test, + single_phase, + with_eip7594_and_later, +) +from eth2spec.test.helpers.sharding import ( + get_sample_blob, +) + + +@with_eip7594_and_later +@spec_test +@single_phase +def test_compute_extended_matrix(spec): + rng = random.Random(5566) + + blob_count = 2 + input_blobs = [get_sample_blob(spec, rng=rng) for _ in range(blob_count)] + extended_matrix = spec.compute_extended_matrix(input_blobs) + assert len(extended_matrix) == spec.CELLS_PER_BLOB * blob_count + + rows = [extended_matrix[i:(i + spec.CELLS_PER_BLOB)] for i in range(0, len(extended_matrix), spec.CELLS_PER_BLOB)] + assert len(rows) == blob_count + assert len(rows[0]) == spec.CELLS_PER_BLOB + + for blob_index, row in enumerate(rows): + extended_blob = [] + for cell in row: + extended_blob.extend(cell) + blob_part = extended_blob[0:len(extended_blob) // 2] + blob = b''.join([spec.bls_field_to_bytes(x) for x in blob_part]) + assert blob == input_blobs[blob_index] + + +@with_eip7594_and_later +@spec_test +@single_phase +def test_recover_matrix(spec): + rng = random.Random(5566) + + # Number of samples we will be recovering from + N_SAMPLES = spec.CELLS_PER_BLOB // 2 + + blob_count = 2 + cells_dict = {} + original_cells = [] + for blob_index in range(blob_count): + # Get the data we will be working with + blob = get_sample_blob(spec, rng=rng) + # Extend data with Reed-Solomon and split the extended data in cells + cells = spec.compute_cells(blob) + original_cells.append(cells) + cell_ids = [] + # First figure out just the indices of the cells + for _ in range(N_SAMPLES): + cell_id = rng.randint(0, spec.CELLS_PER_BLOB - 1) + while cell_id in cell_ids: + cell_id = rng.randint(0, spec.CELLS_PER_BLOB - 1) + cell_ids.append(cell_id) + cell = cells[cell_id] + cells_dict[(blob_index, cell_id)] = cell + assert len(cell_ids) == N_SAMPLES + + # Recover the matrix + recovered_matrix = spec.recover_matrix(cells_dict, blob_count) + flatten_original_cells = [cell for cells in original_cells for cell in cells] + assert recovered_matrix == flatten_original_cells diff --git a/tests/core/pyspec/eth2spec/test/eip7594/unittests/polynomial_commitments/test_polynomial_commitments.py b/tests/core/pyspec/eth2spec/test/eip7594/unittests/polynomial_commitments/test_polynomial_commitments.py index f8a879e468..d49894adb1 100644 --- a/tests/core/pyspec/eth2spec/test/eip7594/unittests/polynomial_commitments/test_polynomial_commitments.py +++ b/tests/core/pyspec/eth2spec/test/eip7594/unittests/polynomial_commitments/test_polynomial_commitments.py @@ -11,10 +11,6 @@ from eth2spec.utils.bls import BLS_MODULUS -def field_element_bytes(x): - return int.to_bytes(x % BLS_MODULUS, 32, "big") - - @with_eip7594_and_later @spec_test @single_phase @@ -40,7 +36,7 @@ def test_verify_cell_proof(spec): commitment = spec.blob_to_kzg_commitment(blob) cells, proofs = spec.compute_cells_and_proofs(blob) - cells_bytes = [[field_element_bytes(element) for element in cell] for cell in cells] + cells_bytes = [[spec.bls_field_to_bytes(element) for element in cell] for cell in cells] cell_id = 0 assert spec.verify_cell_proof(commitment, cell_id, cells_bytes[cell_id], proofs[cell_id]) @@ -55,14 +51,14 @@ def test_verify_cell_proof_batch(spec): blob = get_sample_blob(spec) commitment = spec.blob_to_kzg_commitment(blob) cells, proofs = spec.compute_cells_and_proofs(blob) - cells_bytes = [[field_element_bytes(element) for element in cell] for cell in cells] + cells_bytes = [[spec.bls_field_to_bytes(element) for element in cell] for cell in cells] assert len(cells) == len(proofs) assert spec.verify_cell_proof_batch( row_commitments_bytes=[commitment], - row_ids=[0, 0], - column_ids=[0, 4], + row_indices=[0, 0], + column_indices=[0, 4], cells_bytes=[cells_bytes[0], cells_bytes[4]], proofs_bytes=[proofs[0], proofs[4]], ) @@ -84,15 +80,15 @@ def test_recover_polynomial(spec): # Extend data with Reed-Solomon and split the extended data in cells cells = spec.compute_cells(blob) - cells_bytes = [[field_element_bytes(element) for element in cell] for cell in cells] + cells_bytes = [[spec.bls_field_to_bytes(element) for element in cell] for cell in cells] # Compute the cells we will be recovering from cell_ids = [] # First figure out just the indices of the cells for i in range(N_SAMPLES): - j = rng.randint(0, spec.CELLS_PER_BLOB) + j = rng.randint(0, spec.CELLS_PER_BLOB - 1) while j in cell_ids: - j = rng.randint(0, spec.CELLS_PER_BLOB) + j = rng.randint(0, spec.CELLS_PER_BLOB - 1) cell_ids.append(j) # Now the cells themselves known_cells_bytes = [cells_bytes[cell_id] for cell_id in cell_ids] diff --git a/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_config_invariants.py b/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_config_invariants.py index 20baca8bf4..c750c67697 100644 --- a/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_config_invariants.py +++ b/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_config_invariants.py @@ -5,6 +5,21 @@ ) +@with_eip7594_and_later +@spec_test +@single_phase +def test_invariants(spec): + assert spec.FIELD_ELEMENTS_PER_BLOB % spec.FIELD_ELEMENTS_PER_CELL == 0 + assert spec.FIELD_ELEMENTS_PER_BLOB * 2 % spec.config.NUMBER_OF_COLUMNS == 0 + assert spec.SAMPLES_PER_SLOT <= spec.config.NUMBER_OF_COLUMNS + assert spec.CUSTODY_REQUIREMENT <= spec.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT + assert spec.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT <= spec.config.NUMBER_OF_COLUMNS + assert spec.config.NUMBER_OF_COLUMNS % spec.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT == 0 + assert spec.config.MAX_REQUEST_DATA_COLUMN_SIDECARS == ( + spec.config.MAX_REQUEST_BLOCKS_DENEB * spec.config.NUMBER_OF_COLUMNS + ) + + @with_eip7594_and_later @spec_test @single_phase diff --git a/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_custody.py b/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_custody.py new file mode 100644 index 0000000000..e1ab136c4f --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_custody.py @@ -0,0 +1,53 @@ +from eth2spec.test.context import ( + expect_assertion_error, + spec_test, + single_phase, + with_eip7594_and_later, +) + + +def run_get_custody_columns(spec, peer_count, custody_subnet_count): + assignments = [spec.get_custody_columns(node_id, custody_subnet_count) for node_id in range(peer_count)] + + columns_per_subnet = spec.NUMBER_OF_COLUMNS // spec.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT + for assignment in assignments: + assert len(assignment) == custody_subnet_count * columns_per_subnet + assert len(assignment) == len(set(assignment)) + + +@with_eip7594_and_later +@spec_test +@single_phase +def test_get_custody_columns_peers_within_number_of_columns(spec): + peer_count = 10 + custody_subnet_count = spec.CUSTODY_REQUIREMENT + assert spec.NUMBER_OF_COLUMNS > peer_count + run_get_custody_columns(spec, peer_count, custody_subnet_count) + + +@with_eip7594_and_later +@spec_test +@single_phase +def test_get_custody_columns_peers_more_than_number_of_columns(spec): + peer_count = 200 + custody_subnet_count = spec.CUSTODY_REQUIREMENT + assert spec.NUMBER_OF_COLUMNS < peer_count + run_get_custody_columns(spec, peer_count, custody_subnet_count) + + +@with_eip7594_and_later +@spec_test +@single_phase +def test_get_custody_columns_maximum_subnets(spec): + peer_count = 10 + custody_subnet_count = spec.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT + run_get_custody_columns(spec, peer_count, custody_subnet_count) + + +@with_eip7594_and_later +@spec_test +@single_phase +def test_get_custody_columns_custody_size_more_than_number_of_columns(spec): + node_id = 1 + custody_subnet_count = spec.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT + 1 + expect_assertion_error(lambda: spec.get_custody_columns(node_id, custody_subnet_count)) diff --git a/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_networking.py b/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_networking.py new file mode 100644 index 0000000000..2ab52be6c5 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_networking.py @@ -0,0 +1,19 @@ +from eth2spec.test.context import ( + spec_test, + single_phase, + with_eip7594_and_later, +) + + +@with_eip7594_and_later +@spec_test +@single_phase +def test_compute_subnet_for_data_column_sidecar(spec): + subnet_results = [] + for column_index in range(spec.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT): + subnet_results.append(spec.compute_subnet_for_data_column_sidecar(column_index)) + # no duplicates + assert len(subnet_results) == len(set(subnet_results)) + # next one should be duplicate + next_subnet = spec.compute_subnet_for_data_column_sidecar(spec.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT) + assert next_subnet == subnet_results[0] diff --git a/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_security.py b/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_security.py new file mode 100644 index 0000000000..dd85a673e5 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/eip7594/unittests/test_security.py @@ -0,0 +1,26 @@ +from eth2spec.test.context import ( + spec_test, + single_phase, + with_eip7594_and_later, + with_phases, +) +from eth2spec.test.helpers.constants import ( + MAINNET, +) + + +@with_eip7594_and_later +@spec_test +@single_phase +@with_phases([MAINNET]) +def test_sampling_config(spec): + probability_of_unavailable = 2 ** (-int(spec.SAMPLES_PER_SLOT)) + # TODO: What is the security requirement? + security_requirement = 0.01 + assert probability_of_unavailable <= security_requirement + + column_size_in_bytes = spec.FIELD_ELEMENTS_PER_CELL * spec.BYTES_PER_FIELD_ELEMENT * spec.MAX_BLOBS_PER_BLOCK + bytes_per_slot = column_size_in_bytes * spec.SAMPLES_PER_SLOT + # TODO: What is the bandwidth requirement? + bandwidth_requirement = 10000 # bytes/s + assert bytes_per_slot // spec.config.SECONDS_PER_SLOT < bandwidth_requirement diff --git a/tests/core/pyspec/eth2spec/test/helpers/das.py b/tests/core/pyspec/eth2spec/test/helpers/das.py new file mode 100644 index 0000000000..e69de29bb2