From 2210cea734c625567cc481c9e9ea352d2ca3a327 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 1 Jun 2023 23:56:14 +0800 Subject: [PATCH 1/6] Add deneb fc tests and update test format --- .../test/deneb/fork_choice/__init__.py | 0 .../test/deneb/fork_choice/test_on_block.py | 105 ++++++++++++++++++ .../eth2spec/test/helpers/fork_choice.py | 78 +++++++++++-- .../test/phase0/fork_choice/test_get_head.py | 5 +- tests/formats/fork_choice/README.md | 38 ++++++- tests/generators/fork_choice/main.py | 8 +- 6 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 tests/core/pyspec/eth2spec/test/deneb/fork_choice/__init__.py create mode 100644 tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py diff --git a/tests/core/pyspec/eth2spec/test/deneb/fork_choice/__init__.py b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py new file mode 100644 index 000000000..85610675c --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py @@ -0,0 +1,105 @@ +from random import Random + +from eth2spec.test.context import ( + spec_state_test, + with_deneb_and_later, +) + +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, +) +from eth2spec.test.helpers.fork_choice import ( + BlobData, + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + tick_and_add_block, + with_blob_data, +) +from eth2spec.test.helpers.state import ( + state_transition_and_sign_block, +) +from eth2spec.test.helpers.sharding import ( + get_sample_opaque_tx +) + + +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_opaque_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) + block.body.blob_kzg_commitments = blob_kzg_commitments + return block, blobs, blob_kzg_proofs + + +@with_deneb_and_later +@spec_state_test +def test_simple_blob_data(spec, state): + 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 + block, blobs, blob_kzg_proofs = get_block_with_blob(spec, state, rng=rng) + signed_block = state_transition_and_sign_block(spec, state, block) + blob_data = BlobData(blobs, blob_kzg_proofs) + + def run_func_1(): + yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data) + + yield from with_blob_data(spec, blob_data, run_func_1) + + assert spec.get_head(store) == signed_block.message.hash_tree_root() + + # On receiving a block of next epoch + store.time = current_time + spec.config.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH + block, blobs, blob_kzg_proofs = get_block_with_blob(spec, state, rng=rng) + signed_block = state_transition_and_sign_block(spec, state, block) + blob_data = BlobData(blobs, blob_kzg_proofs) + + def run_func_2(): + yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data) + + yield from with_blob_data(spec, blob_data, run_func_2) + + assert spec.get_head(store) == signed_block.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_deneb_and_later +@spec_state_test +def test_invalid_incorrect_proof(spec, state): + 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 + block, blobs, blob_kzg_proofs = get_block_with_blob(spec, state, rng=rng) + signed_block = state_transition_and_sign_block(spec, state, block) + # Insert incorrect proof + blob_kzg_proofs = [b'\xc0' + b'\x00' * 47] + blob_data = BlobData(blobs, blob_kzg_proofs) + + def run_func_1(): + yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data, valid=False) + + yield from with_blob_data(spec, blob_data, run_func_1) + + assert spec.get_head(store) != signed_block.message.hash_tree_root() + + yield 'steps', test_steps diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index af231d87f..d73a9a01b 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -1,3 +1,5 @@ +from typing import NamedTuple, Sequence, Any + from eth_utils import encode_hex from eth2spec.test.exceptions import BlockNotFoundException from eth2spec.test.helpers.attestations import ( @@ -7,6 +9,33 @@ from eth2spec.test.helpers.attestations import ( ) +class BlobData(NamedTuple): + blobs: Sequence[Any] + proofs: Sequence[bytes] + + +def with_blob_data(spec, blob_data, func): + def retrieve_blobs_and_proofs(beacon_block_root): + return blob_data.blobs, blob_data.proofs + + retrieve_blobs_and_proofs_backup = spec.retrieve_blobs_and_proofs + spec.retrieve_blobs_and_proofs = retrieve_blobs_and_proofs + + 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_blobs_and_proofs = retrieve_blobs_and_proofs_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(): @@ -15,7 +44,8 @@ def get_anchor_root(spec, state): def tick_and_add_block(spec, store, signed_block, test_steps, valid=True, - merge_block=False, block_not_found=False, is_optimistic=False): + merge_block=False, block_not_found=False, is_optimistic=False, + blob_data=None): pre_state = store.block_states[signed_block.message.parent_root] if merge_block: assert spec.is_merge_transition_block(pre_state, signed_block.message.body) @@ -30,6 +60,7 @@ def tick_and_add_block(spec, store, signed_block, test_steps, valid=True, valid=valid, block_not_found=block_not_found, is_optimistic=is_optimistic, + blob_data=blob_data, ) return post_state @@ -94,6 +125,13 @@ def get_attester_slashing_file_name(attester_slashing): return f"attester_slashing_{encode_hex(attester_slashing.hash_tree_root())}" +def get_blobs_file_name(blobs=None, blobs_root=None): + if blobs: + return f"blobs_{encode_hex(blobs.hash_tree_root())}" + else: + return f"blobs_{encode_hex(blobs_root)}" + + def on_tick_and_append_step(spec, store, time, test_steps): spec.on_tick(store, time) test_steps.append({'tick': int(time)}) @@ -119,35 +157,53 @@ def add_block(spec, test_steps, valid=True, block_not_found=False, - is_optimistic=False): + is_optimistic=False, + blob_data=None): """ Run on_block and on_attestation """ yield get_block_file_name(signed_block), signed_block + # Check blob_data + if blob_data is not None: + assert len(blob_data.blobs) == len(blob_data.proofs) + blobs = spec.List[spec.Blob, spec.MAX_BLOBS_PER_BLOCK](blob_data.blobs) + 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 + + 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, + }) + else: + test_steps.append({ + 'block': get_block_file_name(signed_block), + 'valid': valid, + }) + if not valid: if is_optimistic: run_on_block(spec, store, signed_block, valid=True) - test_steps.append({ - 'block': get_block_file_name(signed_block), - 'valid': False, - }) + _append_step(is_blob_data_test, 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 - test_steps.append({ - 'block': get_block_file_name(signed_block), - 'valid': False, - }) + _append_step(is_blob_data_test, valid=False) return else: assert False else: run_on_block(spec, store, signed_block, valid=True) - test_steps.append({'block': get_block_file_name(signed_block)}) + _append_step(is_blob_data_test) # An on_block step implies receiving block's attestations for attestation in signed_block.message.body.attestations: diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py index 30f94b854..886fcbd20 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py @@ -34,9 +34,6 @@ from eth2spec.test.helpers.state import ( ) -rng = random.Random(1001) - - @with_altair_and_later @spec_state_test def test_genesis(spec, state): @@ -271,6 +268,7 @@ def test_proposer_boost_correct_head(spec, state): next_slots(spec, state_2, 2) block_2 = build_empty_block_for_next_slot(spec, state_2) signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) + rng = random.Random(1001) while spec.hash_tree_root(block_1) >= spec.hash_tree_root(block_2): block_2.body.graffiti = spec.Bytes32(hex(rng.getrandbits(8 * 32))[2:].zfill(64)) signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) @@ -339,6 +337,7 @@ def test_discard_equivocations_on_attester_slashing(spec, state): next_slots(spec, state_2, 2) block_2 = build_empty_block_for_next_slot(spec, state_2) signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) + rng = random.Random(1001) while spec.hash_tree_root(block_1) >= spec.hash_tree_root(block_2): block_2.body.graffiti = spec.Bytes32(hex(rng.getrandbits(8 * 32))[2:].zfill(64)) signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index 3b28837de..bfc8a423d 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -2,6 +2,30 @@ The aim of the fork choice tests is to provide test coverage of the various components of the fork choice. +## Table of contents + + + + +- [Test case format](#test-case-format) + - [`meta.yaml`](#metayaml) + - [`anchor_state.ssz_snappy`](#anchor_statessz_snappy) + - [`anchor_block.ssz_snappy`](#anchor_blockssz_snappy) + - [`steps.yaml`](#stepsyaml) + - [`on_tick` execution step](#on_tick-execution-step) + - [`on_attestation` execution step](#on_attestation-execution-step) + - [`on_block` execution step](#on_block-execution-step) + - [`on_merge_block` execution step](#on_merge_block-execution-step) + - [`on_attester_slashing` execution step](#on_attester_slashing-execution-step) + - [`on_payload_info` execution step](#on_payload_info-execution-step) + - [Checks step](#checks-step) + - [`attestation_<32-byte-root>.ssz_snappy`](#attestation_32-byte-rootssz_snappy) + - [`block_<32-byte-root>.ssz_snappy`](#block_32-byte-rootssz_snappy) +- [Condition](#condition) + + + + ## Test case format ### `meta.yaml` @@ -59,14 +83,20 @@ The parameter that is required for executing `on_block(store, block)`. ```yaml { - block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file. - To execute `on_block(store, block)` with the given attestation. - valid: bool -- optional, default to `true`. - If it's `false`, this execution step is expected to be invalid. + block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file. + To execute `on_block(store, block)` with the given attestation. + blobs: string -- optional, the name of the `blobs_<32-byte-root>.ssz_snappy` file. + The blobs file content is a `List[Blob, MAX_BLOBS_PER_BLOCK]` SSZ object. + proofs: array of byte48 hex string -- optional, the proofs of blob commitments. + valid: bool -- optional, default to `true`. + If it's `false`, this execution step is expected to be invalid. } ``` + The file is located in the same folder (see below). +`blobs` and `proofs` are new fields from Deneb EIP-4844. These are the expected values from `retrieve_blobs_and_proofs()` helper inside `is_data_available()` helper. + After this step, the `store` object may have been updated. #### `on_merge_block` execution step diff --git a/tests/generators/fork_choice/main.py b/tests/generators/fork_choice/main.py index b0c9a9bb9..7ff028cd8 100644 --- a/tests/generators/fork_choice/main.py +++ b/tests/generators/fork_choice/main.py @@ -19,7 +19,13 @@ if __name__ == "__main__": ]} bellatrix_mods = combine_mods(_new_bellatrix_mods, altair_mods) capella_mods = bellatrix_mods # No additional Capella specific fork choice tests - deneb_mods = capella_mods # No additional Deneb specific fork choice tests + + # Deneb adds `is_data_available` tests + _new_deneb_mods = {key: 'eth2spec.test.deneb.fork_choice.test_' + key for key in [ + 'on_block', + ]} + deneb_mods = combine_mods(_new_deneb_mods, capella_mods) + eip6110_mods = deneb_mods # No additional EIP6110 specific fork choice tests all_mods = { From d2d351f7c99a7ac8ab69e3f9ff706467bf7c9664 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 25 Jul 2023 23:30:31 +0800 Subject: [PATCH 2/6] Add `test_invalid_data_unavailable` --- .../test/deneb/fork_choice/test_on_block.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py index 85610675c..e1dc536c6 100644 --- a/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py +++ b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py @@ -89,7 +89,7 @@ def test_invalid_incorrect_proof(spec, state): assert store.time == current_time # On receiving a block of `GENESIS_SLOT + 1` slot - block, blobs, blob_kzg_proofs = get_block_with_blob(spec, state, rng=rng) + block, blobs, _ = get_block_with_blob(spec, state, rng=rng) signed_block = state_transition_and_sign_block(spec, state, block) # Insert incorrect proof blob_kzg_proofs = [b'\xc0' + b'\x00' * 47] @@ -103,3 +103,34 @@ def test_invalid_incorrect_proof(spec, state): assert spec.get_head(store) != signed_block.message.hash_tree_root() yield 'steps', test_steps + + +@with_deneb_and_later +@spec_state_test +def test_invalid_data_unavailable(spec, state): + 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 + block, _, _ = get_block_with_blob(spec, state, rng=rng) + signed_block = state_transition_and_sign_block(spec, state, block) + + # data unavailable + blob_data = BlobData([], []) + + def run_func_1(): + yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data, valid=False) + + yield from with_blob_data(spec, blob_data, run_func_1) + + assert spec.get_head(store) != signed_block.message.hash_tree_root() + + yield 'steps', test_steps From e79caff2f7db930fa6e164cab29a1817f61a26aa Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 25 Jul 2023 23:32:55 +0800 Subject: [PATCH 3/6] Clean up `is_data_available`. Remove the stub `retrieve_blobs_and_proofs` responses. --- pysetup/spec_builders/deneb.py | 4 ++-- specs/deneb/fork-choice.md | 5 ----- tests/core/pyspec/eth2spec/test/helpers/fork_choice.py | 7 +++++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pysetup/spec_builders/deneb.py b/pysetup/spec_builders/deneb.py index b4e180c2a..c32bee830 100644 --- a/pysetup/spec_builders/deneb.py +++ b/pysetup/spec_builders/deneb.py @@ -21,9 +21,9 @@ T = TypeVar('T') # For generic function @classmethod def sundry_functions(cls) -> str: return ''' -def retrieve_blobs_and_proofs(beacon_block_root: Root) -> PyUnion[Tuple[Blob, KZGProof], Tuple[str, str]]: +def retrieve_blobs_and_proofs(beacon_block_root: Root) -> Tuple[Sequence[Blob], Sequence[KZGProof]]: # pylint: disable=unused-argument - return ("TEST", "TEST")''' + return [], []''' @classmethod def execution_engine_cls(cls) -> str: diff --git a/specs/deneb/fork-choice.md b/specs/deneb/fork-choice.md index 23eef436c..2805fd146 100644 --- a/specs/deneb/fork-choice.md +++ b/specs/deneb/fork-choice.md @@ -55,11 +55,6 @@ def is_data_available(beacon_block_root: Root, blob_kzg_commitments: Sequence[KZ # `MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS` blobs, proofs = retrieve_blobs_and_proofs(beacon_block_root) - # For testing, `retrieve_blobs_and_proofs` returns ("TEST", "TEST"). - # TODO: Remove it once we have a way to inject `BlobSidecar` into tests. - if isinstance(blobs, str) or isinstance(proofs, str): - return True - return verify_blob_kzg_proof_batch(blobs, blob_kzg_commitments, proofs) ``` diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index d73a9a01b..c3f496a49 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -10,11 +10,18 @@ from eth2spec.test.helpers.attestations import ( class BlobData(NamedTuple): + """ + The return values of ``retrieve_blobs_and_proofs`` helper. + """ blobs: Sequence[Any] proofs: Sequence[bytes] def with_blob_data(spec, blob_data, 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): return blob_data.blobs, blob_data.proofs From 85b0ae854f6958c6c6815fdfd3d14cd052ee805a Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 27 Jul 2023 23:12:49 +0800 Subject: [PATCH 4/6] handle `len(blobs) == 0` case --- tests/core/pyspec/eth2spec/test/helpers/fork_choice.py | 10 ++++++---- tests/formats/fork_choice/README.md | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index c3f496a49..a6babefd3 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -173,10 +173,12 @@ def add_block(spec, # Check blob_data if blob_data is not None: - assert len(blob_data.blobs) == len(blob_data.proofs) blobs = spec.List[spec.Blob, spec.MAX_BLOBS_PER_BLOCK](blob_data.blobs) - blobs_root = blobs.hash_tree_root() - yield get_blobs_file_name(blobs_root=blobs_root), blobs + if len(blobs) > 0: + blobs_root = blobs.hash_tree_root() + yield get_blobs_file_name(blobs_root=blobs_root), blobs + else: + blobs_root = None is_blob_data_test = blob_data is not None @@ -184,7 +186,7 @@ def add_block(spec, if is_blob_data_test: test_steps.append({ 'block': get_block_file_name(signed_block), - 'blobs': get_blobs_file_name(blobs_root=blobs_root), + 'blobs': None if blobs_root is None else get_blobs_file_name(blobs_root=blobs_root), 'proofs': [encode_hex(proof) for proof in blob_data.proofs], 'valid': valid, }) diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index bfc8a423d..4347e95f2 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -85,8 +85,9 @@ The parameter that is required for executing `on_block(store, block)`. { block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file. To execute `on_block(store, block)` with the given attestation. - blobs: string -- optional, the name of the `blobs_<32-byte-root>.ssz_snappy` file. + blobs: string or `null` -- optional, the name of the `blobs_<32-byte-root>.ssz_snappy` file. The blobs file content is a `List[Blob, MAX_BLOBS_PER_BLOCK]` SSZ object. + If it's `null`, `blobs` is an empty list. proofs: array of byte48 hex string -- optional, the proofs of blob commitments. valid: bool -- optional, default to `true`. If it's `false`, this execution step is expected to be invalid. From 32056b2d44de3a4a3f41858f800bbabb01737252 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 31 Jul 2023 19:23:17 +0800 Subject: [PATCH 5/6] PR feedback from @djrtwo --- .../test/deneb/fork_choice/test_on_block.py | 86 ++++++++++++++----- .../eth2spec/test/helpers/fork_choice.py | 7 ++ 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py index e1dc536c6..12451f4ca 100644 --- a/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py +++ b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py @@ -8,18 +8,20 @@ from eth2spec.test.context import ( 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.fork_choice import ( BlobData, get_genesis_forkchoice_store_and_block, on_tick_and_append_step, - tick_and_add_block, - with_blob_data, + tick_and_add_block_with_data, ) from eth2spec.test.helpers.state import ( state_transition_and_sign_block, ) from eth2spec.test.helpers.sharding import ( - get_sample_opaque_tx + get_sample_opaque_tx, ) @@ -27,7 +29,7 @@ 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_opaque_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) + block.body.execution_payload.block_hash = compute_el_block_hash(spec, block.body.execution_payload) block.body.blob_kzg_commitments = blob_kzg_commitments return block, blobs, blob_kzg_proofs @@ -51,10 +53,7 @@ def test_simple_blob_data(spec, state): signed_block = state_transition_and_sign_block(spec, state, block) blob_data = BlobData(blobs, blob_kzg_proofs) - def run_func_1(): - yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data) - - yield from with_blob_data(spec, blob_data, run_func_1) + 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() @@ -64,10 +63,7 @@ def test_simple_blob_data(spec, state): signed_block = state_transition_and_sign_block(spec, state, block) blob_data = BlobData(blobs, blob_kzg_proofs) - def run_func_2(): - yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data) - - yield from with_blob_data(spec, blob_data, run_func_2) + 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() @@ -95,10 +91,7 @@ def test_invalid_incorrect_proof(spec, state): blob_kzg_proofs = [b'\xc0' + b'\x00' * 47] blob_data = BlobData(blobs, blob_kzg_proofs) - def run_func_1(): - yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data, valid=False) - - yield from with_blob_data(spec, blob_data, run_func_1) + 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() @@ -126,10 +119,63 @@ def test_invalid_data_unavailable(spec, state): # data unavailable blob_data = BlobData([], []) - def run_func_1(): - yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data, valid=False) - - yield from with_blob_data(spec, blob_data, run_func_1) + 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_deneb_and_later +@spec_state_test +def test_invalid_wrong_proofs_length(spec, state): + 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 + block, blobs, _ = get_block_with_blob(spec, state, rng=rng) + signed_block = state_transition_and_sign_block(spec, state, block) + + # unavailable proofs + blob_data = BlobData(blobs, []) + + 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_deneb_and_later +@spec_state_test +def test_invalid_wrong_blobs_length(spec, state): + 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 + block, _, blob_kzg_proofs = get_block_with_blob(spec, state, rng=rng) + signed_block = state_transition_and_sign_block(spec, state, block) + + # unavailable blobs + blob_data = BlobData([], 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() diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index a6babefd3..ba4f294bc 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -73,6 +73,13 @@ def tick_and_add_block(spec, store, signed_block, test_steps, valid=True, return post_state +def tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data, valid=True): + def run_func(): + yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data, valid=valid) + + yield from with_blob_data(spec, blob_data, run_func) + + def add_attestation(spec, store, attestation, test_steps, is_from_block=False): spec.on_attestation(store, attestation, is_from_block=is_from_block) yield get_attestation_file_name(attestation), attestation From 39134d594ae0b8f345ec51b95b6ebec713321915 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 31 Jul 2023 19:47:53 +0800 Subject: [PATCH 6/6] Change it back to allow empty `blobs` list file --- tests/core/pyspec/eth2spec/test/helpers/fork_choice.py | 9 +++------ tests/formats/fork_choice/README.md | 5 ++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index ba4f294bc..e0e354722 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -181,11 +181,8 @@ def add_block(spec, # Check blob_data if blob_data is not None: blobs = spec.List[spec.Blob, spec.MAX_BLOBS_PER_BLOCK](blob_data.blobs) - if len(blobs) > 0: - blobs_root = blobs.hash_tree_root() - yield get_blobs_file_name(blobs_root=blobs_root), blobs - else: - blobs_root = None + 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 @@ -193,7 +190,7 @@ def add_block(spec, if is_blob_data_test: test_steps.append({ 'block': get_block_file_name(signed_block), - 'blobs': None if blobs_root is None else get_blobs_file_name(blobs_root=blobs_root), + 'blobs': get_blobs_file_name(blobs_root=blobs_root), 'proofs': [encode_hex(proof) for proof in blob_data.proofs], 'valid': valid, }) diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index 4347e95f2..d23de865b 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -85,9 +85,8 @@ The parameter that is required for executing `on_block(store, block)`. { block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file. To execute `on_block(store, block)` with the given attestation. - blobs: string or `null` -- optional, the name of the `blobs_<32-byte-root>.ssz_snappy` file. + blobs: string -- optional, the name of the `blobs_<32-byte-root>.ssz_snappy` file. The blobs file content is a `List[Blob, MAX_BLOBS_PER_BLOCK]` SSZ object. - If it's `null`, `blobs` is an empty list. proofs: array of byte48 hex string -- optional, the proofs of blob commitments. valid: bool -- optional, default to `true`. If it's `false`, this execution step is expected to be invalid. @@ -96,7 +95,7 @@ The parameter that is required for executing `on_block(store, block)`. The file is located in the same folder (see below). -`blobs` and `proofs` are new fields from Deneb EIP-4844. These are the expected values from `retrieve_blobs_and_proofs()` helper inside `is_data_available()` helper. +`blobs` and `proofs` are new fields from Deneb EIP-4844. These fields indicate the expected values from `retrieve_blobs_and_proofs()` helper inside `is_data_available()` helper. If these two fields are not provided, `retrieve_blobs_and_proofs()` returns empty lists. After this step, the `store` object may have been updated.