Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bb674ef
Copied from Danny's ethresearch post
hwwhww Nov 9, 2023
b62d532
wip
hwwhww Nov 14, 2023
af9a9b7
wip
hwwhww Nov 29, 2023
54661f4
Add `TARGET_NUMBER_OF_PEERS`
hwwhww Nov 29, 2023
a27bee0
Add networking spec draft
hwwhww Nov 29, 2023
5a0b661
fix
hwwhww Nov 29, 2023
ef3586e
simplification
hwwhww Nov 29, 2023
88d840e
Rename `DoYouHave` to `GetCustodyStatus`
hwwhww Nov 29, 2023
ab85c7c
Add DataLineSidecar design
hwwhww Nov 29, 2023
0440a8c
Apply suggestions from code review
hwwhww Dec 1, 2023
b2d08fa
Revamp after reviews and discussion
hwwhww Dec 1, 2023
65ce492
Remove `CustodyStatus`
hwwhww Dec 1, 2023
b36bbf5
minor fix
hwwhww Dec 2, 2023
b970213
Change`DataColumn` to `List[DataCell, MAX_BLOBS_PER_BLOCK]`
hwwhww Dec 2, 2023
28a914a
Move folder
hwwhww Dec 2, 2023
3c386fe
Replace `DataColumnByRootAndIndex` with `DataColumnSidecarByRoot` mes…
hwwhww Dec 2, 2023
b5d1220
Remove `DataRow`
hwwhww Dec 2, 2023
9d0cd0f
Apply suggestions from @jacobkaufmann code review
hwwhww Dec 4, 2023
8d55943
Represent matrix in `BLSFieldElement` form
hwwhww Dec 4, 2023
8e7dca2
Merge branch 'dev' into peer-das
hwwhww Dec 5, 2023
b2ffa27
Misc minor fix
hwwhww Dec 5, 2023
2feed04
Add linter support
hwwhww Dec 5, 2023
82d2e58
Add column subnet validation. Split `verify_column_sidecar` into two …
hwwhww Dec 5, 2023
208a4f7
Fix `get_data_column_sidecars` by using `compute_samples_and_proofs`
hwwhww Dec 5, 2023
be5a0e2
Apply suggestions from code review
hwwhww Dec 5, 2023
9435da3
Do not assign row custody
hwwhww Dec 5, 2023
e896d40
Apply suggestions from code review
hwwhww Dec 6, 2023
0dde817
Revamp reconstruction section
hwwhww Dec 6, 2023
6f08051
Use depth as the primary preset for inclusion proof. Fix `get_data_co…
hwwhww Dec 6, 2023
b08e1ed
Change `SAMPLES_PER_SLOT` to 8 and add tests (requirement TBD)
hwwhww Dec 6, 2023
d714e97
Apply PR feedback from @ppopth and @jtraglia
hwwhww Dec 7, 2023
826782c
Fix `get_data_column_sidecars`
hwwhww Dec 7, 2023
08856d0
Apply suggestions from code review
hwwhww Dec 11, 2023
317b30e
Apply suggestions from code review
hwwhww Dec 12, 2023
8e28884
Fix `get_data_column_sidecars` and `get_custody_lines`
hwwhww Dec 12, 2023
df76abf
Apply suggestions from code review
hwwhww Dec 12, 2023
e61f454
Enhance tests
hwwhww Dec 12, 2023
e38c4c4
fix typo
hwwhww Dec 19, 2023
f40c75e
Remove `epoch` from `get_custody_lines`
hwwhww Dec 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions specs/deneb/p2p-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ The specification of these changes continues in the same format as the network s
- [Topics and messages](#topics-and-messages)
- [Global topics](#global-topics)
- [`beacon_block`](#beacon_block)
- [`blob_sidecar_{subnet_id}`](#blob_sidecar_subnet_id)
- [`beacon_aggregate_and_proof`](#beacon_aggregate_and_proof)
- [Blob subnets](#blob-subnets)
- [`blob_sidecar_{subnet_id}`](#blob_sidecar_subnet_id)
- [Attestation subnets](#attestation-subnets)
- [`beacon_attestation_{subnet_id}`](#beacon_attestation_subnet_id)
- [Transitioning the gossip](#transitioning-the-gossip)
Expand Down Expand Up @@ -146,6 +147,25 @@ New validation:
- _[REJECT]_ The length of KZG commitments is less than or equal to the limitation defined in Consensus Layer --
i.e. validate that `len(body.signed_beacon_block.message.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK`

###### `beacon_aggregate_and_proof`

*[Modified in Deneb:EIP7045]*

The following validation is removed:
* _[IGNORE]_ `aggregate.data.slot` is within the last `ATTESTATION_PROPAGATION_SLOT_RANGE` slots (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) --
i.e. `aggregate.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= aggregate.data.slot`
(a client MAY queue future aggregates for processing at the appropriate slot).

The following validations are added in its place:
* _[IGNORE]_ `aggregate.data.slot` is equal to or earlier than the `current_slot` (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) --
i.e. `aggregate.data.slot <= current_slot`
(a client MAY queue future aggregates for processing at the appropriate slot).
* _[IGNORE]_ the epoch of `aggregate.data.slot` is either the current or previous epoch
(with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) --
i.e. `compute_epoch_at_slot(aggregate.data.slot) in (get_previous_epoch(state), get_current_epoch(state))`

##### Blob subnets

###### `blob_sidecar_{subnet_id}`

*[New in Deneb:EIP4844]*
Expand All @@ -169,23 +189,6 @@ The following validations MUST pass before forwarding the `blob_sidecar` on the
- _[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.

###### `beacon_aggregate_and_proof`

*[Modified in Deneb:EIP7045]*

The following validation is removed:
* _[IGNORE]_ `aggregate.data.slot` is within the last `ATTESTATION_PROPAGATION_SLOT_RANGE` slots (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) --
i.e. `aggregate.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= aggregate.data.slot`
(a client MAY queue future aggregates for processing at the appropriate slot).

The following validations are added in its place:
* _[IGNORE]_ `aggregate.data.slot` is equal to or earlier than the `current_slot` (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) --
i.e. `aggregate.data.slot <= current_slot`
(a client MAY queue future aggregates for processing at the appropriate slot).
* _[IGNORE]_ the epoch of `aggregate.data.slot` is either the current or previous epoch
(with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) --
i.e. `compute_epoch_at_slot(aggregate.data.slot) in (get_previous_epoch(state), get_current_epoch(state))`

##### Attestation subnets

###### `beacon_attestation_{subnet_id}`
Expand Down
150 changes: 150 additions & 0 deletions specs/peerdas/das-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Peer Data Availability Sampling -- Core

**Notice**: This document is a work-in-progress for researchers and implementers.

## 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 -->

- [Configuration](#configuration)
- [Data size](#data-size)
- [Custody setting](#custody-setting)
- [Helper functions](#helper-functions)
- [`LineType`](#linetype)
- [`get_custody_lines`](#get_custody_lines)
- [Custody](#custody)
- [Custody requirement](#custody-requirement)
- [Public, deterministic selection](#public-deterministic-selection)
- [Peer discovery](#peer-discovery)
- [Row/Column gossip](#rowcolumn-gossip)
- [Parameters](#parameters)
- [Reconstruction and cross-seeding](#reconstruction-and-cross-seeding)
- [Peer sampling](#peer-sampling)
- [Peer scoring](#peer-scoring)
- [DAS providers](#das-providers)
- [A note on fork choice](#a-note-on-fork-choice)

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

## Configuration

### Data size

| Name | Value | Description |
| - | - | - |
| `NUMBER_OF_COLUMNS` | `uint64(2**4)` (= 32) | Number of columns in the 1D data array |

### Custody setting

| Name | Value | Description |
| - | - | - |
| `SAMPLES_PER_SLOT` | `70` | Number of random samples a node queries per slot |
| `CUSTODY_REQUIREMENT` | `2` | Minimum number of both rows and columns an honest node custodies and serves samples from |
| `TARGET_NUMBER_OF_PEERS` | `70` | Suggested minimum peer count |

### Helper functions

#### `LineType`

It is implementation-dependent helpers for distinguishing the rows and columns in the following helpers.

```python
class LineType(enum.Enum):
ROW = 0
COLUMN = 1
```

#### `get_custody_lines`

```python
def get_custody_lines(node_id: int, epoch: int, custody_size: int, line_type: LineType) -> list[int]:
bound = MAX_BLOBS_PER_BLOCK if line_type else NUMBER_OF_COLUMNS
all_items = list(range(bound))
assert custody_size <= len(all_items)
line_index = (node_id + epoch) % bound
return [all_items[(line_index + i) % len(all_items)] for i in range(custody_size)]
```

## Custody

### Custody requirement

Each node downloads and custodies a minimum of `CUSTODY_REQUIREMENT` rows and `CUSTODY_REQUIREMENT` columns per slot. The particular rows and columns 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_lines: 8` if the node custodies `8` rows and `8` columns each slot) -- up to a maximum of `max(MAX_BLOBS_PER_BLOCK, NUMBER_OF_COLUMNS)` (i.e. a super-full node).

A node stores the custodied rows/columns for the duration of the pruning period and responds to peer requests for samples on those rows/columns.

### Public, deterministic selection

The particular rows and columns that a node custodies are selected pseudo-randomly as a function (`get_custody_lines`) of the node-id, epoch, 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` and `epoch` 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.

*Note*: Even though this function accepts `epoch` as an input, the function can be tuned to remain stable for many epochs depending on network/subnet stability requirements. There is a trade-off between the rigidity of the network and the depth to which a subnet can be utilized for recovery. To ensure subnets can be utilized for recovery, staggered rotation likely needs to happen on the order of the pruning period.

## Peer discovery

At each slot, a node needs to be able to readily sample from *any* set of rows and 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 row/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.

## Row/Column gossip

### Parameters

There are both `MAX_BLOBS_PER_BLOCK` row and `NUMBER_OF_COLUMNS` column gossip topics.

1. For each column -- `row_x` for `x` from `0` to `NUMBER_OF_COLUMNS` (non-inclusive).
2. For each row -- `column_y` for `y` from `0` to `MAX_BLOBS_PER_BLOCK` (non-inclusive).

To custody a particular row or column, a node joins the respective gossip subnet. Verifiable samples from their respective row/column are gossiped on the assigned subnet.

### Reconstruction and cross-seeding

In the event a node does *not* receive all samples for a given row/column but does receive enough to reconstruct (e.g., 50%+, a function of coding rate), the node should reconstruct locally and send the reconstructed samples on the subnet.

Additionally, the node should send (cross-seed) any samples missing from a given row/column they are assigned to that they have obtained via an alternative method (ancillary gossip or reconstruction). E.g., if the node reconstructs `row_x` and is also participating in the `column_y` subnet in which the `(x, y)` sample was missing, send the reconstructed sample to `column_y`.

*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.

## Peer sampling

At each slot, a node makes (locally randomly determined) `SAMPLES_PER_SLOT` queries for samples from their peers. A node utilizes `get_custody_lines(..., line_type=LineType.ROW)`/`get_custody_lines(..., line_type=LineType.COLUMN)` 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.
Copy link

Choose a reason for hiding this comment

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

I think:

  • Having a sample available should be positive
  • Not having a sample (bitfield negative) should be neutral
  • Not having a sample after responding with a positive bitfield value should come with a high penalty


*Note*: a peer might not respond to requests either because they are dishonest (don't actually custody the data), because of bandwidth saturation (local throttling), or because they were, themselves, not able to get all the samples. In the first two cases, the peer is not of consistent DAS value and a node can/should seek to optimize for better peers. In the latter, the node can make local determinations based on repeated `DO_YOU_HAVE` queries to that peer and other peers to assess the value/honesty of the peer.

## 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.

They can also be found out-of-band and configured into a node to connect to directly and prioritize. For example, some L2 DAO might support 10 super-full nodes as a public good, and nodes could choose to add some set of these to their local configuration 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) alternative peer discovery mechanisms.

## A note on fork choice

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.
186 changes: 186 additions & 0 deletions specs/peerdas/p2p-interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Peer Data Availability Sampling -- Networking

**Notice**: This document is a work-in-progress for researchers and implementers.

## 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 -->

- [Modifications in PeerDAS](#modifications-in-peerdas)
- [Custom types](#custom-types)
- [Preset](#preset)
- [Containers](#containers)
- [`DataColumnSidecar`](#datacolumnsidecar)
- [Helpers](#helpers)
- [`get_row`](#get_row)
- [`get_column`](#get_column)
- [`verify_column_sidecar`](#verify_column_sidecar)
- [The gossip domain: gossipsub](#the-gossip-domain-gossipsub)
- [Topics and messages](#topics-and-messages)
- [Samples subnets](#samples-subnets)
- [`data_column_{subnet_id}`](#data_column_subnet_id)
- [The Req/Resp domain](#the-reqresp-domain)
- [Messages](#messages)
- [DataRowByRootAndIndex v1](#datarowbyrootandindex-v1)
- [DataColumnByRootAndIndex v1](#datacolumnbyrootandindex-v1)

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

## Modifications in PeerDAS

### Custom types

We define the following Python custom types for type hinting and readability:

| Name | SSZ equivalent | Description |
| - | - | - |
| `ExtendedData` | `ByteList[MAX_BLOBS_PER_BLOCK * BYTES_PER_BLOB * 2]` | The full data with blobs and 1-D erasure coding extension |
| `DataRow` | `ByteList[BYTES_PER_BLOB * 2]` | The data of each row in PeerDAS |
| `DataColumn` | `ByteList[MAX_BLOBS_PER_BLOCK * BYTES_PER_BLOB * 2 // NUMBER_OF_COLUMNS]` | The data of each column in PeerDAS |
| `LineIndex` | `uint64` | The index of the rows or columns in `ExtendedData` matrix |

Choose a reason for hiding this comment

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

if we can continue to use BlobIndex to specify a particular BlobSidecar (or extended blob), then could we use something like DataColumnIndex here? is there some forward compatibility concern?

Copy link
Owner Author

Choose a reason for hiding this comment

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

The benefit of not using BlobIndex is to not bind NUMBER_OF_ROWS == MAX_BLOBS_PER_BLOCK invariant with DataRowByRootAndIndex req. But I can see your point that it's more symmetric to use a DataColumnIndex in DataColumnSidecar. 🤔

Copy link

Choose a reason for hiding this comment

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

I agree with @jacobkaufmann on this and there is no NUMBER_OF_ROWS anymore.


### Preset

| Name | Value | Description |
|------------------------------------------|-----------------------------------|---------------------------------------------------------------------|
| `KZG_COMMITMENTS_MERKLE_PROOF_INDEX` | `uint64(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments'))` (= 27) | <!-- predefined --> Merkle proof index for `blob_kzg_commitments` |

### Containers

#### `DataColumnSidecar`

```python
class DataColumnSidecar(Container):
index: LineIndex # Index of column in extended data
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_commitment_merkle_proof: Vector[Bytes32, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH]
```


### Helpers

##### `get_row`

```python
def get_row(data: ExtendedData, index: LineIndex) -> DataRow:
length = BYTES_PER_BLOB * 2
assert len(data) % (BYTES_PER_BLOB * 2) == 0
assert len(data) // (BYTES_PER_BLOB * 2) <= MAX_BLOBS_PER_BLOCK
return data[index * length:(index + 1) * length]
```

##### `get_column`

```python
def get_column(data: ExtendedData, index: LineIndex) -> DataColumn:
assert len(data) % NUMBER_OF_COLUMNS = 0
assert BYTES_PER_BLOB * 2 % NUMBER_OF_COLUMNS == 0

row_count = len(data) // NUMBER_OF_COLUMNS
column_width = BYTES_PER_BLOB * 2 // NUMBER_OF_COLUMNS
column = []
for row in range(row_count):
start = row * NUMBER_OF_COLUMNS + column_index
column.append(data[start:start + column_width])
return column
```

##### `verify_column_sidecar`

```python
def verify_column_sidecar(sidecar: DataColumnSidecar) -> bool:
column = sidecar.column
column_width = MAX_BLOBS_PER_BLOCK * BYTES_PER_BLOB * 2
cell_count = len(column) // column_width
cells = [column[i * column_width:(i + 1) * column_width] for i in range(cell_count)]

assert len(cells) == len(sidecar.kzg_commitments) == len(sidecar.kzg_proofs)
# KZG batch verify the cells match the corresponding commitments and proofs
assert verify_cells(cells, sidecar.index, sidecar.kzg_commitments, sidecar.kzg_proofs)
# Verify if it's included in the beacon block
return is_valid_merkle_branch(
leaf=hash_tree_root(data_line_sidecar.kzg_commitments),
branch=data_line_sidecar.kzg_commitments_merkle_proof,
depth=floorlog2(KZG_COMMITMENTS_MERKLE_PROOF_INDEX),
index=KZG_COMMITMENTS_MERKLE_PROOF_INDEX,
root=data_line_sidecar.signed_block_header.message.body_root,
)
```

TODO: define `verify_cells` helper.

### The gossip domain: gossipsub

Some gossip meshes are upgraded in the fork of Pe to support upgraded types.

#### Topics and messages

##### Samples subnets

###### `data_column_{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 `DataColumn`.

TODO: add verification rules. Verify with `verify_column_sidecar`.

### The Req/Resp domain

#### Messages

##### DataRowByRootAndIndex v1

**Protocol ID:** `/eth2/beacon_chain/req/data_row_by_root_and_index/1/`

The `<context-bytes>` field is calculated as `context = compute_fork_digest(fork_version, genesis_validators_root)`:

Request Content:
```
(
block_root: Root
index: LineIndex
)
```

`index` maps the the row index of the extened data.

Response Content:
```
(
DataRow
)
```

The response is the row as `get_row(data: ExtendedData, index: LineIndex)` computed.

##### DataColumnByRootAndIndex v1

**Protocol ID:** `/eth2/beacon_chain/req/data_column_by_root_and_index/1/`

The `<context-bytes>` field is calculated as `context = compute_fork_digest(fork_version, genesis_validators_root)`:

Request Content:
```
(
block_root: Root
index: LineIndex
)
```

`index` maps the the column index of the extened data.

Response Content:
```
(
DataColumn
)
```

The response is the column as `get_column(data: ExtendedData, index: LineIndex)` computed.