Add PeerDAS tests (#4391)

This commit is contained in:
Leo Lara
2025-06-30 02:23:15 +07:00
committed by GitHub
parent 50e2cc5867
commit 815751ecb3
7 changed files with 521 additions and 42 deletions

View File

@@ -32,6 +32,7 @@ dependencies = [
[project.optional-dependencies]
test = [
"deepdiff==8.5.0",
"pytest-cov==6.2.1",
"pytest-xdist==3.7.0",
"pytest==8.4.1",

View File

@@ -4,20 +4,12 @@ from eth2spec.test.context import (
spec_state_test,
with_all_phases_from_except,
)
from eth2spec.test.helpers.blob import (
get_sample_blob_tx,
)
from eth2spec.test.helpers.block import (
build_empty_block_for_next_slot,
)
from eth2spec.test.helpers.blob import get_block_with_blob
from eth2spec.test.helpers.constants import (
DENEB,
EIP7732,
FULU,
)
from eth2spec.test.helpers.execution_payload import (
compute_el_block_hash,
)
from eth2spec.test.helpers.fork_choice import (
BlobData,
get_genesis_forkchoice_store_and_block,
@@ -29,19 +21,6 @@ from eth2spec.test.helpers.state import (
)
def get_block_with_blob(spec, state, rng=None):
block = build_empty_block_for_next_slot(spec, state)
opaque_tx, blobs, blob_kzg_commitments, blob_kzg_proofs = get_sample_blob_tx(
spec, blob_count=1, rng=rng
)
block.body.execution_payload.transactions = [opaque_tx]
block.body.execution_payload.block_hash = compute_el_block_hash(
spec, block.body.execution_payload, state
)
block.body.blob_kzg_commitments = blob_kzg_commitments
return block, blobs, blob_kzg_proofs
# TODO(jtraglia): Use with_all_phases_from_to_except after EIP7732 is based on Fulu.
# This applies to every other test in this file too.
@with_all_phases_from_except(DENEB, [FULU, EIP7732])

View File

@@ -1 +1,337 @@
# TODO: add new data availability tests.
from random import Random
from eth2spec.test.context import (
spec_state_test,
with_fulu_and_later,
)
from eth2spec.test.helpers.blob import get_block_with_blob_and_sidecars
from eth2spec.test.helpers.fork_choice import (
BlobData,
get_genesis_forkchoice_store_and_block,
on_tick_and_append_step,
tick_and_add_block_with_data,
)
def flip_one_bit_in_bytes(data: bytes, index: int = 0) -> bytes:
"""
Flip one bit in a bytes object at the given index.
"""
constr = data.__class__
byte_index = index // 8
bit_index = 7 - (index % 8)
byte = data[byte_index]
flipped_byte = byte ^ (1 << bit_index)
return constr(bytes(data[:byte_index]) + bytes([flipped_byte]) + bytes(data[byte_index + 1 :]))
def get_alt_sidecars(spec, state):
"""
Get alternative sidecars for negative test cases.
"""
rng = Random(4321)
_, _, _, _, alt_sidecars = get_block_with_blob_and_sidecars(spec, state, rng=rng, blob_count=2)
return alt_sidecars
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__ok(spec, state):
"""
Similar to test_simple_blob_data, but in PeerDAS version that is from Fulu onwards.
It covers code related to the blob sidecars because on_block calls `is_data_available`
and we are calling `get_data_column_sidecars_from_block` in the test itself.
"""
rng = Random(1234)
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield "anchor_state", state
yield "anchor_block", anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
# On receiving a block of `GENESIS_SLOT + 1` slot
_, blobs, blob_kzg_proofs, signed_block, sidecars = get_block_with_blob_and_sidecars(
spec, state, rng=rng, blob_count=2
)
blob_data = BlobData(blobs, blob_kzg_proofs, sidecars)
yield from tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data)
assert spec.get_head(store) == signed_block.message.hash_tree_root()
# On receiving a block of next epoch
_, blobs, blob_kzg_proofs, signed_block, sidecars = get_block_with_blob_and_sidecars(
spec, state, rng=rng, blob_count=2
)
blob_data = BlobData(blobs, blob_kzg_proofs, sidecars)
yield from tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data)
assert spec.get_head(store) == signed_block.message.hash_tree_root()
yield "steps", test_steps
def run_on_block_peerdas_invalid_test(spec, state, fn):
"""
Run a invalid PeerDAS on_block test with a sidecars mutation function.
"""
rng = Random(1234)
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield "anchor_state", state
yield "anchor_block", anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
_, blobs, blob_kzg_proofs, signed_block, sidecars = get_block_with_blob_and_sidecars(
spec, state, rng=rng, blob_count=2
)
sidecars = fn(sidecars)
blob_data = BlobData(blobs, blob_kzg_proofs, [])
yield from tick_and_add_block_with_data(
spec, store, signed_block, test_steps, blob_data, valid=False
)
assert spec.get_head(store) != signed_block.message.hash_tree_root()
yield "steps", test_steps
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__not_available(spec, state):
"""
Test is_data_available throws an exception when not enough columns are sampled.
"""
yield from run_on_block_peerdas_invalid_test(
spec,
state,
# Empty sidecars will trigger the simulation of not enough columns being sampled
lambda _: [],
)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_zero_blobs(spec, state):
"""
Test is_data_available returns false when there are no blobs in the sidecars.
"""
def invalid_zero_blobs(sidecars):
sidecars[0].column = []
sidecars[0].kzg_commitments = []
sidecars[0].kzg_proofs = []
return sidecars
yield from run_on_block_peerdas_invalid_test(spec, state, invalid_zero_blobs)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_index_1(spec, state):
"""
Test invalid index in sidecars for negative PeerDAS on_block test.
"""
def invalid_index(sidecars):
sidecars[0].index = 128 # Invalid index
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_index)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_index_2(spec, state):
"""
Test invalid index in sidecars for negative PeerDAS on_block test.
"""
def invalid_index(sidecars):
sidecars[0].index = 256 # Invalid index
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_index)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_mismatch_len_column_1(spec, state):
"""
Test mismatch length in column for negative PeerDAS on_block test.
"""
def invalid_mismatch_len_column(sidecars):
sidecars[0].column = sidecars[0].column[1:]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_mismatch_len_column)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_mismatch_len_column_2(spec, state):
"""
Test mismatch length in column for negative PeerDAS on_block test.
"""
def invalid_mismatch_len_column(sidecars):
sidecars[1].column = sidecars[1].column[1:]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_mismatch_len_column)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_mismatch_len_kzg_commitments_1(spec, state):
"""
Test mismatch length in kzg_commitments for negative PeerDAS on_block test.
"""
def invalid_mismatch_len_kzg_commitments(sidecars):
sidecars[0].kzg_commitments = sidecars[0].kzg_commitments[1:]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_mismatch_len_kzg_commitments)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_mismatch_len_kzg_commitments_2(spec, state):
"""
Test mismatch length in kzg_commitments for negative PeerDAS on_block test.
"""
def invalid_mismatch_len_kzg_commitments(sidecars):
sidecars[1].kzg_commitments = sidecars[1].kzg_commitments[1:]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_mismatch_len_kzg_commitments)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_mismatch_len_kzg_proofs_1(spec, state):
"""
Test mismatch length in kzg_proofs for negative PeerDAS on_block test.
"""
def invalid_mismatch_len_kzg_proofs(sidecars):
sidecars[0].kzg_proofs = sidecars[0].kzg_proofs[1:]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_mismatch_len_kzg_proofs)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_mismatch_len_kzg_proofs_2(spec, state):
"""
Test mismatch length in kzg_proofs for negative PeerDAS on_block test.
"""
def invalid_mismatch_len_kzg_proofs(sidecars):
sidecars[1].kzg_proofs = sidecars[1].kzg_proofs[1:]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_mismatch_len_kzg_proofs)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_wrong_column_1(spec, state):
"""
Test wrong column for negative PeerDAS on_block test.
"""
def invalid_wrong_column(sidecars):
sidecars[0].column[0] = flip_one_bit_in_bytes(sidecars[0].column[0], 80)
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_wrong_column)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_wrong_column_2(spec, state):
"""
Test wrong column for negative PeerDAS on_block test.
"""
def invalid_wrong_column(sidecars):
sidecars[1].column[1] = flip_one_bit_in_bytes(sidecars[1].column[1], 20)
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_wrong_column)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_wrong_commitment_1(spec, state):
"""
Test wrong commitment for negative PeerDAS on_block test.
"""
alt_sidecars = get_alt_sidecars(spec, state)
def invalid_wrong_commitment(sidecars):
sidecars[0].kzg_commitments[0] = alt_sidecars[0].kzg_commitments[0]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_wrong_commitment)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_wrong_commitment_2(spec, state):
"""
Test wrong commitment for negative PeerDAS on_block test.
"""
alt_sidecars = get_alt_sidecars(spec, state)
def invalid_wrong_commitment(sidecars):
sidecars[1].kzg_commitments[1] = alt_sidecars[1].kzg_commitments[1]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_wrong_commitment)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_wrong_proof_1(spec, state):
"""
Test wrong proof for negative PeerDAS on_block test.
"""
alt_sidecars = get_alt_sidecars(spec, state)
def invalid_wrong_proof(sidecars):
sidecars[0].kzg_proofs[0] = alt_sidecars[0].kzg_proofs[0]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_wrong_proof)
@with_fulu_and_later
@spec_state_test
def test_on_block_peerdas__invalid_wrong_proof_2(spec, state):
"""
Test wrong proof for negative PeerDAS on_block test.
"""
alt_sidecars = get_alt_sidecars(spec, state)
def invalid_wrong_proof(sidecars):
sidecars[1].kzg_proofs[1] = alt_sidecars[1].kzg_proofs[1]
return sidecars
run_on_block_peerdas_invalid_test(spec, state, invalid_wrong_proof)

View File

@@ -1,13 +1,18 @@
import random
from deepdiff import DeepDiff
from eth2spec.test.context import (
single_phase,
spec_state_test,
spec_test,
with_fulu_and_later,
)
from eth2spec.test.helpers.blob import (
get_block_with_blob_and_sidecars,
get_sample_blob,
)
from eth2spec.test.helpers.fork_choice import BlobData, with_blob_data
def chunks(lst, n):
@@ -65,3 +70,62 @@ def test_recover_matrix(spec):
# Ensure that the recovered matrix matches the original matrix
assert recovered_matrix == matrix
def run_is_data_available_peerdas_test(spec, blob_data):
def callback():
yield spec.is_data_available(spec.Root(b"\x00" * 32))
return next(with_blob_data(spec, blob_data, callback))
@with_fulu_and_later
@spec_state_test
def test_is_data_available_peerdas(spec, state):
rng = random.Random(1234)
_, blobs, blob_kzg_proofs, _, sidecars = get_block_with_blob_and_sidecars(
spec, state, rng=rng, blob_count=2
)
blob_data = BlobData(blobs, blob_kzg_proofs, sidecars)
result = run_is_data_available_peerdas_test(spec, blob_data)
assert result is True, "Data should be available for the block with blob data"
@with_fulu_and_later
@spec_state_test
def test_get_data_column_sidecars(spec, state):
rng = random.Random(1234)
_, blobs, _, signed_block, sidecars = get_block_with_blob_and_sidecars(
spec, state, rng=rng, blob_count=2
)
sidecars_result = spec.get_data_column_sidecars(
signed_block_header=spec.compute_signed_block_header(signed_block),
kzg_commitments=sidecars[0].kzg_commitments,
kzg_commitments_inclusion_proof=sidecars[0].kzg_commitments_inclusion_proof,
cells_and_kzg_proofs=[spec.compute_cells_and_kzg_proofs(blob) for blob in blobs],
)
assert len(sidecars_result) == len(sidecars), (
"Should return the same number of sidecars as input"
)
assert DeepDiff(sidecars, sidecars_result) == {}, "Sidecars should match the expected sidecars"
@with_fulu_and_later
@spec_state_test
def test_get_data_column_sidecars_from_column_sidecar(spec, state):
rng = random.Random(1234)
_, blobs, _, _, sidecars = get_block_with_blob_and_sidecars(spec, state, rng=rng, blob_count=2)
sidecars_result = spec.get_data_column_sidecars_from_column_sidecar(
sidecar=sidecars[0],
cells_and_kzg_proofs=[spec.compute_cells_and_kzg_proofs(blob) for blob in blobs],
)
assert len(sidecars_result) == len(sidecars), (
"Should return the same number of sidecars as input"
)
assert DeepDiff(sidecars, sidecars_result) == {}, "Sidecars should match the expected sidecars"

View File

@@ -1,12 +1,17 @@
import random
from functools import cache
from random import Random
from rlp import encode, Serializable
from rlp.sedes import big_endian_int, Binary, binary, CountableList, List as RLPList
from eth2spec.test.helpers.block import build_empty_block_for_next_slot
from eth2spec.test.helpers.execution_payload import compute_el_block_hash
from eth2spec.test.helpers.forks import (
is_post_electra,
is_post_fulu,
)
from eth2spec.test.helpers.state import state_transition_and_sign_block
class Eip4844RlpTransaction(Serializable):
@@ -126,3 +131,32 @@ def get_max_blob_count(spec, state):
return spec.config.MAX_BLOBS_PER_BLOCK_ELECTRA
else:
return spec.config.MAX_BLOBS_PER_BLOCK
def get_block_with_blob(spec, state, rng: Random | None = None, blob_count=1):
block = build_empty_block_for_next_slot(spec, state)
opaque_tx, blobs, blob_kzg_commitments, blob_kzg_proofs = get_sample_blob_tx(
spec, blob_count=blob_count, rng=rng or random.Random(5566)
)
block.body.execution_payload.transactions = [opaque_tx]
block.body.execution_payload.block_hash = compute_el_block_hash(
spec, block.body.execution_payload, state
)
block.body.blob_kzg_commitments = blob_kzg_commitments
return block, blobs, blob_kzg_proofs
def get_block_with_blob_and_sidecars(spec, state, rng=None, blob_count=1):
block, blobs, blob_kzg_proofs = get_block_with_blob(spec, state, rng=rng, blob_count=blob_count)
cells_and_kzg_proofs = [_cached_compute_cells_and_kzg_proofs(spec, blob) for blob in blobs]
# We need a signed block to call `get_data_column_sidecars_from_block`
signed_block = state_transition_and_sign_block(spec, state, block)
sidecars = spec.get_data_column_sidecars_from_block(signed_block, cells_and_kzg_proofs)
return block, blobs, blob_kzg_proofs, signed_block, sidecars
@cache
def _cached_compute_cells_and_kzg_proofs(spec, blob):
return spec.compute_cells_and_kzg_proofs(blob)

View File

@@ -3,13 +3,14 @@ from typing import Any, NamedTuple
from eth_utils import encode_hex
from eth2spec.fulu.mainnet import DataColumnSidecar
from eth2spec.test.exceptions import BlockNotFoundException
from eth2spec.test.helpers.attestations import (
next_epoch_with_attestations,
next_slots_with_attestations,
state_transition_with_full_block,
)
from eth2spec.test.helpers.forks import is_post_eip7732
from eth2spec.test.helpers.forks import is_post_eip7732, is_post_fulu
from eth2spec.test.helpers.state import (
payload_state_transition,
payload_state_transition_no_store,
@@ -30,16 +31,29 @@ class BlobData(NamedTuple):
"""
blobs: Sequence[Any]
proofs: Sequence[bytes]
proofs: Sequence[bytes] | None = None
sidecars: Sequence[DataColumnSidecar] | None = None
def with_blob_data(spec, blob_data, func):
def with_blob_data(spec, blob_data: BlobData, func):
if not is_post_fulu(spec):
if blob_data.proofs is None:
raise ValueError("blob_data.proofs must be provided when pre FULU fork")
yield from with_blob_data_deneb(spec, blob_data, func)
else:
if blob_data.sidecars is None:
raise ValueError("blob_data.sidecars must be provided when post FULU fork")
yield from with_blob_data_fulu(spec, blob_data, func)
def with_blob_data_deneb(spec, blob_data: BlobData, func):
"""
This helper runs the given ``func`` with monkeypatched ``retrieve_blobs_and_proofs``
that returns ``blob_data.blobs, blob_data.proofs``.
"""
def retrieve_blobs_and_proofs(beacon_block_root):
def retrieve_blobs_and_proofs(_):
assert blob_data.proofs is not None, "blob_data.proofs must be provided"
return blob_data.blobs, blob_data.proofs
retrieve_blobs_and_proofs_backup = spec.retrieve_blobs_and_proofs
@@ -61,6 +75,37 @@ def with_blob_data(spec, blob_data, func):
assert is_called.value
def with_blob_data_fulu(spec, blob_data: BlobData, func):
"""
This helper runs the given ``func`` with monkeypatched ``retrieve_column_sidecars``
that returns ``blob_data``.
"""
def retrieve_column_sidecars(_):
assert blob_data.sidecars is not None, "blob_data.sidecars must be provided"
if len(blob_data.sidecars) == 0:
assert False, "Simulation: not all required columns have been sampled"
return blob_data.sidecars
retrieve_column_sidecars_backup = spec.retrieve_column_sidecars
spec.retrieve_column_sidecars = retrieve_column_sidecars
class AtomicBoolean:
value = False
is_called = AtomicBoolean()
def wrap(flag: AtomicBoolean):
yield from func()
flag.value = True
try:
yield from wrap(is_called)
finally:
spec.retrieve_column_sidecars = retrieve_column_sidecars_backup
assert is_called.value
def get_anchor_root(spec, state):
anchor_block_header = state.latest_block_header.copy()
if anchor_block_header.state_root == spec.Bytes32():
@@ -77,7 +122,7 @@ def tick_and_add_block(
merge_block=False,
block_not_found=False,
is_optimistic=False,
blob_data=None,
blob_data: BlobData | None = None,
):
pre_state = get_store_full_state(spec, store, signed_block.message.parent_root)
if merge_block:
@@ -189,6 +234,20 @@ def get_blobs_file_name(blobs=None, blobs_root=None):
return f"blobs_{encode_hex(blobs_root)}"
def get_sidecars_file_names(sidecars: Sequence[DataColumnSidecar]) -> Sequence[str]:
"""
Returns the file names for sidecars.
"""
return [get_sidecar_file_name(sidecar) for sidecar in sidecars]
def get_sidecar_file_name(sidecar: DataColumnSidecar) -> str:
"""
Returns the file name for a single sidecar.
"""
return f"sidecar_{encode_hex(sidecar.hash_tree_root())}"
def on_tick_and_append_step(spec, store, time, test_steps):
assert time >= store.time
spec.on_tick(store, time)
@@ -237,18 +296,23 @@ def add_block(
blobs_root = blobs.hash_tree_root()
yield get_blobs_file_name(blobs_root=blobs_root), blobs
is_blob_data_test = blob_data is not None
if blob_data.sidecars is not None:
for sidecar in blob_data.sidecars:
yield get_sidecar_file_name(sidecar), sidecar
def _append_step(is_blob_data_test, valid=True):
if is_blob_data_test:
test_steps.append(
{
"block": get_block_file_name(signed_block),
"blobs": get_blobs_file_name(blobs_root=blobs_root),
"proofs": [encode_hex(proof) for proof in blob_data.proofs],
"valid": valid,
}
)
def _append_step(valid=True):
if blob_data is not None:
step = {
"block": get_block_file_name(signed_block),
"blobs": get_blobs_file_name(blobs_root=blobs_root),
"valid": valid,
}
if blob_data.proofs is not None:
step["proofs"] = [encode_hex(proof) for proof in blob_data.proofs]
if blob_data.sidecars is not None:
step["sidecars"] = get_sidecars_file_names(blob_data.sidecars)
test_steps.append(step)
else:
test_steps.append(
{
@@ -260,20 +324,20 @@ def add_block(
if not valid:
if is_optimistic:
run_on_block(spec, store, signed_block, valid=True)
_append_step(is_blob_data_test, valid=False)
_append_step(valid=False)
else:
try:
run_on_block(spec, store, signed_block, valid=True)
except (AssertionError, BlockNotFoundException) as e:
if isinstance(e, BlockNotFoundException) and not block_not_found:
assert False
_append_step(is_blob_data_test, valid=False)
_append_step(valid=False)
return
else:
assert False
else:
run_on_block(spec, store, signed_block, valid=True)
_append_step(is_blob_data_test)
_append_step()
# An on_block step implies receiving block's attestations
for attestation in signed_block.message.body.attestations:

View File

@@ -84,6 +84,7 @@ The parameter that is required for executing `on_block(store, block)`.
blobs: string -- optional, the name of the `blobs_<32-byte-root>.ssz_snappy` file.
The blobs file content is a `List[Blob, MAX_BLOB_COMMITMENTS_PER_BLOCK]` SSZ object.
proofs: array of byte48 hex string -- optional, the proofs of blob commitments.
sidecars: string -- optional, array of the names of the `sidecar_<32-byte-root>.ssz_snappy` files.
valid: bool -- optional, default to `true`.
If it's `false`, this execution step is expected to be invalid.
}