diff --git a/test_generators/shuffling/constants.py b/test_generators/shuffling/constants.py deleted file mode 100644 index 92862f898..000000000 --- a/test_generators/shuffling/constants.py +++ /dev/null @@ -1,6 +0,0 @@ -SLOTS_PER_EPOCH = 2**6 # 64 slots, 6.4 minutes -FAR_FUTURE_EPOCH = 2**64 - 1 # uint64 max -SHARD_COUNT = 2**10 # 1024 -TARGET_COMMITTEE_SIZE = 2**7 # 128 validators -ACTIVATION_EXIT_DELAY = 2**2 # 4 epochs -SHUFFLE_ROUND_COUNT = 90 diff --git a/test_generators/shuffling/core_helpers.py b/test_generators/shuffling/core_helpers.py deleted file mode 100644 index c424b771e..000000000 --- a/test_generators/shuffling/core_helpers.py +++ /dev/null @@ -1,95 +0,0 @@ -from typing import Any, List, NewType - -from constants import SLOTS_PER_EPOCH, SHARD_COUNT, TARGET_COMMITTEE_SIZE, SHUFFLE_ROUND_COUNT -from utils import hash -from yaml_objects import Validator - -Epoch = NewType("Epoch", int) -ValidatorIndex = NewType("ValidatorIndex", int) -Bytes32 = NewType("Bytes32", bytes) - - -def int_to_bytes1(x): - return x.to_bytes(1, 'little') - - -def int_to_bytes4(x): - return x.to_bytes(4, 'little') - - -def bytes_to_int(data: bytes) -> int: - return int.from_bytes(data, 'little') - - -def is_active_validator(validator: Validator, epoch: Epoch) -> bool: - """ - Check if ``validator`` is active. - """ - return validator.activation_epoch <= epoch < validator.exit_epoch - - -def get_active_validator_indices(validators: List[Validator], epoch: Epoch) -> List[ValidatorIndex]: - """ - Get indices of active validators from ``validators``. - """ - return [i for i, v in enumerate(validators) if is_active_validator(v, epoch)] - - -def split(values: List[Any], split_count: int) -> List[List[Any]]: - """ - Splits ``values`` into ``split_count`` pieces. - """ - list_length = len(values) - return [ - values[(list_length * i // split_count): (list_length * (i + 1) // split_count)] - for i in range(split_count) - ] - - -def get_epoch_committee_count(active_validator_count: int) -> int: - """ - Return the number of committees in one epoch. - """ - return max( - 1, - min( - SHARD_COUNT // SLOTS_PER_EPOCH, - active_validator_count // SLOTS_PER_EPOCH // TARGET_COMMITTEE_SIZE, - ) - ) * SLOTS_PER_EPOCH - - -def get_permuted_index(index: int, list_size: int, seed: Bytes32) -> int: - """ - Return `p(index)` in a pseudorandom permutation `p` of `0...list_size-1` with ``seed`` as entropy. - - Utilizes 'swap or not' shuffling found in - https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf - See the 'generalized domain' algorithm on page 3. - """ - for round in range(SHUFFLE_ROUND_COUNT): - pivot = bytes_to_int(hash(seed + int_to_bytes1(round))[0:8]) % list_size - flip = (pivot - index) % list_size - position = max(index, flip) - source = hash(seed + int_to_bytes1(round) + int_to_bytes4(position // 256)) - byte = source[(position % 256) // 8] - bit = (byte >> (position % 8)) % 2 - index = flip if bit else index - - return index - - -def get_shuffling(seed: Bytes32, - validators: List[Validator], - epoch: Epoch) -> List[List[ValidatorIndex]]: - """ - Shuffle active validators and split into crosslink committees. - Return a list of committees (each a list of validator indices). - """ - # Shuffle active validator indices - active_validator_indices = get_active_validator_indices(validators, epoch) - length = len(active_validator_indices) - shuffled_indices = [active_validator_indices[get_permuted_index(i, length, seed)] for i in range(length)] - - # Split the shuffled active validator indices - return split(shuffled_indices, get_epoch_committee_count(length)) diff --git a/test_generators/shuffling/main.py b/test_generators/shuffling/main.py index 03352944a..e2edff7c7 100644 --- a/test_generators/shuffling/main.py +++ b/test_generators/shuffling/main.py @@ -1,160 +1,127 @@ import random -import sys -import os -import yaml - -from constants import ACTIVATION_EXIT_DELAY, FAR_FUTURE_EPOCH -from core_helpers import get_shuffling -from yaml_objects import Validator +from eth2spec.phase0.spec import * +from eth_utils import ( + to_dict, to_tuple +) +from gen_base import gen_runner, gen_suite, gen_typing +from preset_loader import loader -def noop(self, *args, **kw): - # Prevent !!str or !!binary tags - pass +@to_dict +def active_exited_validator_case(idx_max: int): + validators = [] + # Standard deviation, around 8% validators will activate or exit within + # ENTRY_EXIT_DELAY inclusive from EPOCH thus creating an edge case for validator + # shuffling + RAND_EPOCH_STD = 35 -yaml.emitter.Emitter.process_tag = noop + # TODO: fix epoch numbers + + slot = 1000 * SLOTS_PER_EPOCH + # The epoch, also a mean for the normal distribution + epoch = slot_to_epoch(slot) + MAX_EXIT_EPOCH = epoch + 5000 # Maximum exit_epoch for easier reading + for idx in range(idx_max): + v = Validator( + pubkey=bytes(random.randint(0, 255) for _ in range(48)), + withdrawal_credentials=bytes(random.randint(0, 255) for _ in range(32)), + activation_epoch=FAR_FUTURE_EPOCH, + exit_epoch=FAR_FUTURE_EPOCH, + withdrawable_epoch=FAR_FUTURE_EPOCH, + initiated_exit=False, + slashed=False, + high_balance=0 + ) + # 4/5 of all validators are active + if random.random() < 0.8: + # Choose a normally distributed epoch number + rand_epoch = round(random.gauss(epoch, RAND_EPOCH_STD)) -EPOCH = 1000 # The epoch, also a mean for the normal distribution + # for 1/2 of *active* validators rand_epoch is the activation epoch + if random.random() < 0.5: + v.activation_epoch = rand_epoch -# Standard deviation, around 8% validators will activate or exit within -# ENTRY_EXIT_DELAY inclusive from EPOCH thus creating an edge case for validator -# shuffling -RAND_EPOCH_STD = 35 -MAX_EXIT_EPOCH = 5000 # Maximum exit_epoch for easier reading - - -def active_exited_validators_generator(): - """ - Random cases with variety of validator's activity status - """ - # Order not preserved - https://github.com/yaml/pyyaml/issues/110 - metadata = { - 'title': 'Shuffling Algorithm Tests 1', - 'summary': 'Test vectors for validator shuffling with different validator\'s activity status.' - ' Note: only relevant validator fields are defined.', - 'test_suite': 'shuffle', - 'fork': 'phase0-0.5.0', - } - - # Config - random.seed(int("0xEF00BEAC", 16)) - num_cases = 10 - - test_cases = [] - - for case in range(num_cases): - seedhash = bytes(random.randint(0, 255) for byte in range(32)) - idx_max = random.randint(128, 512) - - validators = [] - for idx in range(idx_max): - v = Validator(original_index=idx) - # 4/5 of all validators are active - if random.random() < 0.8: - # Choose a normally distributed epoch number - rand_epoch = round(random.gauss(EPOCH, RAND_EPOCH_STD)) - - # for 1/2 of *active* validators rand_epoch is the activation epoch + # 1/4 of active validators will exit in forseeable future if random.random() < 0.5: - v.activation_epoch = rand_epoch - - # 1/4 of active validators will exit in forseeable future - if random.random() < 0.5: - v.exit_epoch = random.randint( - rand_epoch + ACTIVATION_EXIT_DELAY + 1, MAX_EXIT_EPOCH) - # 1/4 of active validators in theory remain in the set indefinitely - else: - v.exit_epoch = FAR_FUTURE_EPOCH - # for the other active 1/2 rand_epoch is the exit epoch + v.exit_epoch = random.randint( + rand_epoch + ACTIVATION_EXIT_DELAY + 1, MAX_EXIT_EPOCH) + # 1/4 of active validators in theory remain in the set indefinitely else: - v.activation_epoch = random.randint( - 0, rand_epoch - ACTIVATION_EXIT_DELAY) - v.exit_epoch = rand_epoch - - # The remaining 1/5 of all validators is not activated + v.exit_epoch = FAR_FUTURE_EPOCH + # for the other active 1/2 rand_epoch is the exit epoch else: - v.activation_epoch = FAR_FUTURE_EPOCH - v.exit_epoch = FAR_FUTURE_EPOCH + v.activation_epoch = random.randint( + 0, rand_epoch - ACTIVATION_EXIT_DELAY) + v.exit_epoch = rand_epoch - validators.append(v) - - input_ = { - 'validators': validators, - 'epoch': EPOCH - } - output = get_shuffling( - seedhash, validators, input_['epoch']) - - test_cases.append({ - 'seed': '0x' + seedhash.hex(), 'input': input_, 'output': output - }) - - return { - 'metadata': metadata, - 'filename': 'test_vector_shuffling.yml', - 'test_cases': test_cases - } - - -def validators_set_size_variety_generator(): - """ - Different validator set size cases, inspired by removed manual `permutated_index` tests - https://github.com/ethereum/eth2.0-test-generators/tree/bcd9ab2933d9f696901d1dfda0828061e9d3093f/permutated_index - """ - # Order not preserved - https://github.com/yaml/pyyaml/issues/110 - metadata = { - 'title': 'Shuffling Algorithm Tests 2', - 'summary': 'Test vectors for validator shuffling with different validator\'s set size.' - ' Note: only relevant validator fields are defined.', - 'test_suite': 'shuffle', - 'fork': 'tchaikovsky', - 'version': 1.0 - } - - # Config - random.seed(int("0xEF00BEAC", 16)) - - test_cases = [] - - seedhash = bytes(random.randint(0, 255) for byte in range(32)) - idx_max = 4096 - set_sizes = [1, 2, 3, 1024, idx_max] - - for size in set_sizes: - validators = [] - for idx in range(size): - v = Validator(original_index=idx) - v.activation_epoch = EPOCH + # The remaining 1/5 of all validators is not activated + else: + v.activation_epoch = FAR_FUTURE_EPOCH v.exit_epoch = FAR_FUTURE_EPOCH - validators.append(v) - input_ = { - 'validators': validators, - 'epoch': EPOCH - } - output = get_shuffling( - seedhash, validators, input_['epoch']) - test_cases.append({ - 'seed': '0x' + seedhash.hex(), 'input': input_, 'output': output - }) + validators.append(v) - return { - 'metadata': metadata, - 'filename': 'shuffling_set_size.yml', - 'test_cases': test_cases - } + query_slot = slot + random.randint(-1, 1) + state = get_genesis_beacon_state([], 0, None) + state.validator_registry = validators + state.latest_randao_mixes = [b'\xde\xad\xbe\xef' * 8 for _ in range(LATEST_RANDAO_MIXES_LENGTH)] + state.slot = slot + state.latest_start_shard = random.randint(0, SHARD_COUNT - 1) + randao_mix = bytes(random.randint(0, 255) for _ in range(32)) + state.latest_randao_mixes[slot_to_epoch(query_slot) % LATEST_RANDAO_MIXES_LENGTH] = randao_mix + + committees = get_crosslink_committees_at_slot(state, query_slot) + yield 'validator_registry', [ + { + 'activation_epoch': v.activation_epoch, + 'exit_epoch': v.exit_epoch + } for v in state.validator_registry + ] + yield 'randao_mix', '0x'+randao_mix.hex() + yield 'state_slot', state.slot + yield 'query_slot', query_slot + yield 'latest_start_shard', state.latest_start_shard + yield 'crosslink_committees', committees -if __name__ == '__main__': - output_dir = sys.argv[2] - for generator in [active_exited_validators_generator, validators_set_size_variety_generator]: - result = generator() - filename = os.path.join(output_dir, result['filename']) - with open(filename, 'w') as outfile: - # Dump at top level - yaml.dump(result['metadata'], outfile, default_flow_style=False) - # default_flow_style will unravel "ValidatorRecord" and "committee" line, exploding file size - yaml.dump({'test_cases': result['test_cases']}, outfile) +@to_tuple +def active_exited_validator_cases(): + for i in range(3): + yield active_exited_validator_case(random.randint(100, min(200, SHARD_COUNT * 2))) + + +def mini_shuffling_suite(configs_path: str) -> gen_typing.TestSuiteOutput: + presets = loader.load_presets(configs_path, 'minimal') + apply_constants_preset(presets) + + return ("shuffling_minimal", "core", gen_suite.render_suite( + title="Shuffling Algorithm Tests with minimal config", + summary="Test vectors for validator shuffling with different validator registry activity status and set size." + " Note: only relevant fields are defined.", + forks_timeline="testing", + forks=["phase0"], + config="minimal", + handler="core", + test_cases=active_exited_validator_cases())) + + +def full_shuffling_suite(configs_path: str) -> gen_typing.TestSuiteOutput: + presets = loader.load_presets(configs_path, 'mainnet') + apply_constants_preset(presets) + + return ("shuffling_full", "core", gen_suite.render_suite( + title="Shuffling Algorithm Tests with mainnet config", + summary="Test vectors for validator shuffling with different validator registry activity status and set size." + " Note: only relevant fields are defined.", + forks_timeline="mainnet", + forks=["phase0"], + config="mainnet", + handler="core", + test_cases=active_exited_validator_cases())) + + +if __name__ == "__main__": + gen_runner.run_generator("shuffling", [mini_shuffling_suite, full_shuffling_suite]) diff --git a/test_generators/shuffling/requirements.txt b/test_generators/shuffling/requirements.txt index dde2fb67d..8f9bede8f 100644 --- a/test_generators/shuffling/requirements.txt +++ b/test_generators/shuffling/requirements.txt @@ -1,4 +1,4 @@ -eth-hash[pycryptodome]==0.2.0 -eth-typing==2.0.0 eth-utils==1.4.1 -PyYAML==4.2b1 +../../test_libs/gen_helpers +../../test_libs/config_helpers +../../test_libs/pyspec \ No newline at end of file diff --git a/test_generators/shuffling/utils.py b/test_generators/shuffling/utils.py deleted file mode 100644 index bcd2c6a3c..000000000 --- a/test_generators/shuffling/utils.py +++ /dev/null @@ -1,6 +0,0 @@ -from eth_typing import Hash32 -from eth_utils import keccak - - -def hash(x: bytes) -> Hash32: - return keccak(x) diff --git a/test_generators/shuffling/yaml_objects.py b/test_generators/shuffling/yaml_objects.py deleted file mode 100644 index 18e45220e..000000000 --- a/test_generators/shuffling/yaml_objects.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Any - -import yaml - - -class Validator(yaml.YAMLObject): - """ - A validator stub containing only the fields relevant for get_shuffling() - """ - fields = { - 'activation_epoch': 'uint64', - 'exit_epoch': 'uint64', - # Extra index field to ease testing/debugging - 'original_index': 'uint64', - } - - def __init__(self, **kwargs): - for k in self.fields.keys(): - setattr(self, k, kwargs.get(k)) - - def __setattr__(self, name: str, value: Any) -> None: - super().__setattr__(name, value) - - def __getattribute__(self, name: str) -> Any: - return super().__getattribute__(name)