├── .circleci └── config.yml ├── .gitignore ├── Makefile ├── README.md ├── bls ├── README.md ├── requirements.txt └── tgen_bls.py ├── shuffling ├── README.md ├── constants.py ├── core_helpers.py ├── requirements.txt ├── tgen_shuffling.py ├── utils.py └── yaml_objects.py └── ssz ├── __init__.py ├── renderers.py ├── requirements.txt ├── test_generator.py └── uint_test_generators.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | 5 | build: 6 | docker: 7 | - image: circleci/python:3.6 8 | steps: 9 | - checkout 10 | - run: 11 | name: Generate tests 12 | command: make all 13 | - run: 14 | name: Save tests for deployment 15 | command: | 16 | mkdir /tmp/workspace 17 | cp -r tests /tmp/workspace/ 18 | git log -1 >> /tmp/workspace/latest_commit_message 19 | 20 | - persist_to_workspace: 21 | root: /tmp/workspace 22 | paths: 23 | - tests 24 | - latest_commit_message 25 | 26 | commit: 27 | docker: 28 | - image: circleci/python:3.6 29 | steps: 30 | - attach_workspace: 31 | at: /tmp/workspace 32 | - add_ssh_keys: 33 | fingerprints: 34 | - "01:85:b6:36:96:a6:84:72:e4:9b:4e:38:ee:21:97:fa" 35 | - run: 36 | name: Checkout test repository 37 | command: | 38 | ssh-keyscan -H github.com >> ~/.ssh/known_hosts 39 | git clone git@github.com:ethereum/eth2.0-tests.git 40 | - run: 41 | name: Commit and push generated tests 42 | command: | 43 | cd eth2.0-tests 44 | 45 | git config user.name 'eth2TestGenBot' 46 | git config user.email '47188154+eth2TestGenBot@users.noreply.github.com' 47 | 48 | for filename in /tmp/workspace/tests/*; do 49 | rm -rf $(basename $filename) 50 | cp -r $filename . 51 | done 52 | git add . 53 | 54 | if git diff --cached --exit-code >& /dev/null; then 55 | echo "No changes to commit" 56 | else 57 | echo -e "Update generated tests\n\nLatest commit message from eth2.0-test-generators:\n" > commit_message 58 | cat /tmp/workspace/latest_commit_message >> commit_message 59 | git commit -F commit_message 60 | 61 | git push origin master 62 | fi 63 | 64 | 65 | workflows: 66 | version: 2.1 67 | 68 | build_and_commit: 69 | jobs: 70 | - build: 71 | filters: 72 | tags: 73 | only: /.*/ 74 | - commit: 75 | requires: 76 | - build 77 | filters: 78 | tags: 79 | only: /.*/ 80 | branches: 81 | ignore: /.*/ 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # General 3 | # 4 | tests/ 5 | .venvs 6 | 7 | 8 | # 9 | # Python 10 | # 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GENERATOR_DIR = . 2 | TEST_DIR = ./tests 3 | VENV_DIR = ./.venvs 4 | 5 | 6 | .PHONY: clean all 7 | 8 | 9 | all: $(TEST_DIR) $(TEST_DIR)/shuffling $(TEST_DIR)/bls $(TEST_DIR)/ssz 10 | 11 | 12 | clean: 13 | rm -rf $(TEST_DIR) 14 | rm -rf $(VENV_DIR) 15 | 16 | 17 | $(TEST_DIR): 18 | mkdir -p $@ 19 | 20 | 21 | # 22 | # test generators 23 | # 24 | 25 | $(TEST_DIR)/shuffling: 26 | mkdir -p $@ 27 | 28 | python3 -m venv $(VENV_DIR)/shuffling 29 | . $(VENV_DIR)/shuffling/bin/activate 30 | pip3 install -r $(GENERATOR_DIR)/shuffling/requirements.txt --user 31 | 32 | python3 $(GENERATOR_DIR)/shuffling/tgen_shuffling.py $@ 33 | 34 | 35 | $(TEST_DIR)/bls: 36 | mkdir -p $@ 37 | 38 | python3 -m venv $(VENV_DIR)/bls 39 | . $(VENV_DIR)/bls/bin/activate 40 | pip3 install -r $(GENERATOR_DIR)/bls/requirements.txt --user 41 | 42 | python3 $(GENERATOR_DIR)/bls/tgen_bls.py $@/test_bls.yml 43 | 44 | 45 | $(TEST_DIR)/ssz: 46 | mkdir -p $@ 47 | 48 | python3 -m venv $(VENV_DIR)/ssz 49 | . $(VENV_DIR)/ssz/bin/activate 50 | pip3 install -r $(GENERATOR_DIR)/ssz/requirements.txt --user 51 | 52 | python3 $(GENERATOR_DIR)/ssz/test_generator.py -o $@ 53 | 54 | 55 | # Example: 56 | # 57 | # $(TEST_DIR)/test-test: 58 | # mkdir -p $@ 59 | # $(GENERATOR_DIR)/test-test/generate $@/test.yaml 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eth2.0 Test Generators 2 | 3 | This repository contains generators that build tests for Eth 2.0 clients. The test files themselves can be found in [ethereum/eth2.0-tests](https://github.com/ethereum/eth2.0-tests/). 4 | 5 | Whenever a release is made, the new tests are automatically built and [eth2TestGenBot](https://github.com/eth2TestGenBot) commits the changes to the test repository. 6 | 7 | ## How to add a new test generator 8 | 9 | In order to add a new test generator that builds `New Tests`, put it in a new directory `new_tests` at the root of this repository. Next, add a new target `$(TEST_DIR)/new_tests` to the [makefile](https://github.com/ethereum/eth2.0-test-generators/blob/master/Makefile), specifying the commands that build the test files. Note that `new_tests` is also the name of the directory in which the tests will appear in the tests repository later. Also, add the new target as a dependency to the `all` target. Finally, add any linting or testing commands to the [circleci config file](https://github.com/ethereum/eth2.0-test-generators/blob/master/.circleci/config.yml) if desired to increase code quality. All of this should be done in a pull request to the master branch. 10 | 11 | To deploy new tests to the testing repository, create a release tag with a new version number on Github. Increment the major version to indicate a change in the general testing format or the minor version if a new test generator has been added. Otherwise, just increment the patch version. 12 | 13 | ## How to remove a test generator 14 | 15 | If a test generator is not needed anymore, undo the steps described above and make a new release. In addition, remove the generated tests in the `eth2.0-tests` repository by opening a PR there. 16 | -------------------------------------------------------------------------------- /bls/README.md: -------------------------------------------------------------------------------- 1 | # BLS Test Generator 2 | 3 | Explanation of BLS12-381 type hierarchy 4 | The base unit is bytes48 of which only 381 bits are used 5 | 6 | - FQ: uint381 modulo field modulus 7 | - FQ2: (FQ, FQ) 8 | - G2: (FQ2, FQ2, FQ2) 9 | 10 | ## Resources 11 | 12 | - [Eth2.0 spec](https://github.com/ethereum/eth2.0-specs/blob/master/specs/bls_signature.md) 13 | - [Finite Field Arithmetic](http://www.springeronline.com/sgw/cda/pageitems/document/cda_downloaddocument/0,11996,0-0-45-110359-0,00.pdf) 14 | - Chapter 2 of [Elliptic Curve Cryptography](http://cacr.uwaterloo.ca/ecc/). Darrel Hankerson, Alfred Menezes, and Scott Vanstone 15 | - [Zcash BLS parameters](https://github.com/zkcrypto/pairing/tree/master/src/bls12_381) 16 | - [Trinity implementation](https://github.com/ethereum/trinity/blob/master/eth2/_utils/bls.py) 17 | 18 | ## Comments 19 | 20 | Compared to Zcash, Ethereum specs always requires the compressed form (c_flag / most significant bit always set). -------------------------------------------------------------------------------- /bls/requirements.txt: -------------------------------------------------------------------------------- 1 | py-ecc==1.6.0 2 | PyYAML==4.2b1 3 | -------------------------------------------------------------------------------- /bls/tgen_bls.py: -------------------------------------------------------------------------------- 1 | """ 2 | BLS test vectors generator 3 | Usage: 4 | "python tgen_bls path/to/output.yml" 5 | """ 6 | 7 | # Standard library 8 | import sys 9 | from typing import Tuple 10 | 11 | # Third-party 12 | import yaml 13 | 14 | # Ethereum 15 | from eth_utils import int_to_big_endian, big_endian_to_int 16 | 17 | # Local imports 18 | from py_ecc import bls 19 | 20 | 21 | def int_to_hex(n: int) -> str: 22 | return '0x' + int_to_big_endian(n).hex() 23 | 24 | 25 | def hex_to_int(x: str) -> int: 26 | return int(x, 16) 27 | 28 | 29 | # Note: even though a domain is only an uint64, 30 | # To avoid issues with YAML parsers that are limited to 53-bit (JS language limit) 31 | # It is serialized as an hex string as well. 32 | DOMAINS = [ 33 | 0, 34 | 1, 35 | 1234, 36 | 2**32-1, 37 | 2**64-1 38 | ] 39 | 40 | MESSAGES = [ 41 | b'\x00' * 32, 42 | b'\x56' * 32, 43 | b'\xab' * 32, 44 | ] 45 | 46 | PRIVKEYS = [ 47 | # Curve order is 256 so private keys are 32 bytes at most. 48 | # Also not all integers is a valid private key, so using pre-generated keys 49 | hex_to_int('0x00000000000000000000000000000000263dbd792f5b1be47ed85f8938c0f29586af0d3ac7b977f21c278fe1462040e3'), 50 | hex_to_int('0x0000000000000000000000000000000047b8192d77bf871b62e87859d653922725724a5c031afeabc60bcef5ff665138'), 51 | hex_to_int('0x00000000000000000000000000000000328388aff0d4a5b7dc9205abd374e7e98f3cd9f3418edb4eafda5fb16473d216'), 52 | ] 53 | 54 | 55 | def hash_message(msg: bytes, 56 | domain: int) ->Tuple[Tuple[str, str], Tuple[str, str], Tuple[str, str]]: 57 | """ 58 | Hash message 59 | Input: 60 | - Message as bytes 61 | - domain as uint64 62 | Output: 63 | - Message hash as a G2 point 64 | """ 65 | return [ 66 | [ 67 | int_to_hex(fq2.coeffs[0]), 68 | int_to_hex(fq2.coeffs[1]), 69 | ] 70 | for fq2 in bls.utils.hash_to_G2(msg, domain) 71 | ] 72 | 73 | 74 | def hash_message_compressed(msg: bytes, domain: int) -> Tuple[str, str]: 75 | """ 76 | Hash message 77 | Input: 78 | - Message as bytes 79 | - domain as uint64 80 | Output: 81 | - Message hash as a compressed G2 point 82 | """ 83 | z1, z2 = bls.utils.compress_G2(bls.utils.hash_to_G2(msg, domain)) 84 | return [int_to_hex(z1), int_to_hex(z2)] 85 | 86 | 87 | if __name__ == '__main__': 88 | 89 | # Order not preserved - https://github.com/yaml/pyyaml/issues/110 90 | metadata = { 91 | 'title': 'BLS signature and aggregation tests', 92 | 'summary': 'Test vectors for BLS signature', 93 | 'test_suite': 'bls', 94 | 'fork': 'phase0-0.5.0', 95 | } 96 | 97 | case01_message_hash_G2_uncompressed = [] 98 | for msg in MESSAGES: 99 | for domain in DOMAINS: 100 | case01_message_hash_G2_uncompressed.append({ 101 | 'input': {'message': '0x' + msg.hex(), 'domain': int_to_hex(domain)}, 102 | 'output': hash_message(msg, domain) 103 | }) 104 | 105 | case02_message_hash_G2_compressed = [] 106 | for msg in MESSAGES: 107 | for domain in DOMAINS: 108 | case02_message_hash_G2_compressed.append({ 109 | 'input': {'message': '0x' + msg.hex(), 'domain': int_to_hex(domain)}, 110 | 'output': hash_message_compressed(msg, domain) 111 | }) 112 | 113 | case03_private_to_public_key = [] 114 | #  Used in later cases 115 | pubkeys = [bls.privtopub(privkey) for privkey in PRIVKEYS] 116 | #  Used in public key aggregation 117 | pubkeys_serial = ['0x' + pubkey.hex() for pubkey in pubkeys] 118 | case03_private_to_public_key = [ 119 | { 120 | 'input': int_to_hex(privkey), 121 | 'output': pubkey_serial, 122 | } 123 | for privkey, pubkey_serial in zip(PRIVKEYS, pubkeys_serial) 124 | ] 125 | 126 | case04_sign_messages = [] 127 | sigs = [] # used in verify 128 | for privkey in PRIVKEYS: 129 | for message in MESSAGES: 130 | for domain in DOMAINS: 131 | sig = bls.sign(message, privkey, domain) 132 | case04_sign_messages.append({ 133 | 'input': { 134 | 'privkey': int_to_hex(privkey), 135 | 'message': '0x' + message.hex(), 136 | 'domain': int_to_hex(domain) 137 | }, 138 | 'output': '0x' + sig.hex() 139 | }) 140 | sigs.append(sig) 141 | 142 | # TODO: case05_verify_messages: Verify messages signed in case04 143 | # It takes too long, empty for now 144 | 145 | case06_aggregate_sigs = [] 146 | for domain in DOMAINS: 147 | for message in MESSAGES: 148 | sigs = [] 149 | for privkey in PRIVKEYS: 150 | sig = bls.sign(message, privkey, domain) 151 | sigs.append(sig) 152 | case06_aggregate_sigs.append({ 153 | 'input': ['0x' + sig.hex() for sig in sigs], 154 | 'output': '0x' + bls.aggregate_signatures(sigs).hex(), 155 | }) 156 | 157 | case07_aggregate_pubkeys = [ 158 | { 159 | 'input': pubkeys_serial, 160 | 'output': '0x' + bls.aggregate_pubkeys(pubkeys).hex(), 161 | } 162 | ] 163 | 164 | # TODO 165 | # Aggregate verify 166 | 167 | # TODO 168 | # Proof-of-possession 169 | 170 | with open(sys.argv[1], 'w') as outfile: 171 | # Dump at top level 172 | yaml.dump(metadata, outfile, default_flow_style=False) 173 | # default_flow_style will unravel "ValidatorRecord" and "committee" line, 174 | # exploding file size 175 | yaml.dump( 176 | {'case01_message_hash_G2_uncompressed': case01_message_hash_G2_uncompressed}, 177 | outfile, 178 | ) 179 | yaml.dump( 180 | {'case02_message_hash_G2_compressed': case02_message_hash_G2_compressed}, 181 | outfile, 182 | ) 183 | yaml.dump( 184 | {'case03_private_to_public_key': case03_private_to_public_key}, 185 | outfile, 186 | ) 187 | yaml.dump({'case04_sign_messages': case04_sign_messages}, outfile) 188 | 189 | # Too time consuming to generate 190 | # yaml.dump({'case05_verify_messages': case05_verify_messages}, outfile) 191 | yaml.dump({'case06_aggregate_sigs': case06_aggregate_sigs}, outfile) 192 | yaml.dump({'case07_aggregate_pubkeys': case07_aggregate_pubkeys}, outfile) 193 | -------------------------------------------------------------------------------- /shuffling/README.md: -------------------------------------------------------------------------------- 1 | # Shuffling Test Generator 2 | 3 | ``` 4 | 2018 Status Research & Development GmbH 5 | Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). 6 | 7 | This work uses public domain work under CC0 from the Ethereum Foundation 8 | https://github.com/ethereum/eth2.0-specs 9 | ``` 10 | 11 | 12 | This file implements a test vectors generator for the shuffling algorithm described in the Ethereum 13 | [specs](https://github.com/ethereum/eth2.0-specs/blob/2983e68f0305551083fac7fcf9330c1fc9da3411/specs/core/0_beacon-chain.md#get_new_shuffling) 14 | 15 | Utilizes 'swap or not' shuffling found in [An Enciphering Scheme Based on a Card Shuffle](https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf). 16 | See the `Generalized domain` algorithm on page 3. 17 | -------------------------------------------------------------------------------- /shuffling/constants.py: -------------------------------------------------------------------------------- 1 | SLOTS_PER_EPOCH = 2**6 # 64 slots, 6.4 minutes 2 | FAR_FUTURE_EPOCH = 2**64 - 1 # uint64 max 3 | SHARD_COUNT = 2**10 # 1024 4 | TARGET_COMMITTEE_SIZE = 2**7 # 128 validators 5 | ACTIVATION_EXIT_DELAY = 2**2 # 4 epochs 6 | SHUFFLE_ROUND_COUNT = 90 7 | -------------------------------------------------------------------------------- /shuffling/core_helpers.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, NewType 2 | 3 | from constants import SLOTS_PER_EPOCH, SHARD_COUNT, TARGET_COMMITTEE_SIZE, SHUFFLE_ROUND_COUNT 4 | from utils import hash 5 | from yaml_objects import Validator 6 | 7 | Epoch = NewType("Epoch", int) 8 | ValidatorIndex = NewType("ValidatorIndex", int) 9 | Bytes32 = NewType("Bytes32", bytes) 10 | 11 | 12 | def int_to_bytes1(x): 13 | return x.to_bytes(1, 'little') 14 | 15 | 16 | def int_to_bytes4(x): 17 | return x.to_bytes(4, 'little') 18 | 19 | 20 | def bytes_to_int(data: bytes) -> int: 21 | return int.from_bytes(data, 'little') 22 | 23 | 24 | def is_active_validator(validator: Validator, epoch: Epoch) -> bool: 25 | """ 26 | Check if ``validator`` is active. 27 | """ 28 | return validator.activation_epoch <= epoch < validator.exit_epoch 29 | 30 | 31 | def get_active_validator_indices(validators: List[Validator], epoch: Epoch) -> List[ValidatorIndex]: 32 | """ 33 | Get indices of active validators from ``validators``. 34 | """ 35 | return [i for i, v in enumerate(validators) if is_active_validator(v, epoch)] 36 | 37 | 38 | def split(values: List[Any], split_count: int) -> List[List[Any]]: 39 | """ 40 | Splits ``values`` into ``split_count`` pieces. 41 | """ 42 | list_length = len(values) 43 | return [ 44 | values[(list_length * i // split_count): (list_length * (i + 1) // split_count)] 45 | for i in range(split_count) 46 | ] 47 | 48 | 49 | def get_epoch_committee_count(active_validator_count: int) -> int: 50 | """ 51 | Return the number of committees in one epoch. 52 | """ 53 | return max( 54 | 1, 55 | min( 56 | SHARD_COUNT // SLOTS_PER_EPOCH, 57 | active_validator_count // SLOTS_PER_EPOCH // TARGET_COMMITTEE_SIZE, 58 | ) 59 | ) * SLOTS_PER_EPOCH 60 | 61 | 62 | def get_permuted_index(index: int, list_size: int, seed: Bytes32) -> int: 63 | """ 64 | Return `p(index)` in a pseudorandom permutation `p` of `0...list_size-1` with ``seed`` as entropy. 65 | 66 | Utilizes 'swap or not' shuffling found in 67 | https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf 68 | See the 'generalized domain' algorithm on page 3. 69 | """ 70 | for round in range(SHUFFLE_ROUND_COUNT): 71 | pivot = bytes_to_int(hash(seed + int_to_bytes1(round))[0:8]) % list_size 72 | flip = (pivot - index) % list_size 73 | position = max(index, flip) 74 | source = hash(seed + int_to_bytes1(round) + int_to_bytes4(position // 256)) 75 | byte = source[(position % 256) // 8] 76 | bit = (byte >> (position % 8)) % 2 77 | index = flip if bit else index 78 | 79 | return index 80 | 81 | 82 | def get_shuffling(seed: Bytes32, 83 | validators: List[Validator], 84 | epoch: Epoch) -> List[List[ValidatorIndex]]: 85 | """ 86 | Shuffle active validators and split into crosslink committees. 87 | Return a list of committees (each a list of validator indices). 88 | """ 89 | # Shuffle active validator indices 90 | active_validator_indices = get_active_validator_indices(validators, epoch) 91 | length = len(active_validator_indices) 92 | shuffled_indices = [active_validator_indices[get_permuted_index(i, length, seed)] for i in range(length)] 93 | 94 | # Split the shuffled active validator indices 95 | return split(shuffled_indices, get_epoch_committee_count(length)) 96 | -------------------------------------------------------------------------------- /shuffling/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-hash[pycryptodome]==0.2.0 2 | eth-typing==2.0.0 3 | eth-utils==1.4.1 4 | PyYAML==4.2b1 5 | -------------------------------------------------------------------------------- /shuffling/tgen_shuffling.py: -------------------------------------------------------------------------------- 1 | import random 2 | import sys 3 | import os 4 | 5 | import yaml 6 | 7 | from constants import ACTIVATION_EXIT_DELAY, FAR_FUTURE_EPOCH 8 | from core_helpers import get_shuffling 9 | from yaml_objects import Validator 10 | 11 | 12 | def noop(self, *args, **kw): 13 | # Prevent !!str or !!binary tags 14 | pass 15 | 16 | 17 | yaml.emitter.Emitter.process_tag = noop 18 | 19 | 20 | EPOCH = 1000 # The epoch, also a mean for the normal distribution 21 | 22 | # Standard deviation, around 8% validators will activate or exit within 23 | # ENTRY_EXIT_DELAY inclusive from EPOCH thus creating an edge case for validator 24 | # shuffling 25 | RAND_EPOCH_STD = 35 26 | MAX_EXIT_EPOCH = 5000 # Maximum exit_epoch for easier reading 27 | 28 | 29 | def active_exited_validators_generator(): 30 | """ 31 | Random cases with variety of validator's activity status 32 | """ 33 | # Order not preserved - https://github.com/yaml/pyyaml/issues/110 34 | metadata = { 35 | 'title': 'Shuffling Algorithm Tests 1', 36 | 'summary': 'Test vectors for validator shuffling with different validator\'s activity status.' 37 | ' Note: only relevant validator fields are defined.', 38 | 'test_suite': 'shuffle', 39 | 'fork': 'phase0-0.5.0', 40 | } 41 | 42 | # Config 43 | random.seed(int("0xEF00BEAC", 16)) 44 | num_cases = 10 45 | 46 | test_cases = [] 47 | 48 | for case in range(num_cases): 49 | seedhash = bytes(random.randint(0, 255) for byte in range(32)) 50 | idx_max = random.randint(128, 512) 51 | 52 | validators = [] 53 | for idx in range(idx_max): 54 | v = Validator(original_index=idx) 55 | # 4/5 of all validators are active 56 | if random.random() < 0.8: 57 | # Choose a normally distributed epoch number 58 | rand_epoch = round(random.gauss(EPOCH, RAND_EPOCH_STD)) 59 | 60 | # for 1/2 of *active* validators rand_epoch is the activation epoch 61 | if random.random() < 0.5: 62 | v.activation_epoch = rand_epoch 63 | 64 | # 1/4 of active validators will exit in forseeable future 65 | if random.random() < 0.5: 66 | v.exit_epoch = random.randint( 67 | rand_epoch + ACTIVATION_EXIT_DELAY + 1, MAX_EXIT_EPOCH) 68 | # 1/4 of active validators in theory remain in the set indefinitely 69 | else: 70 | v.exit_epoch = FAR_FUTURE_EPOCH 71 | # for the other active 1/2 rand_epoch is the exit epoch 72 | else: 73 | v.activation_epoch = random.randint( 74 | 0, rand_epoch - ACTIVATION_EXIT_DELAY) 75 | v.exit_epoch = rand_epoch 76 | 77 | # The remaining 1/5 of all validators is not activated 78 | else: 79 | v.activation_epoch = FAR_FUTURE_EPOCH 80 | v.exit_epoch = FAR_FUTURE_EPOCH 81 | 82 | validators.append(v) 83 | 84 | input_ = { 85 | 'validators': validators, 86 | 'epoch': EPOCH 87 | } 88 | output = get_shuffling( 89 | seedhash, validators, input_['epoch']) 90 | 91 | test_cases.append({ 92 | 'seed': '0x' + seedhash.hex(), 'input': input_, 'output': output 93 | }) 94 | 95 | return { 96 | 'metadata': metadata, 97 | 'filename': 'test_vector_shuffling.yml', 98 | 'test_cases': test_cases 99 | } 100 | 101 | 102 | def validators_set_size_variety_generator(): 103 | """ 104 | Different validator set size cases, inspired by removed manual `permutated_index` tests 105 | https://github.com/ethereum/eth2.0-test-generators/tree/bcd9ab2933d9f696901d1dfda0828061e9d3093f/permutated_index 106 | """ 107 | # Order not preserved - https://github.com/yaml/pyyaml/issues/110 108 | metadata = { 109 | 'title': 'Shuffling Algorithm Tests 2', 110 | 'summary': 'Test vectors for validator shuffling with different validator\'s set size.' 111 | ' Note: only relevant validator fields are defined.', 112 | 'test_suite': 'shuffle', 113 | 'fork': 'tchaikovsky', 114 | 'version': 1.0 115 | } 116 | 117 | # Config 118 | random.seed(int("0xEF00BEAC", 16)) 119 | 120 | test_cases = [] 121 | 122 | seedhash = bytes(random.randint(0, 255) for byte in range(32)) 123 | idx_max = 4096 124 | set_sizes = [1, 2, 3, 1024, idx_max] 125 | 126 | for size in set_sizes: 127 | validators = [] 128 | for idx in range(size): 129 | v = Validator(original_index=idx) 130 | v.activation_epoch = EPOCH 131 | v.exit_epoch = FAR_FUTURE_EPOCH 132 | validators.append(v) 133 | input_ = { 134 | 'validators': validators, 135 | 'epoch': EPOCH 136 | } 137 | output = get_shuffling( 138 | seedhash, validators, input_['epoch']) 139 | 140 | test_cases.append({ 141 | 'seed': '0x' + seedhash.hex(), 'input': input_, 'output': output 142 | }) 143 | 144 | return { 145 | 'metadata': metadata, 146 | 'filename': 'shuffling_set_size.yml', 147 | 'test_cases': test_cases 148 | } 149 | 150 | 151 | if __name__ == '__main__': 152 | output_dir = sys.argv[1] 153 | for generator in [active_exited_validators_generator, validators_set_size_variety_generator]: 154 | result = generator() 155 | filename = os.path.join(output_dir, result['filename']) 156 | with open(filename, 'w') as outfile: 157 | # Dump at top level 158 | yaml.dump(result['metadata'], outfile, default_flow_style=False) 159 | # default_flow_style will unravel "ValidatorRecord" and "committee" line, exploding file size 160 | yaml.dump({'test_cases': result['test_cases']}, outfile) 161 | -------------------------------------------------------------------------------- /shuffling/utils.py: -------------------------------------------------------------------------------- 1 | from eth_typing import Hash32 2 | from eth_utils import keccak 3 | 4 | 5 | def hash(x: bytes) -> Hash32: 6 | return keccak(x) 7 | -------------------------------------------------------------------------------- /shuffling/yaml_objects.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import yaml 4 | 5 | 6 | class Validator(yaml.YAMLObject): 7 | """ 8 | A validator stub containing only the fields relevant for get_shuffling() 9 | """ 10 | fields = { 11 | 'activation_epoch': 'uint64', 12 | 'exit_epoch': 'uint64', 13 | # Extra index field to ease testing/debugging 14 | 'original_index': 'uint64', 15 | } 16 | 17 | def __init__(self, **kwargs): 18 | for k in self.fields.keys(): 19 | setattr(self, k, kwargs.get(k)) 20 | 21 | def __setattr__(self, name: str, value: Any) -> None: 22 | super().__setattr__(name, value) 23 | 24 | def __getattribute__(self, name: str) -> Any: 25 | return super().__getattribute__(name) 26 | -------------------------------------------------------------------------------- /ssz/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/eth2.0-test-generators/bbcfb29d4864d802890d440ba5fa998e70fc2b4b/ssz/__init__.py -------------------------------------------------------------------------------- /ssz/renderers.py: -------------------------------------------------------------------------------- 1 | from collections.abc import ( 2 | Mapping, 3 | Sequence, 4 | ) 5 | 6 | from eth_utils import ( 7 | encode_hex, 8 | to_dict, 9 | ) 10 | 11 | from ssz.sedes import ( 12 | BaseSedes, 13 | Boolean, 14 | Bytes, 15 | BytesN, 16 | Container, 17 | List, 18 | UInt, 19 | ) 20 | 21 | 22 | def render_value(value): 23 | if isinstance(value, bool): 24 | return value 25 | elif isinstance(value, int): 26 | return str(value) 27 | elif isinstance(value, bytes): 28 | return encode_hex(value) 29 | elif isinstance(value, Sequence): 30 | return tuple(render_value(element) for element in value) 31 | elif isinstance(value, Mapping): 32 | return render_dict_value(value) 33 | else: 34 | raise ValueError(f"Cannot render value {value}") 35 | 36 | 37 | @to_dict 38 | def render_dict_value(value): 39 | for key, value in value.items(): 40 | yield key, render_value(value) 41 | 42 | 43 | def render_type_definition(sedes): 44 | if isinstance(sedes, Boolean): 45 | return "bool" 46 | 47 | elif isinstance(sedes, UInt): 48 | return f"uint{sedes.length * 8}" 49 | 50 | elif isinstance(sedes, BytesN): 51 | return f"bytes{sedes.length}" 52 | 53 | elif isinstance(sedes, Bytes): 54 | return f"bytes" 55 | 56 | elif isinstance(sedes, List): 57 | return [render_type_definition(sedes.element_sedes)] 58 | 59 | elif isinstance(sedes, Container): 60 | return { 61 | field_name: render_type_definition(field_sedes) 62 | for field_name, field_sedes in sedes.fields 63 | } 64 | 65 | elif isinstance(sedes, BaseSedes): 66 | raise Exception("Unreachable: All sedes types have been checked") 67 | 68 | else: 69 | raise TypeError("Expected BaseSedes") 70 | 71 | 72 | @to_dict 73 | def render_test_case(*, sedes, valid, value=None, serial=None, description=None, tags=None): 74 | value_and_serial_given = value is not None and serial is not None 75 | if valid: 76 | if not value_and_serial_given: 77 | raise ValueError("For valid test cases, both value and ssz must be present") 78 | else: 79 | if value_and_serial_given: 80 | raise ValueError("For invalid test cases, either value or ssz must not be present") 81 | 82 | if tags is None: 83 | tags = [] 84 | 85 | yield "type", render_type_definition(sedes) 86 | yield "valid", valid 87 | if value is not None: 88 | yield "value", render_value(value) 89 | if serial is not None: 90 | yield "ssz", encode_hex(serial) 91 | if description is not None: 92 | yield description 93 | yield "tags", tags 94 | 95 | 96 | @to_dict 97 | def render_test(*, title, summary, fork, test_cases): 98 | yield "title", title, 99 | if summary is not None: 100 | yield "summary", summary 101 | yield "fork", fork 102 | yield "test_cases", test_cases 103 | -------------------------------------------------------------------------------- /ssz/requirements.txt: -------------------------------------------------------------------------------- 1 | ruamel.yaml==0.15.87 2 | ssz==0.1.0a2 3 | -------------------------------------------------------------------------------- /ssz/test_generator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pathlib 3 | import sys 4 | 5 | from ruamel.yaml import ( 6 | YAML, 7 | ) 8 | 9 | from uint_test_generators import ( 10 | generate_uint_bounds_test, 11 | generate_uint_random_test, 12 | generate_uint_wrong_length_test, 13 | ) 14 | 15 | test_generators = [ 16 | generate_uint_random_test, 17 | generate_uint_wrong_length_test, 18 | generate_uint_bounds_test, 19 | ] 20 | 21 | 22 | def make_filename_for_test(test): 23 | title = test["title"] 24 | filename = title.lower().replace(" ", "_") + ".yaml" 25 | return pathlib.Path(filename) 26 | 27 | 28 | def validate_output_dir(path_str): 29 | path = pathlib.Path(path_str) 30 | 31 | if not path.exists(): 32 | raise argparse.ArgumentTypeError("Output directory must exist") 33 | 34 | if not path.is_dir(): 35 | raise argparse.ArgumentTypeError("Output path must lead to a directory") 36 | 37 | return path 38 | 39 | 40 | parser = argparse.ArgumentParser( 41 | prog="gen-ssz-tests", 42 | description="Generate YAML test files for SSZ and tree hashing", 43 | ) 44 | parser.add_argument( 45 | "-o", 46 | "--output-dir", 47 | dest="output_dir", 48 | required=True, 49 | type=validate_output_dir, 50 | help="directory into which the generated YAML files will be dumped" 51 | ) 52 | parser.add_argument( 53 | "-f", 54 | "--force", 55 | action="store_true", 56 | default=False, 57 | help="if set overwrite test files if they exist", 58 | ) 59 | 60 | 61 | if __name__ == "__main__": 62 | args = parser.parse_args() 63 | output_dir = args.output_dir 64 | if not args.force: 65 | file_mode = "x" 66 | else: 67 | file_mode = "w" 68 | 69 | yaml = YAML(pure=True) 70 | 71 | print(f"generating {len(test_generators)} test files...") 72 | for test_generator in test_generators: 73 | test = test_generator() 74 | 75 | filename = make_filename_for_test(test) 76 | path = output_dir / filename 77 | 78 | try: 79 | with path.open(file_mode) as f: 80 | yaml.dump(test, f) 81 | except IOError as e: 82 | sys.exit(f'Error when dumping test "{test["title"]}" ({e})') 83 | 84 | print("done.") 85 | -------------------------------------------------------------------------------- /ssz/uint_test_generators.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from eth_utils import ( 4 | to_tuple, 5 | ) 6 | 7 | import ssz 8 | from ssz.sedes import ( 9 | UInt, 10 | ) 11 | from renderers import ( 12 | render_test, 13 | render_test_case, 14 | ) 15 | 16 | random.seed(0) 17 | 18 | 19 | BIT_SIZES = [i for i in range(8, 512 + 1, 8)] 20 | RANDOM_TEST_CASES_PER_BIT_SIZE = 10 21 | RANDOM_TEST_CASES_PER_LENGTH = 3 22 | 23 | 24 | def get_random_bytes(length): 25 | return bytes(random.randint(0, 255) for _ in range(length)) 26 | 27 | 28 | def generate_uint_bounds_test(): 29 | test_cases = generate_uint_bounds_test_cases() + generate_uint_out_of_bounds_test_cases() 30 | 31 | return render_test( 32 | title="UInt Bounds", 33 | summary="Integers right at or beyond the bounds of the allowed value range", 34 | fork="phase0-0.2.0", 35 | test_cases=test_cases, 36 | ) 37 | 38 | 39 | def generate_uint_random_test(): 40 | test_cases = generate_random_uint_test_cases() 41 | 42 | return render_test( 43 | title="UInt Random", 44 | summary="Random integers chosen uniformly over the allowed value range", 45 | fork="phase0-0.2.0", 46 | test_cases=test_cases, 47 | ) 48 | 49 | 50 | def generate_uint_wrong_length_test(): 51 | test_cases = generate_uint_wrong_length_test_cases() 52 | 53 | return render_test( 54 | title="UInt Wrong Length", 55 | summary="Serialized integers that are too short or too long", 56 | fork="phase0-0.2.0", 57 | test_cases=test_cases, 58 | ) 59 | 60 | 61 | @to_tuple 62 | def generate_random_uint_test_cases(): 63 | for bit_size in BIT_SIZES: 64 | sedes = UInt(bit_size) 65 | 66 | for _ in range(RANDOM_TEST_CASES_PER_BIT_SIZE): 67 | value = random.randrange(0, 2 ** bit_size) 68 | serial = ssz.encode(value, sedes) 69 | # note that we need to create the tags in each loop cycle, otherwise ruamel will use 70 | # YAML references which makes the resulting file harder to read 71 | tags = tuple(["atomic", "uint", "random"]) 72 | yield render_test_case( 73 | sedes=sedes, 74 | valid=True, 75 | value=value, 76 | serial=serial, 77 | tags=tags, 78 | ) 79 | 80 | 81 | @to_tuple 82 | def generate_uint_wrong_length_test_cases(): 83 | for bit_size in BIT_SIZES: 84 | sedes = UInt(bit_size) 85 | lengths = sorted({ 86 | 0, 87 | sedes.length // 2, 88 | sedes.length - 1, 89 | sedes.length + 1, 90 | sedes.length * 2, 91 | }) 92 | for length in lengths: 93 | for _ in range(RANDOM_TEST_CASES_PER_LENGTH): 94 | tags = tuple(["atomic", "uint", "wrong_length"]) 95 | yield render_test_case( 96 | sedes=sedes, 97 | valid=False, 98 | serial=get_random_bytes(length), 99 | tags=tags, 100 | ) 101 | 102 | 103 | @to_tuple 104 | def generate_uint_bounds_test_cases(): 105 | common_tags = ("atomic", "uint") 106 | for bit_size in BIT_SIZES: 107 | sedes = UInt(bit_size) 108 | 109 | for value, tag in ((0, "uint_lower_bound"), (2 ** bit_size - 1, "uint_upper_bound")): 110 | serial = ssz.encode(value, sedes) 111 | yield render_test_case( 112 | sedes=sedes, 113 | valid=True, 114 | value=value, 115 | serial=serial, 116 | tags=common_tags + (tag,), 117 | ) 118 | 119 | 120 | @to_tuple 121 | def generate_uint_out_of_bounds_test_cases(): 122 | common_tags = ("atomic", "uint") 123 | for bit_size in BIT_SIZES: 124 | sedes = UInt(bit_size) 125 | 126 | for value, tag in ((-1, "uint_underflow"), (2 ** bit_size, "uint_overflow")): 127 | yield render_test_case( 128 | sedes=sedes, 129 | valid=False, 130 | value=value, 131 | tags=common_tags + (tag,), 132 | ) 133 | --------------------------------------------------------------------------------