-
Notifications
You must be signed in to change notification settings - Fork 997
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #962 from ethereum/ssz-static-testing
SSZ static testing [blocked by #960]
- Loading branch information
Showing
19 changed files
with
307 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# SSZ, generic tests | ||
|
||
This set of test-suites provides general testing for SSZ: | ||
to instantiate any container/list/vector/other type from binary data. | ||
|
||
Since SSZ is in a development-phase, not the full suite of features is covered yet. | ||
Note that these tests are based on the older SSZ package. | ||
The tests are still relevant, but limited in scope: | ||
more complex object encodings have changed since the original SSZ testing. | ||
|
||
A minimal but useful series of tests covering `uint` encoding and decoding is provided. | ||
This is a direct port of the older SSZ `uint` tests (minus outdated test cases). | ||
|
||
[uint test format](./uint.md). | ||
|
||
Note: the current phase-0 spec does not use larger uints, and uses byte vectors (fixed length) instead to represent roots etc. | ||
The exact uint lengths to support may be redefined in the future. | ||
|
||
Extension of the SSZ tests collection is planned, with an update to the new spec-maintained `minimal_ssz.py`, | ||
see CI/testing issues for progress tracking. |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# SSZ, static tests | ||
|
||
This set of test-suites provides static testing for SSZ: | ||
to instantiate just the known ETH-2.0 SSZ types from binary data. | ||
|
||
This series of tests is based on the spec-maintained `minimal_ssz.py`, i.e. fully consistent with the SSZ spec. | ||
|
||
Test format documentation can be found here: [core test format](./core.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Test format: SSZ static types | ||
|
||
The goal of this type is to provide clients with a solid reference how the known SSZ objects should be encoded. | ||
Each object described in the Phase-0 spec is covered. | ||
This is important, as many of the clients aiming to serialize/deserialize objects directly into structs/classes | ||
do not support (or have alternatives for) generic SSZ encoding/decoding. | ||
This test-format ensures these direct serializations are covered. | ||
|
||
## Test case format | ||
|
||
```yaml | ||
type_name: string -- string, object name, formatted as in spec. E.g. "BeaconBlock" | ||
value: dynamic -- the YAML-encoded value, of the type specified by type_name. | ||
serialized: bytes -- string, SSZ-serialized data, hex encoded, with prefix 0x | ||
root: bytes32 -- string, hash-tree-root of the value, hex encoded, with prefix 0x | ||
``` | ||
## Condition | ||
A test-runner can implement the following assertions: | ||
- Serialization: After parsing the `value`, SSZ-serialize it: the output should match `serialized` | ||
- Hash-tree-root: After parsing the `value`, Hash-tree-root it: the output should match `root` | ||
- Deserialization: SSZ-deserialize the `serialized` value, and see if it matches the parsed `value` |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# SSZ-static | ||
|
||
The purpose of this test-generator is to provide test-vectors for the most important applications of SSZ: | ||
the serialization and hashing of ETH 2.0 data types |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
from random import Random | ||
|
||
from eth2spec.debug import random_value, encode | ||
from eth2spec.phase0 import spec | ||
from eth2spec.utils.minimal_ssz import hash_tree_root, serialize | ||
from eth_utils import ( | ||
to_tuple, to_dict | ||
) | ||
from gen_base import gen_runner, gen_suite, gen_typing | ||
from preset_loader import loader | ||
|
||
MAX_BYTES_LENGTH = 100 | ||
MAX_LIST_LENGTH = 10 | ||
|
||
|
||
@to_dict | ||
def create_test_case(rng: Random, name: str, mode: random_value.RandomizationMode, chaos: bool): | ||
typ = spec.get_ssz_type_by_name(name) | ||
value = random_value.get_random_ssz_object(rng, typ, MAX_BYTES_LENGTH, MAX_LIST_LENGTH, mode, chaos) | ||
yield "type_name", name | ||
yield "value", encode.encode(value, typ) | ||
yield "serialized", '0x' + serialize(value).hex() | ||
yield "root", '0x' + hash_tree_root(value).hex() | ||
|
||
|
||
@to_tuple | ||
def ssz_static_cases(rng: Random, mode: random_value.RandomizationMode, chaos: bool, count: int): | ||
for type_name in spec.ssz_types: | ||
for i in range(count): | ||
yield create_test_case(rng, type_name, mode, chaos) | ||
|
||
|
||
def get_ssz_suite(seed: int, config_name: str, mode: random_value.RandomizationMode, chaos: bool, cases_if_random: int): | ||
def ssz_suite(configs_path: str) -> gen_typing.TestSuiteOutput: | ||
# Apply changes to presets, this affects some of the vector types. | ||
presets = loader.load_presets(configs_path, config_name) | ||
spec.apply_constants_preset(presets) | ||
|
||
# Reproducible RNG | ||
rng = Random(seed) | ||
|
||
random_mode_name = mode.to_name() | ||
|
||
suite_name = f"ssz_{config_name}_{random_mode_name}{'_chaos' if chaos else ''}" | ||
|
||
count = cases_if_random if chaos or mode.is_changing() else 1 | ||
print(f"generating SSZ-static suite ({count} cases per ssz type): {suite_name}") | ||
|
||
return (suite_name, "core", gen_suite.render_suite( | ||
title=f"ssz testing, with {config_name} config, randomized with mode {random_mode_name}{' and with chaos applied' if chaos else ''}", | ||
summary="Test suite for ssz serialization and hash-tree-root", | ||
forks_timeline="testing", | ||
forks=["phase0"], | ||
config=config_name, | ||
runner="ssz", | ||
handler="static", | ||
test_cases=ssz_static_cases(rng, mode, chaos, count))) | ||
|
||
return ssz_suite | ||
|
||
|
||
if __name__ == "__main__": | ||
# [(seed, config name, randomization mode, chaos on/off, cases_if_random)] | ||
settings = [] | ||
seed = 1 | ||
for mode in random_value.RandomizationMode: | ||
settings.append((seed, "minimal", mode, False, 30)) | ||
seed += 1 | ||
settings.append((seed, "minimal", random_value.RandomizationMode.mode_random, True, 30)) | ||
seed += 1 | ||
settings.append((seed, "mainnet", random_value.RandomizationMode.mode_random, False, 5)) | ||
seed += 1 | ||
|
||
print("Settings: %d, SSZ-types: %d" % (len(settings), len(spec.ssz_types))) | ||
|
||
gen_runner.run_generator("ssz_static", [ | ||
get_ssz_suite(seed, config_name, mode, chaos, cases_if_random) | ||
for (seed, config_name, mode, chaos, cases_if_random) in settings | ||
]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
eth-utils==1.4.1 | ||
../../test_libs/gen_helpers | ||
../../test_libs/config_helpers | ||
../../test_libs/pyspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
from random import Random | ||
from typing import Any | ||
from enum import Enum | ||
|
||
|
||
UINT_SIZES = [8, 16, 32, 64, 128, 256] | ||
|
||
basic_types = ["uint%d" % v for v in UINT_SIZES] + ['bool', 'byte'] | ||
|
||
random_mode_names = ["random", "zero", "max", "nil", "one", "lengthy"] | ||
|
||
|
||
class RandomizationMode(Enum): | ||
# random content / length | ||
mode_random = 0 | ||
# Zero-value | ||
mode_zero = 1 | ||
# Maximum value, limited to count 1 however | ||
mode_max = 2 | ||
# Return 0 values, i.e. empty | ||
mode_nil_count = 3 | ||
# Return 1 value, random content | ||
mode_one_count = 4 | ||
# Return max amount of values, random content | ||
mode_max_count = 5 | ||
|
||
def to_name(self): | ||
return random_mode_names[self.value] | ||
|
||
def is_changing(self): | ||
return self.value in [0, 4, 5] | ||
|
||
|
||
def get_random_ssz_object(rng: Random, typ: Any, max_bytes_length: int, max_list_length: int, mode: RandomizationMode, chaos: bool) -> Any: | ||
""" | ||
Create an object for a given type, filled with random data. | ||
:param rng: The random number generator to use. | ||
:param typ: The type to instantiate | ||
:param max_bytes_length: the max. length for a random bytes array | ||
:param max_list_length: the max. length for a random list | ||
:param mode: how to randomize | ||
:param chaos: if true, the randomization-mode will be randomly changed | ||
:return: the random object instance, of the given type. | ||
""" | ||
if chaos: | ||
mode = rng.choice(list(RandomizationMode)) | ||
if isinstance(typ, str): | ||
# Bytes array | ||
if typ == 'bytes': | ||
if mode == RandomizationMode.mode_nil_count: | ||
return b'' | ||
if mode == RandomizationMode.mode_max_count: | ||
return get_random_bytes_list(rng, max_bytes_length) | ||
if mode == RandomizationMode.mode_one_count: | ||
return get_random_bytes_list(rng, 1) | ||
if mode == RandomizationMode.mode_zero: | ||
return b'\x00' | ||
if mode == RandomizationMode.mode_max: | ||
return b'\xff' | ||
return get_random_bytes_list(rng, rng.randint(0, max_bytes_length)) | ||
elif typ[:5] == 'bytes' and len(typ) > 5: | ||
length = int(typ[5:]) | ||
# Sanity, don't generate absurdly big random values | ||
# If a client is aiming to performance-test, they should create a benchmark suite. | ||
assert length <= max_bytes_length | ||
if mode == RandomizationMode.mode_zero: | ||
return b'\x00' * length | ||
if mode == RandomizationMode.mode_max: | ||
return b'\xff' * length | ||
return get_random_bytes_list(rng, length) | ||
# Basic types | ||
else: | ||
if mode == RandomizationMode.mode_zero: | ||
return get_min_basic_value(typ) | ||
if mode == RandomizationMode.mode_max: | ||
return get_max_basic_value(typ) | ||
return get_random_basic_value(rng, typ) | ||
# Vector: | ||
elif isinstance(typ, list) and len(typ) == 2: | ||
return [get_random_ssz_object(rng, typ[0], max_bytes_length, max_list_length, mode, chaos) for _ in range(typ[1])] | ||
# List: | ||
elif isinstance(typ, list) and len(typ) == 1: | ||
length = rng.randint(0, max_list_length) | ||
if mode == RandomizationMode.mode_one_count: | ||
length = 1 | ||
if mode == RandomizationMode.mode_max_count: | ||
length = max_list_length | ||
return [get_random_ssz_object(rng, typ[0], max_bytes_length, max_list_length, mode, chaos) for _ in range(length)] | ||
# Container: | ||
elif hasattr(typ, 'fields'): | ||
return typ(**{field: get_random_ssz_object(rng, subtype, max_bytes_length, max_list_length, mode, chaos) for field, subtype in typ.fields.items()}) | ||
else: | ||
print(typ) | ||
raise Exception("Type not recognized") | ||
|
||
|
||
def get_random_bytes_list(rng: Random, length: int) -> bytes: | ||
return bytes(rng.getrandbits(8) for _ in range(length)) | ||
|
||
|
||
def get_random_basic_value(rng: Random, typ: str) -> Any: | ||
if typ == 'bool': | ||
return rng.choice((True, False)) | ||
if typ[:4] == 'uint': | ||
size = int(typ[4:]) | ||
assert size in UINT_SIZES | ||
return rng.randint(0, 2**size - 1) | ||
if typ == 'byte': | ||
return rng.randint(0, 8) | ||
else: | ||
raise ValueError("Not a basic type") | ||
|
||
|
||
def get_min_basic_value(typ: str) -> Any: | ||
if typ == 'bool': | ||
return False | ||
if typ[:4] == 'uint': | ||
size = int(typ[4:]) | ||
assert size in UINT_SIZES | ||
return 0 | ||
if typ == 'byte': | ||
return 0x00 | ||
else: | ||
raise ValueError("Not a basic type") | ||
|
||
|
||
def get_max_basic_value(typ: str) -> Any: | ||
if typ == 'bool': | ||
return True | ||
if typ[:4] == 'uint': | ||
size = int(typ[4:]) | ||
assert size in UINT_SIZES | ||
return 2**size - 1 | ||
if typ == 'byte': | ||
return 0xff | ||
else: | ||
raise ValueError("Not a basic type") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters