diff --git a/Makefile b/Makefile index 73d8adea8..86303680d 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ install_test: cd $(PY_SPEC_DIR); python3 -m venv venv; . venv/bin/activate; pip3 install -r requirements-testing.txt; test: $(PY_SPEC_ALL_TARGETS) - cd $(PY_SPEC_DIR); . venv/bin/activate; python -m pytest . + cd $(PY_SPEC_DIR); . venv/bin/activate; python -m pytest eth2spec citest: $(PY_SPEC_ALL_TARGETS) cd $(PY_SPEC_DIR); mkdir -p test-reports/eth2spec; . venv/bin/activate; python -m pytest --junitxml=test-reports/eth2spec/test_results.xml . diff --git a/test_generators/operations/deposits.py b/test_generators/operations/deposits.py deleted file mode 100644 index 075ccbd5b..000000000 --- a/test_generators/operations/deposits.py +++ /dev/null @@ -1,180 +0,0 @@ -from eth2spec.phase0 import spec -from eth_utils import ( - to_dict, to_tuple -) -from gen_base import gen_suite, gen_typing -from preset_loader import loader -from eth2spec.debug.encode import encode -from eth2spec.utils.minimal_ssz import signing_root -from eth2spec.utils.merkle_minimal import get_merkle_root, calc_merkle_tree_from_leaves, get_merkle_proof - -from typing import List, Tuple - -import genesis -import keys -from py_ecc import bls - - -def build_deposit_data(state, - pubkey: spec.BLSPubkey, - withdrawal_cred: spec.Bytes32, - privkey: int, - amount: int): - deposit_data = spec.DepositData( - pubkey=pubkey, - withdrawal_credentials=spec.BLS_WITHDRAWAL_PREFIX_BYTE + withdrawal_cred[1:], - amount=amount, - ) - deposit_data.proof_of_possession = bls.sign( - message_hash=signing_root(deposit_data), - privkey=privkey, - domain=spec.get_domain( - state, - spec.get_current_epoch(state), - spec.DOMAIN_DEPOSIT, - ) - ) - return deposit_data - - -def build_deposit(state, - deposit_data_leaves: List[spec.Bytes32], - pubkey: spec.BLSPubkey, - withdrawal_cred: spec.Bytes32, - privkey: int, - amount: int) -> spec.Deposit: - - deposit_data = build_deposit_data(state, pubkey, withdrawal_cred, privkey, amount) - - item = deposit_data.hash_tree_root() - index = len(deposit_data_leaves) - deposit_data_leaves.append(item) - tree = calc_merkle_tree_from_leaves(tuple(deposit_data_leaves)) - proof = list(get_merkle_proof(tree, item_index=index)) - - deposit = spec.Deposit( - proof=list(proof), - index=index, - data=deposit_data, - ) - assert spec.verify_merkle_branch(item, proof, spec.DEPOSIT_CONTRACT_TREE_DEPTH, index, get_merkle_root(tuple(deposit_data_leaves))) - - return deposit - - -def build_deposit_for_index(initial_validator_count: int, index: int) -> Tuple[spec.Deposit, spec.BeaconState]: - genesis_deposits = genesis.create_deposits( - keys.pubkeys[:initial_validator_count], - keys.withdrawal_creds[:initial_validator_count] - ) - state = genesis.create_genesis_state(genesis_deposits) - - deposit_data_leaves = [dep.data.hash_tree_root() for dep in genesis_deposits] - - deposit = build_deposit( - state, - deposit_data_leaves, - keys.pubkeys[index], - keys.withdrawal_creds[index], - keys.privkeys[index], - spec.MAX_EFFECTIVE_BALANCE, - ) - - state.latest_eth1_data.deposit_root = get_merkle_root(tuple(deposit_data_leaves)) - state.latest_eth1_data.deposit_count = len(deposit_data_leaves) - - return deposit, state - - -@to_dict -def valid_deposit(): - new_dep, state = build_deposit_for_index(10, 10) - yield 'description', 'valid deposit to add new validator' - yield 'pre', encode(state, spec.BeaconState) - yield 'deposit', encode(new_dep, spec.Deposit) - spec.process_deposit(state, new_dep) - yield 'post', encode(state, spec.BeaconState) - - -@to_dict -def valid_topup(): - new_dep, state = build_deposit_for_index(10, 3) - yield 'description', 'valid deposit to top-up existing validator' - yield 'pre', encode(state, spec.BeaconState) - yield 'deposit', encode(new_dep, spec.Deposit) - spec.process_deposit(state, new_dep) - yield 'post', encode(state, spec.BeaconState) - - -@to_dict -def invalid_deposit_index(): - new_dep, state = build_deposit_for_index(10, 10) - # Mess up deposit index, 1 too small - state.deposit_index = 9 - - yield 'description', 'invalid deposit index' - yield 'pre', encode(state, spec.BeaconState) - yield 'deposit', encode(new_dep, spec.Deposit) - try: - spec.process_deposit(state, new_dep) - except AssertionError: - # expected - yield 'post', None - return - raise Exception('invalid_deposit_index has unexpectedly allowed deposit') - - -@to_dict -def invalid_deposit_proof(): - new_dep, state = build_deposit_for_index(10, 10) - # Make deposit proof invalid (at bottom of proof) - new_dep.proof[-1] = spec.ZERO_HASH - - yield 'description', 'invalid deposit proof' - yield 'pre', encode(state, spec.BeaconState) - yield 'deposit', encode(new_dep, spec.Deposit) - try: - spec.process_deposit(state, new_dep) - except AssertionError: - # expected - yield 'post', None - return - raise Exception('invalid_deposit_index has unexpectedly allowed deposit') - - -@to_tuple -def deposit_cases(): - yield valid_deposit() - yield valid_topup() - yield invalid_deposit_index() - yield invalid_deposit_proof() - - -def mini_deposits_suite(configs_path: str) -> gen_typing.TestSuiteOutput: - presets = loader.load_presets(configs_path, 'minimal') - spec.apply_constants_preset(presets) - - return ("deposit_minimal", "deposits", gen_suite.render_suite( - title="deposit operation", - summary="Test suite for deposit type operation processing", - forks_timeline="testing", - forks=["phase0"], - config="minimal", - runner="operations", - handler="deposits", - test_cases=deposit_cases())) - - -def full_deposits_suite(configs_path: str) -> gen_typing.TestSuiteOutput: - presets = loader.load_presets(configs_path, 'mainnet') - spec.apply_constants_preset(presets) - - return ("deposit_full", "deposits", gen_suite.render_suite( - title="deposit operation", - summary="Test suite for deposit type operation processing", - forks_timeline="mainnet", - forks=["phase0"], - config="mainnet", - runner="operations", - handler="deposits", - test_cases=deposit_cases())) diff --git a/test_generators/operations/genesis.py b/test_generators/operations/genesis.py deleted file mode 100644 index f4d63c10e..000000000 --- a/test_generators/operations/genesis.py +++ /dev/null @@ -1,44 +0,0 @@ -from eth2spec.phase0 import spec -from eth2spec.utils.merkle_minimal import get_merkle_root, calc_merkle_tree_from_leaves, get_merkle_proof -from typing import List - - -def create_genesis_state(deposits: List[spec.Deposit]) -> spec.BeaconState: - deposit_root = get_merkle_root((tuple([(dep.data.hash_tree_root()) for dep in deposits]))) - - return spec.get_genesis_beacon_state( - deposits, - genesis_time=0, - genesis_eth1_data=spec.Eth1Data( - deposit_root=deposit_root, - deposit_count=len(deposits), - block_hash=spec.ZERO_HASH, - ), - ) - - -def create_deposits(pubkeys: List[spec.BLSPubkey], withdrawal_cred: List[spec.Bytes32]) -> List[spec.Deposit]: - - # Mock proof of possession - proof_of_possession = b'\x33' * 96 - - deposit_data = [ - spec.DepositData( - pubkey=pubkeys[i], - withdrawal_credentials=spec.BLS_WITHDRAWAL_PREFIX_BYTE + withdrawal_cred[i][1:], - amount=spec.MAX_EFFECTIVE_BALANCE, - proof_of_possession=proof_of_possession, - ) for i in range(len(pubkeys)) - ] - - # Fill tree with existing deposits - deposit_data_leaves = [data.hash_tree_root() for data in deposit_data] - tree = calc_merkle_tree_from_leaves(tuple(deposit_data_leaves)) - - return [ - spec.Deposit( - proof=list(get_merkle_proof(tree, item_index=i)), - index=i, - data=deposit_data[i] - ) for i in range(len(deposit_data)) - ] diff --git a/test_generators/operations/keys.py b/test_generators/operations/keys.py deleted file mode 100644 index db4f59e0e..000000000 --- a/test_generators/operations/keys.py +++ /dev/null @@ -1,7 +0,0 @@ -from py_ecc import bls -from eth2spec.phase0.spec import hash - -privkeys = list(range(1, 101)) -pubkeys = [bls.privtopub(k) for k in privkeys] -# Insecure, but easier to follow -withdrawal_creds = [hash(bls.privtopub(k)) for k in privkeys] diff --git a/test_generators/operations/main.py b/test_generators/operations/main.py index 8b0a2a6d8..41a6ff806 100644 --- a/test_generators/operations/main.py +++ b/test_generators/operations/main.py @@ -1,9 +1,31 @@ +from eth2spec.test.block_processing import ( + test_process_attestation, + test_process_attester_slashing, + test_process_block_header, + test_process_deposit, + test_process_proposer_slashing, + test_process_transfer, + test_process_voluntary_exit +) + from gen_base import gen_runner -from deposits import mini_deposits_suite, full_deposits_suite +from suite_creator import generate_from_tests, create_suite if __name__ == "__main__": gen_runner.run_generator("operations", [ - mini_deposits_suite, - full_deposits_suite + create_suite('attestation', 'minimal', lambda: generate_from_tests(test_process_attestation)), + create_suite('attestation', 'mainnet', lambda: generate_from_tests(test_process_attestation)), + create_suite('attester_slashing', 'minimal', lambda: generate_from_tests(test_process_attester_slashing)), + create_suite('attester_slashing', 'mainnet', lambda: generate_from_tests(test_process_attester_slashing)), + create_suite('block_header', 'minimal', lambda: generate_from_tests(test_process_block_header)), + create_suite('block_header', 'mainnet', lambda: generate_from_tests(test_process_block_header)), + create_suite('deposit', 'minimal', lambda: generate_from_tests(test_process_deposit)), + create_suite('deposit', 'mainnet', lambda: generate_from_tests(test_process_deposit)), + create_suite('proposer_slashing', 'minimal', lambda: generate_from_tests(test_process_proposer_slashing)), + create_suite('proposer_slashing', 'mainnet', lambda: generate_from_tests(test_process_proposer_slashing)), + create_suite('transfer', 'minimal', lambda: generate_from_tests(test_process_transfer)), + create_suite('transfer', 'mainnet', lambda: generate_from_tests(test_process_transfer)), + create_suite('voluntary_exit', 'minimal', lambda: generate_from_tests(test_process_voluntary_exit)), + create_suite('voluntary_exit', 'mainnet', lambda: generate_from_tests(test_process_voluntary_exit)), ]) diff --git a/test_generators/operations/requirements.txt b/test_generators/operations/requirements.txt index dfe853536..8f9bede8f 100644 --- a/test_generators/operations/requirements.txt +++ b/test_generators/operations/requirements.txt @@ -1,5 +1,4 @@ eth-utils==1.4.1 ../../test_libs/gen_helpers ../../test_libs/config_helpers -../../test_libs/pyspec -py_ecc \ No newline at end of file +../../test_libs/pyspec \ No newline at end of file diff --git a/test_generators/operations/suite_creator.py b/test_generators/operations/suite_creator.py new file mode 100644 index 000000000..d27e3efc1 --- /dev/null +++ b/test_generators/operations/suite_creator.py @@ -0,0 +1,39 @@ +from typing import Callable, Iterable +from inspect import getmembers, isfunction +from gen_base import gen_suite, gen_typing +from preset_loader import loader +from eth2spec.phase0 import spec + + +def generate_from_tests(pkg): + fn_names = [ + name for (name, _) in getmembers(pkg, isfunction) + if name.startswith('test_') + ] + out = [] + print("generating test vectors from tests package: %s" % pkg.__name__) + for name in fn_names: + tfn = getattr(pkg, name) + try: + out.append(tfn(generator_mode=True)) + except AssertionError: + print("ERROR: failed to generate vector from test: %s (pkg: %s)" % (name, pkg.__name__)) + return out + + +def create_suite(operation_name: str, config_name: str, get_cases: Callable[[], Iterable[gen_typing.TestCase]])\ + -> Callable[[str], gen_typing.TestSuiteOutput]: + def suite_definition(configs_path: str) -> gen_typing.TestSuiteOutput: + presets = loader.load_presets(configs_path, config_name) + spec.apply_constants_preset(presets) + + return ("%s_%s" % (operation_name, config_name), operation_name, gen_suite.render_suite( + title="%s operation" % operation_name, + summary="Test suite for %s type operation processing" % operation_name, + forks_timeline="testing", + forks=["phase0"], + config=config_name, + runner="operations", + handler=config_name, + test_cases=get_cases())) + return suite_definition diff --git a/test_libs/pyspec/README.md b/test_libs/pyspec/README.md index df1834210..330972e77 100644 --- a/test_libs/pyspec/README.md +++ b/test_libs/pyspec/README.md @@ -46,8 +46,9 @@ The `-B` flag may be helpful to force-overwrite the `pyspec` output after you ma Run the tests: ``` -pytest --config=minimal +pytest --config=minimal eth2spec ``` +Note the package-name, this is to locate the tests. ## Contributing diff --git a/test_libs/pyspec/tests/__init__.py b/test_libs/pyspec/eth2spec/test/__init__.py similarity index 100% rename from test_libs/pyspec/tests/__init__.py rename to test_libs/pyspec/eth2spec/test/__init__.py diff --git a/test_libs/pyspec/eth2spec/test/block_processing/__init__.py b/test_libs/pyspec/eth2spec/test/block_processing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_libs/pyspec/eth2spec/test/block_processing/test_process_attestation.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_attestation.py new file mode 100644 index 000000000..248b04ef4 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_attestation.py @@ -0,0 +1,214 @@ +from copy import deepcopy + +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.state_transition import ( + state_transition, +) +from eth2spec.phase0.spec import ( + get_current_epoch, + process_attestation +) +from eth2spec.test.helpers import ( + build_empty_block_for_next_slot, + get_valid_attestation, + next_epoch, + next_slot, +) + +from eth2spec.test.context import spec_state_test, expect_assertion_error + + +def run_attestation_processing(state, attestation, valid=True): + """ + Run ``process_attestation``, yielding: + - pre-state ('pre') + - attestation ('attestation') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + # yield pre-state + yield 'pre', state + + yield 'attestation', attestation + + # If the attestation is invalid, processing is aborted, and there is no post-state. + if not valid: + expect_assertion_error(lambda: process_attestation(state, attestation)) + yield 'post', None + return + + current_epoch_count = len(state.current_epoch_attestations) + previous_epoch_count = len(state.previous_epoch_attestations) + + # process attestation + process_attestation(state, attestation) + + # Make sure the attestation has been processed + if attestation.data.target_epoch == get_current_epoch(state): + assert len(state.current_epoch_attestations) == current_epoch_count + 1 + else: + assert len(state.previous_epoch_attestations) == previous_epoch_count + 1 + + # yield post-state + yield 'post', state + + +@spec_state_test +def test_success(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + yield from run_attestation_processing(state, attestation) + + +@spec_state_test +def test_success_previous_epoch(state): + attestation = get_valid_attestation(state) + block = build_empty_block_for_next_slot(state) + block.slot = state.slot + spec.SLOTS_PER_EPOCH + state_transition(state, block) + + yield from run_attestation_processing(state, attestation) + + +@spec_state_test +def test_before_inclusion_delay(state): + attestation = get_valid_attestation(state) + # do not increment slot to allow for inclusion delay + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_after_epoch_slots(state): + attestation = get_valid_attestation(state) + block = build_empty_block_for_next_slot(state) + # increment past latest inclusion slot + block.slot = state.slot + spec.SLOTS_PER_EPOCH + 1 + state_transition(state, block) + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_old_source_epoch(state): + state.slot = spec.SLOTS_PER_EPOCH * 5 + state.finalized_epoch = 2 + state.previous_justified_epoch = 3 + state.current_justified_epoch = 4 + attestation = get_valid_attestation(state, slot=(spec.SLOTS_PER_EPOCH * 3) + 1) + + # test logic sanity check: make sure the attestation is pointing to oldest known source epoch + assert attestation.data.source_epoch == state.previous_justified_epoch + + # Now go beyond that, it will be invalid + attestation.data.source_epoch -= 1 + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_wrong_shard(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + attestation.data.shard += 1 + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_new_source_epoch(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + attestation.data.source_epoch += 1 + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_source_root_is_target_root(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + attestation.data.source_root = attestation.data.target_root + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_invalid_current_source_root(state): + state.slot = spec.SLOTS_PER_EPOCH * 5 + state.finalized_epoch = 2 + + state.previous_justified_epoch = 3 + state.previous_justified_root = b'\x01' * 32 + + state.current_justified_epoch = 4 + state.current_justified_root = b'\xff' * 32 + + attestation = get_valid_attestation(state, slot=(spec.SLOTS_PER_EPOCH * 3) + 1) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + # Test logic sanity checks: + assert state.current_justified_root != state.previous_justified_root + assert attestation.data.source_root == state.previous_justified_root + + # Make attestation source root invalid: should be previous justified, not current one + attestation.data.source_root = state.current_justified_root + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_bad_source_root(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + attestation.data.source_root = b'\x42' * 32 + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_non_zero_crosslink_data_root(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + attestation.data.crosslink_data_root = b'\x42' * 32 + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_bad_previous_crosslink(state): + next_epoch(state) + attestation = get_valid_attestation(state) + for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): + next_slot(state) + + state.current_crosslinks[attestation.data.shard].epoch += 10 + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_non_empty_custody_bitfield(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + attestation.custody_bitfield = deepcopy(attestation.aggregation_bitfield) + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_empty_aggregation_bitfield(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + attestation.aggregation_bitfield = b'\x00' * len(attestation.aggregation_bitfield) + + yield from run_attestation_processing(state, attestation, False) diff --git a/test_libs/pyspec/tests/block_processing/test_process_attester_slashing.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_attester_slashing.py similarity index 58% rename from test_libs/pyspec/tests/block_processing/test_process_attester_slashing.py rename to test_libs/pyspec/eth2spec/test/block_processing/test_process_attester_slashing.py index 2ea16f13d..957b9a9f0 100644 --- a/test_libs/pyspec/tests/block_processing/test_process_attester_slashing.py +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_attester_slashing.py @@ -1,63 +1,73 @@ -from copy import deepcopy -import pytest - import eth2spec.phase0.spec as spec from eth2spec.phase0.spec import ( get_beacon_proposer_index, process_attester_slashing, ) -from tests.helpers import ( +from eth2spec.test.helpers import ( get_balance, get_valid_attester_slashing, next_epoch, ) -# mark entire file as 'attester_slashing' -pytestmark = pytest.mark.attester_slashings +from eth2spec.test.context import spec_state_test, expect_assertion_error def run_attester_slashing_processing(state, attester_slashing, valid=True): """ - Run ``process_attester_slashing`` returning the pre and post state. + Run ``process_attester_slashing``, yielding: + - pre-state ('pre') + - attester_slashing ('attester_slashing') + - post-state ('post'). If ``valid == False``, run expecting ``AssertionError`` """ - post_state = deepcopy(state) + + yield 'pre', state + yield 'attester_slashing', attester_slashing if not valid: - with pytest.raises(AssertionError): - process_attester_slashing(post_state, attester_slashing) - return state, None - - process_attester_slashing(post_state, attester_slashing) + expect_assertion_error(lambda: process_attester_slashing(state, attester_slashing)) + yield 'post', None + return slashed_index = attester_slashing.attestation_1.custody_bit_0_indices[0] - slashed_validator = post_state.validator_registry[slashed_index] + pre_slashed_balance = get_balance(state, slashed_index) + + proposer_index = get_beacon_proposer_index(state) + pre_proposer_balance = get_balance(state, proposer_index) + + # Process slashing + process_attester_slashing(state, attester_slashing) + + slashed_validator = state.validator_registry[slashed_index] + + # Check slashing assert slashed_validator.slashed assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + # lost whistleblower reward assert ( - get_balance(post_state, slashed_index) < - get_balance(state, slashed_index) + get_balance(state, slashed_index) < + pre_slashed_balance ) - proposer_index = get_beacon_proposer_index(state) + # gained whistleblower reward assert ( - get_balance(post_state, proposer_index) > - get_balance(state, proposer_index) + get_balance(state, proposer_index) > + pre_proposer_balance ) - return state, post_state + yield 'post', state +@spec_state_test def test_success_double(state): attester_slashing = get_valid_attester_slashing(state) - pre_state, post_state = run_attester_slashing_processing(state, attester_slashing) - - return pre_state, attester_slashing, post_state + yield from run_attester_slashing_processing(state, attester_slashing) +@spec_state_test def test_success_surround(state): next_epoch(state) state.current_justified_epoch += 1 @@ -67,31 +77,28 @@ def test_success_surround(state): attester_slashing.attestation_1.data.source_epoch = attester_slashing.attestation_2.data.source_epoch - 1 attester_slashing.attestation_1.data.target_epoch = attester_slashing.attestation_2.data.target_epoch + 1 - pre_state, post_state = run_attester_slashing_processing(state, attester_slashing) - - return pre_state, attester_slashing, post_state + yield from run_attester_slashing_processing(state, attester_slashing) +@spec_state_test def test_same_data(state): attester_slashing = get_valid_attester_slashing(state) attester_slashing.attestation_1.data = attester_slashing.attestation_2.data - pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) - - return pre_state, attester_slashing, post_state + yield from run_attester_slashing_processing(state, attester_slashing, False) +@spec_state_test def test_no_double_or_surround(state): attester_slashing = get_valid_attester_slashing(state) attester_slashing.attestation_1.data.target_epoch += 1 - pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) - - return pre_state, attester_slashing, post_state + yield from run_attester_slashing_processing(state, attester_slashing, False) +@spec_state_test def test_participants_already_slashed(state): attester_slashing = get_valid_attester_slashing(state) @@ -101,17 +108,15 @@ def test_participants_already_slashed(state): for index in validator_indices: state.validator_registry[index].slashed = True - pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) - - return pre_state, attester_slashing, post_state + yield from run_attester_slashing_processing(state, attester_slashing, False) +@spec_state_test def test_custody_bit_0_and_1(state): attester_slashing = get_valid_attester_slashing(state) attester_slashing.attestation_1.custody_bit_1_indices = ( attester_slashing.attestation_1.custody_bit_0_indices ) - pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) - return pre_state, attester_slashing, post_state + yield from run_attester_slashing_processing(state, attester_slashing, False) diff --git a/test_libs/pyspec/tests/block_processing/test_process_block_header.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_block_header.py similarity index 59% rename from test_libs/pyspec/tests/block_processing/test_process_block_header.py rename to test_libs/pyspec/eth2spec/test/block_processing/test_process_block_header.py index b35b0a9c1..a176f7958 100644 --- a/test_libs/pyspec/tests/block_processing/test_process_block_header.py +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_block_header.py @@ -1,6 +1,4 @@ from copy import deepcopy -import pytest - from eth2spec.phase0.spec import ( get_beacon_proposer_index, @@ -8,13 +6,12 @@ from eth2spec.phase0.spec import ( advance_slot, process_block_header, ) -from tests.helpers import ( +from eth2spec.test.helpers import ( build_empty_block_for_next_slot, next_slot, ) -# mark entire file as 'header' -pytestmark = pytest.mark.header +from eth2spec.test.context import spec_state_test, expect_assertion_error def prepare_state_for_header_processing(state): @@ -24,43 +21,49 @@ def prepare_state_for_header_processing(state): def run_block_header_processing(state, block, valid=True): """ - Run ``process_block_header`` returning the pre and post state. + Run ``process_block_header``, yielding: + - pre-state ('pre') + - block ('block') + - post-state ('post'). If ``valid == False``, run expecting ``AssertionError`` """ prepare_state_for_header_processing(state) - post_state = deepcopy(state) + + yield 'pre', state + yield 'block', block if not valid: - with pytest.raises(AssertionError): - process_block_header(post_state, block) - return state, None + expect_assertion_error(lambda: process_block_header(state, block)) + yield 'post', None + return - process_block_header(post_state, block) - return state, post_state + process_block_header(state, block) + yield 'post', state +@spec_state_test def test_success(state): block = build_empty_block_for_next_slot(state) - pre_state, post_state = run_block_header_processing(state, block) - return state, block, post_state + yield from run_block_header_processing(state, block) +@spec_state_test def test_invalid_slot(state): block = build_empty_block_for_next_slot(state) block.slot = state.slot + 2 # invalid slot - pre_state, post_state = run_block_header_processing(state, block, valid=False) - return pre_state, block, None + yield from run_block_header_processing(state, block, valid=False) +@spec_state_test def test_invalid_previous_block_root(state): block = build_empty_block_for_next_slot(state) block.previous_block_root = b'\12' * 32 # invalid prev root - pre_state, post_state = run_block_header_processing(state, block, valid=False) - return pre_state, block, None + yield from run_block_header_processing(state, block, valid=False) +@spec_state_test def test_proposer_slashed(state): # use stub state to get proposer index of next slot stub_state = deepcopy(state) @@ -72,5 +75,4 @@ def test_proposer_slashed(state): block = build_empty_block_for_next_slot(state) - pre_state, post_state = run_block_header_processing(state, block, valid=False) - return pre_state, block, None + yield from run_block_header_processing(state, block, valid=False) diff --git a/test_libs/pyspec/eth2spec/test/block_processing/test_process_deposit.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_deposit.py new file mode 100644 index 000000000..fe2dae6a8 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_deposit.py @@ -0,0 +1,102 @@ +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.spec import ( + ZERO_HASH, + process_deposit, +) +from eth2spec.test.helpers import ( + get_balance, + build_deposit, + prepare_state_and_deposit, + privkeys, + pubkeys, +) + +from eth2spec.test.context import spec_state_test, expect_assertion_error + + +def run_deposit_processing(state, deposit, validator_index, valid=True): + """ + Run ``process_deposit``, yielding: + - pre-state ('pre') + - deposit ('deposit') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + pre_validator_count = len(state.validator_registry) + pre_balance = 0 + if validator_index < pre_validator_count: + pre_balance = get_balance(state, validator_index) + else: + # if it is a new validator, it should be right at the end of the current registry. + assert validator_index == pre_validator_count + + yield 'pre', state + yield 'deposit', deposit + + if not valid: + expect_assertion_error(lambda: process_deposit(state, deposit)) + yield 'post', None + return + + process_deposit(state, deposit) + + yield 'post', state + + if validator_index < pre_validator_count: + # top-up + assert len(state.validator_registry) == pre_validator_count + assert len(state.balances) == pre_validator_count + else: + # new validator + assert len(state.validator_registry) == pre_validator_count + 1 + assert len(state.balances) == pre_validator_count + 1 + + assert state.deposit_index == state.latest_eth1_data.deposit_count + assert get_balance(state, validator_index) == pre_balance + deposit.data.amount + + +@spec_state_test +def test_success(state): + # fresh deposit = next validator index = validator appended to registry + validator_index = len(state.validator_registry) + amount = spec.MAX_EFFECTIVE_BALANCE + deposit = prepare_state_and_deposit(state, validator_index, amount) + + yield from run_deposit_processing(state, deposit, validator_index) + + +@spec_state_test +def test_success_top_up(state): + validator_index = 0 + amount = spec.MAX_EFFECTIVE_BALANCE // 4 + deposit = prepare_state_and_deposit(state, validator_index, amount) + + yield from run_deposit_processing(state, deposit, validator_index) + + +@spec_state_test +def test_wrong_index(state): + validator_index = len(state.validator_registry) + amount = spec.MAX_EFFECTIVE_BALANCE + deposit = prepare_state_and_deposit(state, validator_index, amount) + + # mess up deposit_index + deposit.index = state.deposit_index + 1 + + yield from run_deposit_processing(state, deposit, validator_index, valid=False) + + +# TODO: test invalid signature + + +@spec_state_test +def test_bad_merkle_proof(state): + validator_index = len(state.validator_registry) + amount = spec.MAX_EFFECTIVE_BALANCE + deposit = prepare_state_and_deposit(state, validator_index, amount) + + # mess up merkle branch + deposit.proof[-1] = spec.ZERO_HASH + + yield from run_deposit_processing(state, deposit, validator_index, valid=False) diff --git a/test_libs/pyspec/eth2spec/test/block_processing/test_process_proposer_slashing.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_proposer_slashing.py new file mode 100644 index 000000000..609c97ce6 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_proposer_slashing.py @@ -0,0 +1,114 @@ +import eth2spec.phase0.spec as spec +from eth2spec.phase0.spec import ( + get_current_epoch, + process_proposer_slashing, +) +from eth2spec.test.helpers import ( + get_balance, + get_valid_proposer_slashing, +) + +from eth2spec.test.context import spec_state_test, expect_assertion_error + + +def run_proposer_slashing_processing(state, proposer_slashing, valid=True): + """ + Run ``process_proposer_slashing``, yielding: + - pre-state ('pre') + - proposer_slashing ('proposer_slashing') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + + yield 'pre', state + yield 'proposer_slashing', proposer_slashing + + if not valid: + expect_assertion_error(lambda: process_proposer_slashing(state, proposer_slashing)) + yield 'post', None + return + + pre_proposer_balance = get_balance(state, proposer_slashing.proposer_index) + + process_proposer_slashing(state, proposer_slashing) + yield 'post', state + + # check if slashed + slashed_validator = state.validator_registry[proposer_slashing.proposer_index] + assert slashed_validator.slashed + assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + + # lost whistleblower reward + assert ( + get_balance(state, proposer_slashing.proposer_index) < + pre_proposer_balance + ) + + +@spec_state_test +def test_success(state): + proposer_slashing = get_valid_proposer_slashing(state) + + yield from run_proposer_slashing_processing(state, proposer_slashing) + + +@spec_state_test +def test_invalid_proposer_index(state): + proposer_slashing = get_valid_proposer_slashing(state) + # Index just too high (by 1) + proposer_slashing.proposer_index = len(state.validator_registry) + + yield from run_proposer_slashing_processing(state, proposer_slashing, False) + + +@spec_state_test +def test_epochs_are_different(state): + proposer_slashing = get_valid_proposer_slashing(state) + + # set slots to be in different epochs + proposer_slashing.header_2.slot += spec.SLOTS_PER_EPOCH + + yield from run_proposer_slashing_processing(state, proposer_slashing, False) + + +@spec_state_test +def test_headers_are_same(state): + proposer_slashing = get_valid_proposer_slashing(state) + + # set headers to be the same + proposer_slashing.header_2 = proposer_slashing.header_1 + + yield from run_proposer_slashing_processing(state, proposer_slashing, False) + + +@spec_state_test +def test_proposer_is_not_activated(state): + proposer_slashing = get_valid_proposer_slashing(state) + + # set proposer to be not active yet + state.validator_registry[proposer_slashing.proposer_index].activation_epoch = get_current_epoch(state) + 1 + + yield from run_proposer_slashing_processing(state, proposer_slashing, False) + + +@spec_state_test +def test_proposer_is_slashed(state): + proposer_slashing = get_valid_proposer_slashing(state) + + # set proposer to slashed + state.validator_registry[proposer_slashing.proposer_index].slashed = True + + yield from run_proposer_slashing_processing(state, proposer_slashing, False) + + +@spec_state_test +def test_proposer_is_withdrawn(state): + proposer_slashing = get_valid_proposer_slashing(state) + + # set proposer withdrawable_epoch in past + current_epoch = get_current_epoch(state) + proposer_index = proposer_slashing.proposer_index + state.validator_registry[proposer_index].withdrawable_epoch = current_epoch - 1 + + yield from run_proposer_slashing_processing(state, proposer_slashing, False) diff --git a/test_libs/pyspec/tests/block_processing/test_process_transfer.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_transfer.py similarity index 53% rename from test_libs/pyspec/tests/block_processing/test_process_transfer.py rename to test_libs/pyspec/eth2spec/test/block_processing/test_process_transfer.py index 0eeaa7792..10d2ccede 100644 --- a/test_libs/pyspec/tests/block_processing/test_process_transfer.py +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_transfer.py @@ -1,6 +1,3 @@ -from copy import deepcopy -import pytest - import eth2spec.phase0.spec as spec from eth2spec.phase0.spec import ( @@ -9,54 +6,56 @@ from eth2spec.phase0.spec import ( get_current_epoch, process_transfer, ) -from tests.helpers import ( +from eth2spec.test.helpers import ( get_valid_transfer, next_epoch, ) - -# mark entire file as 'transfers' -pytestmark = pytest.mark.transfers +from eth2spec.test.context import spec_state_test, expect_assertion_error def run_transfer_processing(state, transfer, valid=True): """ - Run ``process_transfer`` returning the pre and post state. + Run ``process_transfer``, yielding: + - pre-state ('pre') + - transfer ('transfer') + - post-state ('post'). If ``valid == False``, run expecting ``AssertionError`` """ - post_state = deepcopy(state) - - if not valid: - with pytest.raises(AssertionError): - process_transfer(post_state, transfer) - return state, None - - - process_transfer(post_state, transfer) proposer_index = get_beacon_proposer_index(state) pre_transfer_sender_balance = state.balances[transfer.sender] pre_transfer_recipient_balance = state.balances[transfer.recipient] pre_transfer_proposer_balance = state.balances[proposer_index] - sender_balance = post_state.balances[transfer.sender] - recipient_balance = post_state.balances[transfer.recipient] + + yield 'pre', state + yield 'transfer', transfer + + if not valid: + expect_assertion_error(lambda: process_transfer(state, transfer)) + yield 'post', None + return + + process_transfer(state, transfer) + yield 'post', state + + sender_balance = state.balances[transfer.sender] + recipient_balance = state.balances[transfer.recipient] assert sender_balance == pre_transfer_sender_balance - transfer.amount - transfer.fee assert recipient_balance == pre_transfer_recipient_balance + transfer.amount - assert post_state.balances[proposer_index] == pre_transfer_proposer_balance + transfer.fee - - return state, post_state + assert state.balances[proposer_index] == pre_transfer_proposer_balance + transfer.fee +@spec_state_test def test_success_non_activated(state): transfer = get_valid_transfer(state) # un-activate so validator can transfer state.validator_registry[transfer.sender].activation_eligibility_epoch = spec.FAR_FUTURE_EPOCH - pre_state, post_state = run_transfer_processing(state, transfer) - - return pre_state, transfer, post_state + yield from run_transfer_processing(state, transfer) +@spec_state_test def test_success_withdrawable(state): next_epoch(state) @@ -65,58 +64,72 @@ def test_success_withdrawable(state): # withdrawable_epoch in past so can transfer state.validator_registry[transfer.sender].withdrawable_epoch = get_current_epoch(state) - 1 - pre_state, post_state = run_transfer_processing(state, transfer) - - return pre_state, transfer, post_state + yield from run_transfer_processing(state, transfer) +@spec_state_test def test_success_active_above_max_effective(state): sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] - amount = spec.MAX_EFFECTIVE_BALANCE // 32 - state.balances[sender_index] = spec.MAX_EFFECTIVE_BALANCE + amount - transfer = get_valid_transfer(state, sender_index=sender_index, amount=amount, fee=0) + state.balances[sender_index] = spec.MAX_EFFECTIVE_BALANCE + 1 + transfer = get_valid_transfer(state, sender_index=sender_index, amount=1, fee=0) - pre_state, post_state = run_transfer_processing(state, transfer) - - return pre_state, transfer, post_state + yield from run_transfer_processing(state, transfer) +@spec_state_test +def test_success_active_above_max_effective_fee(state): + sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] + state.balances[sender_index] = spec.MAX_EFFECTIVE_BALANCE + 1 + transfer = get_valid_transfer(state, sender_index=sender_index, amount=0, fee=1) + + yield from run_transfer_processing(state, transfer) + + +@spec_state_test def test_active_but_transfer_past_effective_balance(state): sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] amount = spec.MAX_EFFECTIVE_BALANCE // 32 state.balances[sender_index] = spec.MAX_EFFECTIVE_BALANCE transfer = get_valid_transfer(state, sender_index=sender_index, amount=amount, fee=0) - pre_state, post_state = run_transfer_processing(state, transfer, False) - - return pre_state, transfer, post_state + yield from run_transfer_processing(state, transfer, False) +@spec_state_test def test_incorrect_slot(state): transfer = get_valid_transfer(state, slot=state.slot+1) # un-activate so validator can transfer state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH - pre_state, post_state = run_transfer_processing(state, transfer, False) - - return pre_state, transfer, post_state + yield from run_transfer_processing(state, transfer, False) -def test_insufficient_balance(state): +@spec_state_test +def test_insufficient_balance_for_fee(state): sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] - amount = spec.MAX_EFFECTIVE_BALANCE state.balances[sender_index] = spec.MAX_EFFECTIVE_BALANCE - transfer = get_valid_transfer(state, sender_index=sender_index, amount=amount + 1, fee=0) + transfer = get_valid_transfer(state, sender_index=sender_index, amount=0, fee=1) # un-activate so validator can transfer state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH - pre_state, post_state = run_transfer_processing(state, transfer, False) - - return pre_state, transfer, post_state + yield from run_transfer_processing(state, transfer, False) -def test_no_dust(state): +@spec_state_test +def test_insufficient_balance(state): + sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] + state.balances[sender_index] = spec.MAX_EFFECTIVE_BALANCE + transfer = get_valid_transfer(state, sender_index=sender_index, amount=1, fee=0) + + # un-activate so validator can transfer + state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH + + yield from run_transfer_processing(state, transfer, False) + + +@spec_state_test +def test_no_dust_sender(state): sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] balance = state.balances[sender_index] transfer = get_valid_transfer(state, sender_index=sender_index, amount=balance - spec.MIN_DEPOSIT_AMOUNT + 1, fee=0) @@ -124,11 +137,23 @@ def test_no_dust(state): # un-activate so validator can transfer state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH - pre_state, post_state = run_transfer_processing(state, transfer, False) - - return pre_state, transfer, post_state + yield from run_transfer_processing(state, transfer, False) +@spec_state_test +def test_no_dust_recipient(state): + sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] + state.balances[sender_index] = spec.MAX_EFFECTIVE_BALANCE + 1 + transfer = get_valid_transfer(state, sender_index=sender_index, amount=1, fee=0) + state.balances[transfer.recipient] = 0 + + # un-activate so validator can transfer + state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH + + yield from run_transfer_processing(state, transfer, False) + + +@spec_state_test def test_invalid_pubkey(state): transfer = get_valid_transfer(state) state.validator_registry[transfer.sender].withdrawal_credentials = spec.ZERO_HASH @@ -136,6 +161,4 @@ def test_invalid_pubkey(state): # un-activate so validator can transfer state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH - pre_state, post_state = run_transfer_processing(state, transfer, False) - - return pre_state, transfer, post_state + yield from run_transfer_processing(state, transfer, False) diff --git a/test_libs/pyspec/tests/block_processing/test_voluntary_exit.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_voluntary_exit.py similarity index 53% rename from test_libs/pyspec/tests/block_processing/test_voluntary_exit.py rename to test_libs/pyspec/eth2spec/test/block_processing/test_process_voluntary_exit.py index c58c5238a..be0ef1e7a 100644 --- a/test_libs/pyspec/tests/block_processing/test_voluntary_exit.py +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_voluntary_exit.py @@ -1,6 +1,3 @@ -from copy import deepcopy -import pytest - import eth2spec.phase0.spec as spec from eth2spec.phase0.spec import ( @@ -9,37 +6,43 @@ from eth2spec.phase0.spec import ( get_current_epoch, process_voluntary_exit, ) -from tests.helpers import ( +from eth2spec.test.helpers import ( build_voluntary_exit, pubkey_to_privkey, ) - -# mark entire file as 'voluntary_exits' -pytestmark = pytest.mark.voluntary_exits +from eth2spec.test.context import spec_state_test, expect_assertion_error def run_voluntary_exit_processing(state, voluntary_exit, valid=True): """ - Run ``process_voluntary_exit`` returning the pre and post state. + Run ``process_voluntary_exit``, yielding: + - pre-state ('pre') + - voluntary_exit ('voluntary_exit') + - post-state ('post'). If ``valid == False``, run expecting ``AssertionError`` """ - post_state = deepcopy(state) + validator_index = voluntary_exit.validator_index + + yield 'pre', state + yield 'voluntary_exit', voluntary_exit if not valid: - with pytest.raises(AssertionError): - process_voluntary_exit(post_state, voluntary_exit) - return state, None + expect_assertion_error(lambda: process_voluntary_exit(state, voluntary_exit)) + yield 'post', None + return - process_voluntary_exit(post_state, voluntary_exit) + pre_exit_epoch = state.validator_registry[validator_index].exit_epoch - validator_index = voluntary_exit.validator_index - assert state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH - assert post_state.validator_registry[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH + process_voluntary_exit(state, voluntary_exit) - return state, post_state + yield 'post', state + + assert pre_exit_epoch == spec.FAR_FUTURE_EPOCH + assert state.validator_registry[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH +@spec_state_test def test_success(state): # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH @@ -55,10 +58,10 @@ def test_success(state): privkey, ) - pre_state, post_state = run_voluntary_exit_processing(state, voluntary_exit) - return pre_state, voluntary_exit, post_state + yield from run_voluntary_exit_processing(state, voluntary_exit) +@spec_state_test def test_success_exit_queue(state): # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH @@ -67,17 +70,23 @@ def test_success_exit_queue(state): # exit `MAX_EXITS_PER_EPOCH` initial_indices = get_active_validator_indices(state, current_epoch)[:get_churn_limit(state)] - post_state = state + + # Prepare a bunch of exits, based on the current state + exit_queue = [] for index in initial_indices: privkey = pubkey_to_privkey[state.validator_registry[index].pubkey] - voluntary_exit = build_voluntary_exit( + exit_queue.append(build_voluntary_exit( state, current_epoch, index, privkey, - ) + )) - pre_state, post_state = run_voluntary_exit_processing(post_state, voluntary_exit) + # Now run all the exits + for voluntary_exit in exit_queue: + # the function yields data, but we are just interested in running it here, ignore yields. + for _ in run_voluntary_exit_processing(state, voluntary_exit): + continue # exit an additional validator validator_index = get_active_validator_indices(state, current_epoch)[-1] @@ -89,16 +98,57 @@ def test_success_exit_queue(state): privkey, ) - pre_state, post_state = run_voluntary_exit_processing(post_state, voluntary_exit) + # This is the interesting part of the test: on a pre-state with a full exit queue, + # when processing an additional exit, it results in an exit in a later epoch + yield from run_voluntary_exit_processing(state, voluntary_exit) assert ( - post_state.validator_registry[validator_index].exit_epoch == - post_state.validator_registry[initial_indices[0]].exit_epoch + 1 + state.validator_registry[validator_index].exit_epoch == + state.validator_registry[initial_indices[0]].exit_epoch + 1 ) - return pre_state, voluntary_exit, post_state + +@spec_state_test +def test_validator_exit_in_future(state): + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + current_epoch = get_current_epoch(state) + validator_index = get_active_validator_indices(state, current_epoch)[0] + privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] + + voluntary_exit = build_voluntary_exit( + state, + current_epoch, + validator_index, + privkey, + ) + voluntary_exit.epoch += 1 + + yield from run_voluntary_exit_processing(state, voluntary_exit, False) +@spec_state_test +def test_validator_invalid_validator_index(state): + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + current_epoch = get_current_epoch(state) + validator_index = get_active_validator_indices(state, current_epoch)[0] + privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] + + voluntary_exit = build_voluntary_exit( + state, + current_epoch, + validator_index, + privkey, + ) + voluntary_exit.validator_index = len(state.validator_registry) + + yield from run_voluntary_exit_processing(state, voluntary_exit, False) + + +@spec_state_test def test_validator_not_active(state): current_epoch = get_current_epoch(state) validator_index = get_active_validator_indices(state, current_epoch)[0] @@ -106,9 +156,7 @@ def test_validator_not_active(state): state.validator_registry[validator_index].activation_epoch = spec.FAR_FUTURE_EPOCH - # # build and test voluntary exit - # voluntary_exit = build_voluntary_exit( state, current_epoch, @@ -116,10 +164,10 @@ def test_validator_not_active(state): privkey, ) - pre_state, post_state = run_voluntary_exit_processing(state, voluntary_exit, False) - return pre_state, voluntary_exit, post_state + yield from run_voluntary_exit_processing(state, voluntary_exit, False) +@spec_state_test def test_validator_already_exited(state): # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow validator able to exit state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH @@ -138,10 +186,10 @@ def test_validator_already_exited(state): privkey, ) - pre_state, post_state = run_voluntary_exit_processing(state, voluntary_exit, False) - return pre_state, voluntary_exit, post_state + yield from run_voluntary_exit_processing(state, voluntary_exit, False) +@spec_state_test def test_validator_not_active_long_enough(state): current_epoch = get_current_epoch(state) validator_index = get_active_validator_indices(state, current_epoch)[0] @@ -159,5 +207,4 @@ def test_validator_not_active_long_enough(state): spec.PERSISTENT_COMMITTEE_PERIOD ) - pre_state, post_state = run_voluntary_exit_processing(state, voluntary_exit, False) - return pre_state, voluntary_exit, post_state + yield from run_voluntary_exit_processing(state, voluntary_exit, False) diff --git a/test_libs/pyspec/eth2spec/test/conftest.py b/test_libs/pyspec/eth2spec/test/conftest.py new file mode 100644 index 000000000..dadb0d5d0 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/conftest.py @@ -0,0 +1,36 @@ +from eth2spec.phase0 import spec + +# We import pytest only when it's present, i.e. when we are running tests. +# The test-cases themselves can be generated without installing pytest. + +def module_exists(module_name): + try: + __import__(module_name) + except ImportError: + return False + else: + return True + + +def fixture(*args, **kwargs): + if module_exists("pytest"): + import pytest + return pytest.fixture(*args, **kwargs) + else: + def ignore(): + pass + return ignore + + +def pytest_addoption(parser): + parser.addoption( + "--config", action="store", default="minimal", help="config: make the pyspec use the specified configuration" + ) + + +@fixture(autouse=True) +def config(request): + config_name = request.config.getoption("--config") + from preset_loader import loader + presets = loader.load_presets('../../configs/', config_name) + spec.apply_constants_preset(presets) diff --git a/test_libs/pyspec/eth2spec/test/context.py b/test_libs/pyspec/eth2spec/test/context.py new file mode 100644 index 000000000..afabd4a57 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/context.py @@ -0,0 +1,27 @@ +from eth2spec.phase0 import spec + +from .helpers import create_genesis_state + +from .utils import spectest, with_args + +# Provides a genesis state as first argument to the function decorated with this +with_state = with_args(lambda: [create_genesis_state(spec.SLOTS_PER_EPOCH * 8, list())]) + + +# shorthand for decorating @with_state @spectest() +def spec_state_test(fn): + return with_state(spectest()(fn)) + + +def expect_assertion_error(fn): + bad = False + try: + fn() + bad = True + except AssertionError: + pass + except IndexError: + # Index errors are special; the spec is not explicit on bound checking, an IndexError is like a failed assert. + pass + if bad: + raise AssertionError('expected an assertion error, but got none.') diff --git a/test_libs/pyspec/eth2spec/test/epoch_processing/__init__.py b/test_libs/pyspec/eth2spec/test/epoch_processing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py b/test_libs/pyspec/eth2spec/test/epoch_processing/test_process_crosslinks.py similarity index 73% rename from test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py rename to test_libs/pyspec/eth2spec/test/epoch_processing/test_process_crosslinks.py index d6765e3a7..203978d29 100644 --- a/test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py +++ b/test_libs/pyspec/eth2spec/test/epoch_processing/test_process_crosslinks.py @@ -1,5 +1,4 @@ from copy import deepcopy -import pytest import eth2spec.phase0.spec as spec @@ -11,7 +10,7 @@ from eth2spec.phase0.spec import ( get_crosslink_deltas, process_crosslinks, ) -from tests.helpers import ( +from eth2spec.test.helpers import ( add_attestation_to_state, build_empty_block_for_next_slot, fill_aggregate_attestation, @@ -19,15 +18,17 @@ from tests.helpers import ( get_valid_attestation, next_epoch, next_slot, - set_bitfield_bit, ) - - -# mark entire file as 'crosslinks' -pytestmark = pytest.mark.crosslinks +from eth2spec.test.context import spec_state_test def run_process_crosslinks(state, valid=True): + """ + Run ``process_crosslinks``, yielding: + - pre-state ('pre') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ # transition state to slot before state transition slot = state.slot + (spec.SLOTS_PER_EPOCH - state.slot % spec.SLOTS_PER_EPOCH) - 1 block = build_empty_block_for_next_slot(state) @@ -37,21 +38,20 @@ def run_process_crosslinks(state, valid=True): # cache state before epoch transition cache_state(state) - post_state = deepcopy(state) - process_crosslinks(post_state) - - return state, post_state + yield 'pre', state + process_crosslinks(state) + yield 'post', state +@spec_state_test def test_no_attestations(state): - pre_state, post_state = run_process_crosslinks(state) + yield from run_process_crosslinks(state) for shard in range(spec.SHARD_COUNT): - assert post_state.previous_crosslinks[shard] == post_state.current_crosslinks[shard] - - return pre_state, post_state + assert state.previous_crosslinks[shard] == state.current_crosslinks[shard] +@spec_state_test def test_single_crosslink_update_from_current_epoch(state): next_epoch(state) @@ -62,15 +62,16 @@ def test_single_crosslink_update_from_current_epoch(state): assert len(state.current_epoch_attestations) == 1 - pre_state, post_state = run_process_crosslinks(state) - shard = attestation.data.shard - assert post_state.previous_crosslinks[shard] != post_state.current_crosslinks[shard] - assert pre_state.current_crosslinks[shard] != post_state.current_crosslinks[shard] + pre_crosslink = deepcopy(state.current_crosslinks[shard]) - return pre_state, post_state + yield from run_process_crosslinks(state) + + assert state.previous_crosslinks[shard] != state.current_crosslinks[shard] + assert pre_crosslink != state.current_crosslinks[shard] +@spec_state_test def test_single_crosslink_update_from_previous_epoch(state): next_epoch(state) @@ -81,20 +82,23 @@ def test_single_crosslink_update_from_previous_epoch(state): assert len(state.previous_epoch_attestations) == 1 - pre_state, post_state = run_process_crosslinks(state) + shard = attestation.data.shard + pre_crosslink = deepcopy(state.current_crosslinks[shard]) + crosslink_deltas = get_crosslink_deltas(state) - shard = attestation.data.shard - assert post_state.previous_crosslinks[shard] != post_state.current_crosslinks[shard] - assert pre_state.current_crosslinks[shard] != post_state.current_crosslinks[shard] + yield from run_process_crosslinks(state) + + assert state.previous_crosslinks[shard] != state.current_crosslinks[shard] + assert pre_crosslink != state.current_crosslinks[shard] + # ensure rewarded for index in get_crosslink_committee(state, attestation.data.target_epoch, attestation.data.shard): assert crosslink_deltas[0][index] > 0 assert crosslink_deltas[1][index] == 0 - return pre_state, post_state - +@spec_state_test def test_double_late_crosslink(state): next_epoch(state) state.slot += 4 @@ -121,16 +125,15 @@ def test_double_late_crosslink(state): assert len(state.previous_epoch_attestations) == 1 assert len(state.current_epoch_attestations) == 0 - pre_state, post_state = run_process_crosslinks(state) crosslink_deltas = get_crosslink_deltas(state) + + yield from run_process_crosslinks(state) shard = attestation_2.data.shard # ensure that the current crosslinks were not updated by the second attestation - assert post_state.previous_crosslinks[shard] == post_state.current_crosslinks[shard] + assert state.previous_crosslinks[shard] == state.current_crosslinks[shard] # ensure no reward, only penalties for the failed crosslink for index in get_crosslink_committee(state, attestation_2.data.target_epoch, attestation_2.data.shard): assert crosslink_deltas[0][index] == 0 assert crosslink_deltas[1][index] > 0 - - return pre_state, post_state diff --git a/test_libs/pyspec/tests/epoch_processing/test_process_registry_updates.py b/test_libs/pyspec/eth2spec/test/epoch_processing/test_process_registry_updates.py similarity index 79% rename from test_libs/pyspec/tests/epoch_processing/test_process_registry_updates.py rename to test_libs/pyspec/eth2spec/test/epoch_processing/test_process_registry_updates.py index 11f5de2ad..970c30942 100644 --- a/test_libs/pyspec/tests/epoch_processing/test_process_registry_updates.py +++ b/test_libs/pyspec/eth2spec/test/epoch_processing/test_process_registry_updates.py @@ -1,21 +1,17 @@ -from copy import deepcopy - -import pytest - import eth2spec.phase0.spec as spec from eth2spec.phase0.spec import ( get_current_epoch, is_active_validator, ) -from tests.helpers import ( +from eth2spec.test.helpers import ( next_epoch, ) -# mark entire file as 'state' -pytestmark = pytest.mark.state +from eth2spec.test.context import spec_state_test +@spec_state_test def test_activation(state): index = 0 assert is_active_validator(state.validator_registry[index], get_current_epoch(state)) @@ -26,13 +22,18 @@ def test_activation(state): state.validator_registry[index].effective_balance = spec.MAX_EFFECTIVE_BALANCE assert not is_active_validator(state.validator_registry[index], get_current_epoch(state)) - pre_state = deepcopy(state) + yield 'pre', state blocks = [] for _ in range(spec.ACTIVATION_EXIT_DELAY + 1): block = next_epoch(state) blocks.append(block) + # provide extra type hinting here, since it is wrapped in a list. + yield 'blocks', blocks, [spec.BeaconBlock] + + yield 'post', state + assert state.validator_registry[index].activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH assert state.validator_registry[index].activation_epoch != spec.FAR_FUTURE_EPOCH assert is_active_validator( @@ -40,9 +41,8 @@ def test_activation(state): get_current_epoch(state), ) - return pre_state, blocks, state - +@spec_state_test def test_ejection(state): index = 0 assert is_active_validator(state.validator_registry[index], get_current_epoch(state)) @@ -51,17 +51,20 @@ def test_ejection(state): # Mock an ejection state.validator_registry[index].effective_balance = spec.EJECTION_BALANCE - pre_state = deepcopy(state) + yield 'pre', state blocks = [] for _ in range(spec.ACTIVATION_EXIT_DELAY + 1): block = next_epoch(state) blocks.append(block) + # provide extra type hinting here, since it is wrapped in a list. + yield 'blocks', blocks, [spec.BeaconBlock] + + yield 'post', state + assert state.validator_registry[index].exit_epoch != spec.FAR_FUTURE_EPOCH assert not is_active_validator( state.validator_registry[index], get_current_epoch(state), ) - - return pre_state, blocks, state diff --git a/test_libs/pyspec/tests/helpers.py b/test_libs/pyspec/eth2spec/test/helpers.py similarity index 94% rename from test_libs/pyspec/tests/helpers.py rename to test_libs/pyspec/eth2spec/test/helpers.py index 3b9b6904d..8eb6d8891 100644 --- a/test_libs/pyspec/tests/helpers.py +++ b/test_libs/pyspec/eth2spec/test/helpers.py @@ -420,3 +420,26 @@ def get_state_root(state, slot) -> bytes: """ assert slot < state.slot <= slot + spec.SLOTS_PER_HISTORICAL_ROOT return state.latest_state_roots[slot % spec.SLOTS_PER_HISTORICAL_ROOT] + + +def prepare_state_and_deposit(state, validator_index, amount): + """ + Prepare the state for the deposit, and create a deposit for the given validator, depositing the given amount. + """ + pre_validator_count = len(state.validator_registry) + # fill previous deposits with zero-hash + deposit_data_leaves = [ZERO_HASH] * pre_validator_count + + pubkey = pubkeys[validator_index] + privkey = privkeys[validator_index] + deposit, root, deposit_data_leaves = build_deposit( + state, + deposit_data_leaves, + pubkey, + privkey, + amount, + ) + + state.latest_eth1_data.deposit_root = root + state.latest_eth1_data.deposit_count = len(deposit_data_leaves) + return deposit diff --git a/test_libs/pyspec/tests/test_finality.py b/test_libs/pyspec/eth2spec/test/test_finality.py similarity index 59% rename from test_libs/pyspec/tests/test_finality.py rename to test_libs/pyspec/eth2spec/test/test_finality.py index ca048c2b2..16bf24a4e 100644 --- a/test_libs/pyspec/tests/test_finality.py +++ b/test_libs/pyspec/eth2spec/test/test_finality.py @@ -1,7 +1,5 @@ from copy import deepcopy -import pytest - import eth2spec.phase0.spec as spec from eth2spec.phase0.state_transition import ( @@ -16,8 +14,7 @@ from .helpers import ( next_epoch, ) -# mark entire file as 'state' -pytestmark = pytest.mark.state +from .context import spec_state_test def check_finality(state, @@ -73,126 +70,130 @@ def next_epoch_with_attestations(state, return state, blocks, post_state +@spec_state_test def test_finality_rule_4(state): - test_state = deepcopy(state) + yield 'pre', state blocks = [] for epoch in range(4): - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, True, False) + prev_state, new_blocks, state = next_epoch_with_attestations(state, True, False) blocks += new_blocks # justification/finalization skipped at GENESIS_EPOCH if epoch == 0: - check_finality(test_state, prev_state, False, False, False) + check_finality(state, prev_state, False, False, False) # justification/finalization skipped at GENESIS_EPOCH + 1 elif epoch == 1: - check_finality(test_state, prev_state, False, False, False) + check_finality(state, prev_state, False, False, False) elif epoch == 2: - check_finality(test_state, prev_state, True, False, False) + check_finality(state, prev_state, True, False, False) elif epoch >= 3: # rule 4 of finality - check_finality(test_state, prev_state, True, True, True) - assert test_state.finalized_epoch == prev_state.current_justified_epoch - assert test_state.finalized_root == prev_state.current_justified_root + check_finality(state, prev_state, True, True, True) + assert state.finalized_epoch == prev_state.current_justified_epoch + assert state.finalized_root == prev_state.current_justified_root - return state, blocks, test_state + yield 'blocks', blocks, [spec.BeaconBlock] + yield 'post', state +@spec_state_test def test_finality_rule_1(state): # get past first two epochs that finality does not run on next_epoch(state) next_epoch(state) - pre_state = deepcopy(state) - test_state = deepcopy(state) + yield 'pre', state blocks = [] for epoch in range(3): - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, False, True) + prev_state, new_blocks, state = next_epoch_with_attestations(state, False, True) blocks += new_blocks if epoch == 0: - check_finality(test_state, prev_state, True, False, False) + check_finality(state, prev_state, True, False, False) elif epoch == 1: - check_finality(test_state, prev_state, True, True, False) + check_finality(state, prev_state, True, True, False) elif epoch == 2: # finalized by rule 1 - check_finality(test_state, prev_state, True, True, True) - assert test_state.finalized_epoch == prev_state.previous_justified_epoch - assert test_state.finalized_root == prev_state.previous_justified_root + check_finality(state, prev_state, True, True, True) + assert state.finalized_epoch == prev_state.previous_justified_epoch + assert state.finalized_root == prev_state.previous_justified_root - return pre_state, blocks, test_state + yield 'blocks', blocks, [spec.BeaconBlock] + yield 'post', state +@spec_state_test def test_finality_rule_2(state): # get past first two epochs that finality does not run on next_epoch(state) next_epoch(state) - pre_state = deepcopy(state) - test_state = deepcopy(state) + yield 'pre', state blocks = [] for epoch in range(3): if epoch == 0: - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, True, False) - check_finality(test_state, prev_state, True, False, False) + prev_state, new_blocks, state = next_epoch_with_attestations(state, True, False) + check_finality(state, prev_state, True, False, False) elif epoch == 1: - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, False, False) - check_finality(test_state, prev_state, False, True, False) + prev_state, new_blocks, state = next_epoch_with_attestations(state, False, False) + check_finality(state, prev_state, False, True, False) elif epoch == 2: - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, False, True) + prev_state, new_blocks, state = next_epoch_with_attestations(state, False, True) # finalized by rule 2 - check_finality(test_state, prev_state, True, False, True) - assert test_state.finalized_epoch == prev_state.previous_justified_epoch - assert test_state.finalized_root == prev_state.previous_justified_root + check_finality(state, prev_state, True, False, True) + assert state.finalized_epoch == prev_state.previous_justified_epoch + assert state.finalized_root == prev_state.previous_justified_root blocks += new_blocks - return pre_state, blocks, test_state + yield 'blocks', blocks, [spec.BeaconBlock] + yield 'post', state +@spec_state_test def test_finality_rule_3(state): """ Test scenario described here https://github.com/ethereum/eth2.0-specs/issues/611#issuecomment-463612892 """ - # get past first two epochs that finality does not run on next_epoch(state) next_epoch(state) - pre_state = deepcopy(state) - test_state = deepcopy(state) + yield 'pre', state blocks = [] - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, True, False) + prev_state, new_blocks, state = next_epoch_with_attestations(state, True, False) blocks += new_blocks - check_finality(test_state, prev_state, True, False, False) + check_finality(state, prev_state, True, False, False) # In epoch N, JE is set to N, prev JE is set to N-1 - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, True, False) + prev_state, new_blocks, state = next_epoch_with_attestations(state, True, False) blocks += new_blocks - check_finality(test_state, prev_state, True, True, True) + check_finality(state, prev_state, True, True, True) # In epoch N+1, JE is N, prev JE is N-1, and not enough messages get in to do anything - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, False, False) + prev_state, new_blocks, state = next_epoch_with_attestations(state, False, False) blocks += new_blocks - check_finality(test_state, prev_state, False, True, False) + check_finality(state, prev_state, False, True, False) # In epoch N+2, JE is N, prev JE is N, and enough messages from the previous epoch get in to justify N+1. # N+1 now becomes the JE. Not enough messages from epoch N+2 itself get in to justify N+2 - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, False, True) + prev_state, new_blocks, state = next_epoch_with_attestations(state, False, True) blocks += new_blocks # rule 2 - check_finality(test_state, prev_state, True, False, True) + check_finality(state, prev_state, True, False, True) # In epoch N+3, LJE is N+1, prev LJE is N, and enough messages get in to justify epochs N+2 and N+3. - prev_state, new_blocks, test_state = next_epoch_with_attestations(test_state, True, True) + prev_state, new_blocks, state = next_epoch_with_attestations(state, True, True) blocks += new_blocks # rule 3 - check_finality(test_state, prev_state, True, True, True) - assert test_state.finalized_epoch == prev_state.current_justified_epoch - assert test_state.finalized_root == prev_state.current_justified_root + check_finality(state, prev_state, True, True, True) + assert state.finalized_epoch == prev_state.current_justified_epoch + assert state.finalized_root == prev_state.current_justified_root - return pre_state, blocks, test_state + yield 'blocks', blocks, [spec.BeaconBlock] + yield 'post', state diff --git a/test_libs/pyspec/eth2spec/test/test_sanity.py b/test_libs/pyspec/eth2spec/test/test_sanity.py new file mode 100644 index 000000000..1951415ac --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/test_sanity.py @@ -0,0 +1,413 @@ +from copy import deepcopy + +from py_ecc import bls +import eth2spec.phase0.spec as spec + +from eth2spec.utils.minimal_ssz import signing_root +from eth2spec.phase0.spec import ( + # SSZ + VoluntaryExit, + # functions + get_active_validator_indices, + get_beacon_proposer_index, + get_block_root_at_slot, + get_current_epoch, + get_domain, + advance_slot, + cache_state, +) +from eth2spec.phase0.state_transition import ( + state_transition, +) +from .helpers import ( + get_balance, + build_empty_block_for_next_slot, + get_state_root, + get_valid_attestation, + get_valid_attester_slashing, + get_valid_proposer_slashing, + get_valid_transfer, + prepare_state_and_deposit, + privkeys, + pubkeys, +) + +from .context import spec_state_test + + +@spec_state_test +def test_slot_transition(state): + pre_slot = state.slot + pre_root = state.hash_tree_root() + yield 'pre', state + + cache_state(state) + advance_slot(state) + yield 'post', state + + assert state.slot == pre_slot + 1 + assert get_state_root(state, pre_slot) == pre_root + + +@spec_state_test +def test_empty_block_transition(state): + pre_slot = state.slot + pre_eth1_votes = len(state.eth1_data_votes) + + yield 'pre', state + + block = build_empty_block_for_next_slot(state) + yield 'blocks', [block], [spec.BeaconBlock] + + state_transition(state, block) + yield 'post', state + + assert len(state.eth1_data_votes) == pre_eth1_votes + 1 + assert get_block_root_at_slot(state, pre_slot) == block.previous_block_root + + +@spec_state_test +def test_skipped_slots(state): + pre_slot = state.slot + yield 'pre', state + + block = build_empty_block_for_next_slot(state) + block.slot += 3 + yield 'blocks', [block], [spec.BeaconBlock] + + state_transition(state, block) + yield 'post', state + + assert state.slot == block.slot + for slot in range(pre_slot, state.slot): + assert get_block_root_at_slot(state, slot) == block.previous_block_root + + +@spec_state_test +def test_empty_epoch_transition(state): + pre_slot = state.slot + yield 'pre', state + + block = build_empty_block_for_next_slot(state) + block.slot += spec.SLOTS_PER_EPOCH + yield 'blocks', [block], [spec.BeaconBlock] + + state_transition(state, block) + yield 'post', state + + assert state.slot == block.slot + for slot in range(pre_slot, state.slot): + assert get_block_root_at_slot(state, slot) == block.previous_block_root + + +@spec_state_test +def test_empty_epoch_transition_not_finalizing(state): + # copy for later balance lookups. + pre_state = deepcopy(state) + yield 'pre', state + + block = build_empty_block_for_next_slot(state) + block.slot += spec.SLOTS_PER_EPOCH * 5 + yield 'blocks', [block], [spec.BeaconBlock] + + state_transition(state, block) + yield 'post', state + + assert state.slot == block.slot + assert state.finalized_epoch < get_current_epoch(state) - 4 + for index in range(len(state.validator_registry)): + assert get_balance(state, index) < get_balance(pre_state, index) + + +@spec_state_test +def test_proposer_slashing(state): + # copy for later balance lookups. + pre_state = deepcopy(state) + proposer_slashing = get_valid_proposer_slashing(state) + validator_index = proposer_slashing.proposer_index + + assert not state.validator_registry[validator_index].slashed + + yield 'pre', state + + # + # Add to state via block transition + # + block = build_empty_block_for_next_slot(state) + block.body.proposer_slashings.append(proposer_slashing) + yield 'blocks', [block], [spec.BeaconBlock] + + state_transition(state, block) + yield 'post', state + + # check if slashed + slashed_validator = state.validator_registry[validator_index] + assert slashed_validator.slashed + assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + # lost whistleblower reward + assert get_balance(state, validator_index) < get_balance(pre_state, validator_index) + + +@spec_state_test +def test_attester_slashing(state): + # copy for later balance lookups. + pre_state = deepcopy(state) + + attester_slashing = get_valid_attester_slashing(state) + validator_index = attester_slashing.attestation_1.custody_bit_0_indices[0] + + assert not state.validator_registry[validator_index].slashed + + yield 'pre', state + + # + # Add to state via block transition + # + block = build_empty_block_for_next_slot(state) + block.body.attester_slashings.append(attester_slashing) + yield 'blocks', [block], [spec.BeaconBlock] + + state_transition(state, block) + yield 'post', state + + slashed_validator = state.validator_registry[validator_index] + assert slashed_validator.slashed + assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + # lost whistleblower reward + assert get_balance(state, validator_index) < get_balance(pre_state, validator_index) + + proposer_index = get_beacon_proposer_index(state) + # gained whistleblower reward + assert ( + get_balance(state, proposer_index) > + get_balance(pre_state, proposer_index) + ) + + +# TODO update functions below to be like above, i.e. with @spec_state_test and yielding data to put into the test vector + +@spec_state_test +def test_deposit_in_block(state): + initial_registry_len = len(state.validator_registry) + initial_balances_len = len(state.balances) + + validator_index = len(state.validator_registry) + amount = spec.MAX_EFFECTIVE_BALANCE + deposit = prepare_state_and_deposit(state, validator_index, amount) + + yield 'pre', state + + block = build_empty_block_for_next_slot(state) + block.body.deposits.append(deposit) + + yield 'blocks', [block], [spec.BeaconBlock] + + state_transition(state, block) + yield 'post', state + + assert len(state.validator_registry) == initial_registry_len + 1 + assert len(state.balances) == initial_balances_len + 1 + assert get_balance(state, validator_index) == spec.MAX_EFFECTIVE_BALANCE + assert state.validator_registry[validator_index].pubkey == pubkeys[validator_index] + + +@spec_state_test +def test_deposit_top_up(state): + validator_index = 0 + amount = spec.MAX_EFFECTIVE_BALANCE // 4 + deposit = prepare_state_and_deposit(state, validator_index, amount) + + initial_registry_len = len(state.validator_registry) + initial_balances_len = len(state.balances) + validator_pre_balance = get_balance(state, validator_index) + + yield 'pre', state + + block = build_empty_block_for_next_slot(state) + block.body.deposits.append(deposit) + + yield 'blocks', [block], [spec.BeaconBlock] + + state_transition(state, block) + yield 'post', state + + assert len(state.validator_registry) == initial_registry_len + assert len(state.balances) == initial_balances_len + assert get_balance(state, validator_index) == validator_pre_balance + amount + + +@spec_state_test +def test_attestation(state): + state.slot = spec.SLOTS_PER_EPOCH + + yield 'pre', state + + attestation = get_valid_attestation(state) + + # Add to state via block transition + pre_current_attestations_len = len(state.current_epoch_attestations) + attestation_block = build_empty_block_for_next_slot(state) + attestation_block.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + attestation_block.body.attestations.append(attestation) + state_transition(state, attestation_block) + + assert len(state.current_epoch_attestations) == pre_current_attestations_len + 1 + + + # Epoch transition should move to previous_epoch_attestations + pre_current_attestations_root = spec.hash_tree_root(state.current_epoch_attestations) + + epoch_block = build_empty_block_for_next_slot(state) + epoch_block.slot += spec.SLOTS_PER_EPOCH + state_transition(state, epoch_block) + + yield 'blocks', [attestation_block, epoch_block], [spec.BeaconBlock] + yield 'post', state + + assert len(state.current_epoch_attestations) == 0 + assert spec.hash_tree_root(state.previous_epoch_attestations) == pre_current_attestations_root + + +@spec_state_test +def test_voluntary_exit(state): + validator_index = get_active_validator_indices( + state, + get_current_epoch(state) + )[-1] + + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + yield 'pre', state + + voluntary_exit = VoluntaryExit( + epoch=get_current_epoch(state), + validator_index=validator_index, + ) + voluntary_exit.signature = bls.sign( + message_hash=signing_root(voluntary_exit), + privkey=privkeys[validator_index], + domain=get_domain( + state=state, + domain_type=spec.DOMAIN_VOLUNTARY_EXIT, + ) + ) + + # Add to state via block transition + initiate_exit_block = build_empty_block_for_next_slot(state) + initiate_exit_block.body.voluntary_exits.append(voluntary_exit) + state_transition(state, initiate_exit_block) + + assert state.validator_registry[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH + + # Process within epoch transition + exit_block = build_empty_block_for_next_slot(state) + exit_block.slot += spec.SLOTS_PER_EPOCH + state_transition(state, exit_block) + + yield 'blocks', [initiate_exit_block, exit_block], [spec.BeaconBlock] + yield 'post', state + + assert state.validator_registry[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH + + +@spec_state_test +def test_transfer(state): + # overwrite default 0 to test + spec.MAX_TRANSFERS = 1 + + sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] + amount = get_balance(state, sender_index) + + transfer = get_valid_transfer(state, state.slot + 1, sender_index, amount) + recipient_index = transfer.recipient + pre_transfer_recipient_balance = get_balance(state, recipient_index) + + # un-activate so validator can transfer + state.validator_registry[sender_index].activation_eligibility_epoch = spec.FAR_FUTURE_EPOCH + + yield 'pre', state + + # Add to state via block transition + block = build_empty_block_for_next_slot(state) + block.body.transfers.append(transfer) + + yield 'blocks', [block], [spec.BeaconBlock] + + state_transition(state, block) + yield 'post', state + + sender_balance = get_balance(state, sender_index) + recipient_balance = get_balance(state, recipient_index) + assert sender_balance == 0 + assert recipient_balance == pre_transfer_recipient_balance + amount + + +@spec_state_test +def test_balance_driven_status_transitions(state): + current_epoch = get_current_epoch(state) + validator_index = get_active_validator_indices(state, current_epoch)[-1] + + assert state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH + + # set validator balance to below ejection threshold + state.validator_registry[validator_index].effective_balance = spec.EJECTION_BALANCE + + yield 'pre', state + + # trigger epoch transition + block = build_empty_block_for_next_slot(state) + block.slot += spec.SLOTS_PER_EPOCH + state_transition(state, block) + + yield 'blocks', [block], [spec.BeaconBlock] + yield 'post', state + + assert state.validator_registry[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH + + +@spec_state_test +def test_historical_batch(state): + state.slot += spec.SLOTS_PER_HISTORICAL_ROOT - (state.slot % spec.SLOTS_PER_HISTORICAL_ROOT) - 1 + pre_historical_roots_len = len(state.historical_roots) + + yield 'pre', state + + block = build_empty_block_for_next_slot(state) + state_transition(state, block) + + yield 'blocks', [block], [spec.BeaconBlock] + yield 'post', state + + assert state.slot == block.slot + assert get_current_epoch(state) % (spec.SLOTS_PER_HISTORICAL_ROOT // spec.SLOTS_PER_EPOCH) == 0 + assert len(state.historical_roots) == pre_historical_roots_len + 1 + + +@spec_state_test +def test_eth1_data_votes(state): + yield 'pre', state + + expected_votes = 0 + assert len(state.eth1_data_votes) == expected_votes + + blocks = [] + for _ in range(spec.SLOTS_PER_ETH1_VOTING_PERIOD - 1): + block = build_empty_block_for_next_slot(state) + state_transition(state, block) + expected_votes += 1 + assert len(state.eth1_data_votes) == expected_votes + blocks.append(block) + + block = build_empty_block_for_next_slot(state) + blocks.append(block) + + state_transition(state, block) + + yield 'blocks', [block], [spec.BeaconBlock] + yield 'post', state + + assert state.slot % spec.SLOTS_PER_ETH1_VOTING_PERIOD == 0 + assert len(state.eth1_data_votes) == 1 diff --git a/test_libs/pyspec/eth2spec/test/utils.py b/test_libs/pyspec/eth2spec/test/utils.py new file mode 100644 index 000000000..b19d4df59 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/utils.py @@ -0,0 +1,49 @@ +from eth2spec.debug.encode import encode + + +def spectest(description: str = None): + def runner(fn): + # this wraps the function, to hide that the function actually is yielding data, instead of returning once. + def entry(*args, **kw): + # check generator mode, may be None/else. + # "pop" removes it, so it is not passed to the inner function. + if kw.pop('generator_mode', False) is True: + out = {} + if description is None: + # fall back on function name for test description + name = fn.__name__ + if name.startswith('test_'): + name = name[5:] + out['description'] = name + else: + # description can be explicit + out['description'] = description + # put all generated data into a dict. + for data in fn(*args, **kw): + # If there is a type argument, encode it as that type. + if len(data) == 3: + (key, value, typ) = data + out[key] = encode(value, typ) + else: + # Otherwise, try to infer the type, but keep it as-is if it's not a SSZ container. + (key, value) = data + if hasattr(value.__class__, 'fields'): + out[key] = encode(value, value.__class__) + else: + out[key] = value + return out + else: + # just complete the function, ignore all yielded data, we are not using it + for _ in fn(*args, **kw): + continue + return entry + return runner + + +def with_args(create_args): + def runner(fn): + # this wraps the function, to hide that the function actually yielding data. + def entry(*args, **kw): + return fn(*(create_args() + list(args)), **kw) + return entry + return runner diff --git a/test_libs/pyspec/tests/block_processing/test_process_attestation.py b/test_libs/pyspec/tests/block_processing/test_process_attestation.py deleted file mode 100644 index bcf71376c..000000000 --- a/test_libs/pyspec/tests/block_processing/test_process_attestation.py +++ /dev/null @@ -1,155 +0,0 @@ -from copy import deepcopy -import pytest - -import eth2spec.phase0.spec as spec - -from eth2spec.phase0.state_transition import ( - state_transition, -) -from eth2spec.phase0.spec import ( - get_current_epoch, - process_attestation, - slot_to_epoch, -) -from tests.helpers import ( - build_empty_block_for_next_slot, - get_valid_attestation, - next_epoch, - next_slot, -) - - -# mark entire file as 'attestations' -pytestmark = pytest.mark.attestations - - -def run_attestation_processing(state, attestation, valid=True): - """ - Run ``process_attestation`` returning the pre and post state. - If ``valid == False``, run expecting ``AssertionError`` - """ - post_state = deepcopy(state) - - if not valid: - with pytest.raises(AssertionError): - process_attestation(post_state, attestation) - return state, None - - process_attestation(post_state, attestation) - - current_epoch = get_current_epoch(state) - if attestation.data.target_epoch == current_epoch: - assert len(post_state.current_epoch_attestations) == len(state.current_epoch_attestations) + 1 - else: - assert len(post_state.previous_epoch_attestations) == len(state.previous_epoch_attestations) + 1 - - return state, post_state - - -def test_success(state): - attestation = get_valid_attestation(state) - state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY - - pre_state, post_state = run_attestation_processing(state, attestation) - - return pre_state, attestation, post_state - - -def test_success_prevous_epoch(state): - attestation = get_valid_attestation(state) - block = build_empty_block_for_next_slot(state) - block.slot = state.slot + spec.SLOTS_PER_EPOCH - state_transition(state, block) - - pre_state, post_state = run_attestation_processing(state, attestation) - - return pre_state, attestation, post_state - - -def test_before_inclusion_delay(state): - attestation = get_valid_attestation(state) - # do not increment slot to allow for inclusion delay - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state - - -def test_after_epoch_slots(state): - attestation = get_valid_attestation(state) - block = build_empty_block_for_next_slot(state) - # increment past latest inclusion slot - block.slot = state.slot + spec.SLOTS_PER_EPOCH + 1 - state_transition(state, block) - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state - - -def test_bad_source_epoch(state): - attestation = get_valid_attestation(state) - state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY - - attestation.data.source_epoch += 10 - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state - - -def test_bad_source_root(state): - attestation = get_valid_attestation(state) - state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY - - attestation.data.source_root = b'\x42' * 32 - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state - - -def test_non_zero_crosslink_data_root(state): - attestation = get_valid_attestation(state) - state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY - - attestation.data.crosslink_data_root = b'\x42' * 32 - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state - - -def test_bad_previous_crosslink(state): - next_epoch(state) - attestation = get_valid_attestation(state) - for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): - next_slot(state) - - state.current_crosslinks[attestation.data.shard].epoch += 10 - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state - - -def test_non_empty_custody_bitfield(state): - attestation = get_valid_attestation(state) - state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY - - attestation.custody_bitfield = deepcopy(attestation.aggregation_bitfield) - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state - - -def test_empty_aggregation_bitfield(state): - attestation = get_valid_attestation(state) - state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY - - attestation.aggregation_bitfield = b'\x00' * len(attestation.aggregation_bitfield) - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state diff --git a/test_libs/pyspec/tests/block_processing/test_process_deposit.py b/test_libs/pyspec/tests/block_processing/test_process_deposit.py deleted file mode 100644 index bbfb390ef..000000000 --- a/test_libs/pyspec/tests/block_processing/test_process_deposit.py +++ /dev/null @@ -1,141 +0,0 @@ -from copy import deepcopy -import pytest - -import eth2spec.phase0.spec as spec - -from eth2spec.phase0.spec import ( - ZERO_HASH, - process_deposit, -) -from tests.helpers import ( - get_balance, - build_deposit, - privkeys, - pubkeys, -) - - -# mark entire file as 'deposits' -pytestmark = pytest.mark.deposits - - -def test_success(state): - pre_state = deepcopy(state) - # fill previous deposits with zero-hash - deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry) - - index = len(deposit_data_leaves) - pubkey = pubkeys[index] - privkey = privkeys[index] - deposit, root, deposit_data_leaves = build_deposit( - pre_state, - deposit_data_leaves, - pubkey, - privkey, - spec.MAX_EFFECTIVE_BALANCE, - ) - - pre_state.latest_eth1_data.deposit_root = root - pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves) - - post_state = deepcopy(pre_state) - - process_deposit(post_state, deposit) - - assert len(post_state.validator_registry) == len(state.validator_registry) + 1 - assert len(post_state.balances) == len(state.balances) + 1 - assert post_state.validator_registry[index].pubkey == pubkeys[index] - assert get_balance(post_state, index) == spec.MAX_EFFECTIVE_BALANCE - assert post_state.deposit_index == post_state.latest_eth1_data.deposit_count - - return pre_state, deposit, post_state - - -def test_success_top_up(state): - pre_state = deepcopy(state) - deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry) - - validator_index = 0 - amount = spec.MAX_EFFECTIVE_BALANCE // 4 - pubkey = pubkeys[validator_index] - privkey = privkeys[validator_index] - deposit, root, deposit_data_leaves = build_deposit( - pre_state, - deposit_data_leaves, - pubkey, - privkey, - amount, - ) - - pre_state.latest_eth1_data.deposit_root = root - pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves) - pre_balance = get_balance(pre_state, validator_index) - - post_state = deepcopy(pre_state) - - process_deposit(post_state, deposit) - - assert len(post_state.validator_registry) == len(state.validator_registry) - assert len(post_state.balances) == len(state.balances) - assert post_state.deposit_index == post_state.latest_eth1_data.deposit_count - assert get_balance(post_state, validator_index) == pre_balance + amount - - return pre_state, deposit, post_state - - -def test_wrong_index(state): - pre_state = deepcopy(state) - deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry) - - index = len(deposit_data_leaves) - pubkey = pubkeys[index] - privkey = privkeys[index] - deposit, root, deposit_data_leaves = build_deposit( - pre_state, - deposit_data_leaves, - pubkey, - privkey, - spec.MAX_EFFECTIVE_BALANCE, - ) - - # mess up deposit_index - deposit.index = pre_state.deposit_index + 1 - - pre_state.latest_eth1_data.deposit_root = root - pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves) - - post_state = deepcopy(pre_state) - - with pytest.raises(AssertionError): - process_deposit(post_state, deposit) - - return pre_state, deposit, None - - -def test_bad_merkle_proof(state): - pre_state = deepcopy(state) - deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry) - - index = len(deposit_data_leaves) - pubkey = pubkeys[index] - privkey = privkeys[index] - deposit, root, deposit_data_leaves = build_deposit( - pre_state, - deposit_data_leaves, - pubkey, - privkey, - spec.MAX_EFFECTIVE_BALANCE, - ) - - # mess up merkle branch - deposit.proof[-1] = spec.ZERO_HASH - - pre_state.latest_eth1_data.deposit_root = root - pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves) - - post_state = deepcopy(pre_state) - - with pytest.raises(AssertionError): - process_deposit(post_state, deposit) - - return pre_state, deposit, None diff --git a/test_libs/pyspec/tests/block_processing/test_process_proposer_slashing.py b/test_libs/pyspec/tests/block_processing/test_process_proposer_slashing.py deleted file mode 100644 index 475221036..000000000 --- a/test_libs/pyspec/tests/block_processing/test_process_proposer_slashing.py +++ /dev/null @@ -1,96 +0,0 @@ -from copy import deepcopy -import pytest - -import eth2spec.phase0.spec as spec -from eth2spec.phase0.spec import ( - get_current_epoch, - process_proposer_slashing, -) -from tests.helpers import ( - get_balance, - get_valid_proposer_slashing, -) - -# mark entire file as 'proposer_slashings' -pytestmark = pytest.mark.proposer_slashings - - -def run_proposer_slashing_processing(state, proposer_slashing, valid=True): - """ - Run ``process_proposer_slashing`` returning the pre and post state. - If ``valid == False``, run expecting ``AssertionError`` - """ - post_state = deepcopy(state) - - if not valid: - with pytest.raises(AssertionError): - process_proposer_slashing(post_state, proposer_slashing) - return state, None - - process_proposer_slashing(post_state, proposer_slashing) - - slashed_validator = post_state.validator_registry[proposer_slashing.proposer_index] - assert slashed_validator.slashed - assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH - assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH - # lost whistleblower reward - assert ( - get_balance(post_state, proposer_slashing.proposer_index) < - get_balance(state, proposer_slashing.proposer_index) - ) - - return state, post_state - - -def test_success(state): - proposer_slashing = get_valid_proposer_slashing(state) - - pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing) - - return pre_state, proposer_slashing, post_state - - -def test_epochs_are_different(state): - proposer_slashing = get_valid_proposer_slashing(state) - - # set slots to be in different epochs - proposer_slashing.header_2.slot += spec.SLOTS_PER_EPOCH - - pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False) - - return pre_state, proposer_slashing, post_state - - -def test_headers_are_same(state): - proposer_slashing = get_valid_proposer_slashing(state) - - # set headers to be the same - proposer_slashing.header_2 = proposer_slashing.header_1 - - pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False) - - return pre_state, proposer_slashing, post_state - - -def test_proposer_is_slashed(state): - proposer_slashing = get_valid_proposer_slashing(state) - - # set proposer to slashed - state.validator_registry[proposer_slashing.proposer_index].slashed = True - - pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False) - - return pre_state, proposer_slashing, post_state - - -def test_proposer_is_withdrawn(state): - proposer_slashing = get_valid_proposer_slashing(state) - - # set proposer withdrawable_epoch in past - current_epoch = get_current_epoch(state) - proposer_index = proposer_slashing.proposer_index - state.validator_registry[proposer_index].withdrawable_epoch = current_epoch - 1 - - pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False) - - return pre_state, proposer_slashing, post_state diff --git a/test_libs/pyspec/tests/conftest.py b/test_libs/pyspec/tests/conftest.py deleted file mode 100644 index 9840dc7b2..000000000 --- a/test_libs/pyspec/tests/conftest.py +++ /dev/null @@ -1,36 +0,0 @@ -import pytest - -from eth2spec.phase0 import spec -from preset_loader import loader - -from .helpers import ( - create_genesis_state, -) - - -def pytest_addoption(parser): - parser.addoption( - "--config", action="store", default="minimal", help="config: make the pyspec use the specified configuration" - ) - - -@pytest.fixture(autouse=True) -def config(request): - config_name = request.config.getoption("--config") - presets = loader.load_presets('../../configs/', config_name) - spec.apply_constants_preset(presets) - - -@pytest.fixture -def num_validators(config): - return spec.SLOTS_PER_EPOCH * 8 - - -@pytest.fixture -def deposit_data_leaves(): - return list() - - -@pytest.fixture -def state(num_validators, deposit_data_leaves): - return create_genesis_state(num_validators, deposit_data_leaves) diff --git a/test_libs/pyspec/tests/test_sanity.py b/test_libs/pyspec/tests/test_sanity.py deleted file mode 100644 index 1b4d20f4c..000000000 --- a/test_libs/pyspec/tests/test_sanity.py +++ /dev/null @@ -1,438 +0,0 @@ -from copy import deepcopy - -import pytest - -from py_ecc import bls -import eth2spec.phase0.spec as spec - -from eth2spec.utils.minimal_ssz import signing_root -from eth2spec.phase0.spec import ( - # constants - ZERO_HASH, - SLOTS_PER_HISTORICAL_ROOT, - # SSZ - Deposit, - Transfer, - VoluntaryExit, - # functions - get_active_validator_indices, - get_beacon_proposer_index, - get_block_root_at_slot, - get_current_epoch, - get_domain, - advance_slot, - cache_state, - verify_merkle_branch, - hash, -) -from eth2spec.phase0.state_transition import ( - state_transition, -) -from eth2spec.utils.merkle_minimal import ( - calc_merkle_tree_from_leaves, - get_merkle_proof, - get_merkle_root, -) -from .helpers import ( - get_balance, - build_deposit_data, - build_empty_block_for_next_slot, - fill_aggregate_attestation, - get_state_root, - get_valid_attestation, - get_valid_attester_slashing, - get_valid_proposer_slashing, - next_slot, - privkeys, - pubkeys, -) - - -# mark entire file as 'sanity' -pytestmark = pytest.mark.sanity - - -def test_slot_transition(state): - test_state = deepcopy(state) - cache_state(test_state) - advance_slot(test_state) - assert test_state.slot == state.slot + 1 - assert get_state_root(test_state, state.slot) == state.hash_tree_root() - return test_state - - -def test_empty_block_transition(state): - test_state = deepcopy(state) - - block = build_empty_block_for_next_slot(test_state) - state_transition(test_state, block) - - assert len(test_state.eth1_data_votes) == len(state.eth1_data_votes) + 1 - assert get_block_root_at_slot(test_state, state.slot) == block.previous_block_root - - return state, [block], test_state - - -def test_skipped_slots(state): - test_state = deepcopy(state) - block = build_empty_block_for_next_slot(test_state) - block.slot += 3 - - state_transition(test_state, block) - - assert test_state.slot == block.slot - for slot in range(state.slot, test_state.slot): - assert get_block_root_at_slot(test_state, slot) == block.previous_block_root - - return state, [block], test_state - - -def test_empty_epoch_transition(state): - test_state = deepcopy(state) - block = build_empty_block_for_next_slot(test_state) - block.slot += spec.SLOTS_PER_EPOCH - - state_transition(test_state, block) - - assert test_state.slot == block.slot - for slot in range(state.slot, test_state.slot): - assert get_block_root_at_slot(test_state, slot) == block.previous_block_root - - return state, [block], test_state - - -def test_empty_epoch_transition_not_finalizing(state): - test_state = deepcopy(state) - block = build_empty_block_for_next_slot(test_state) - block.slot += spec.SLOTS_PER_EPOCH * 5 - - state_transition(test_state, block) - - assert test_state.slot == block.slot - assert test_state.finalized_epoch < get_current_epoch(test_state) - 4 - for index in range(len(test_state.validator_registry)): - assert get_balance(test_state, index) < get_balance(state, index) - - return state, [block], test_state - - -def test_proposer_slashing(state): - test_state = deepcopy(state) - proposer_slashing = get_valid_proposer_slashing(state) - validator_index = proposer_slashing.proposer_index - - # - # Add to state via block transition - # - block = build_empty_block_for_next_slot(test_state) - block.body.proposer_slashings.append(proposer_slashing) - state_transition(test_state, block) - - assert not state.validator_registry[validator_index].slashed - - slashed_validator = test_state.validator_registry[validator_index] - assert slashed_validator.slashed - assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH - assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH - # lost whistleblower reward - assert get_balance(test_state, validator_index) < get_balance(state, validator_index) - - return state, [block], test_state - - -def test_attester_slashing(state): - test_state = deepcopy(state) - attester_slashing = get_valid_attester_slashing(state) - validator_index = attester_slashing.attestation_1.custody_bit_0_indices[0] - - # - # Add to state via block transition - # - block = build_empty_block_for_next_slot(test_state) - block.body.attester_slashings.append(attester_slashing) - state_transition(test_state, block) - - assert not state.validator_registry[validator_index].slashed - - slashed_validator = test_state.validator_registry[validator_index] - assert slashed_validator.slashed - assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH - assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH - # lost whistleblower reward - assert get_balance(test_state, validator_index) < get_balance(state, validator_index) - - proposer_index = get_beacon_proposer_index(test_state) - # gained whistleblower reward - assert ( - get_balance(test_state, proposer_index) > - get_balance(state, proposer_index) - ) - - return state, [block], test_state - - -def test_deposit_in_block(state): - pre_state = deepcopy(state) - test_deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry) - - index = len(test_deposit_data_leaves) - pubkey = pubkeys[index] - privkey = privkeys[index] - deposit_data = build_deposit_data(pre_state, pubkey, privkey, spec.MAX_EFFECTIVE_BALANCE) - - item = deposit_data.hash_tree_root() - test_deposit_data_leaves.append(item) - tree = calc_merkle_tree_from_leaves(tuple(test_deposit_data_leaves)) - root = get_merkle_root((tuple(test_deposit_data_leaves))) - proof = list(get_merkle_proof(tree, item_index=index)) - assert verify_merkle_branch(item, proof, spec.DEPOSIT_CONTRACT_TREE_DEPTH, index, root) - - deposit = Deposit( - proof=list(proof), - index=index, - data=deposit_data, - ) - - pre_state.latest_eth1_data.deposit_root = root - pre_state.latest_eth1_data.deposit_count = len(test_deposit_data_leaves) - post_state = deepcopy(pre_state) - block = build_empty_block_for_next_slot(post_state) - block.body.deposits.append(deposit) - - state_transition(post_state, block) - assert len(post_state.validator_registry) == len(state.validator_registry) + 1 - assert len(post_state.balances) == len(state.balances) + 1 - assert get_balance(post_state, index) == spec.MAX_EFFECTIVE_BALANCE - assert post_state.validator_registry[index].pubkey == pubkeys[index] - - return pre_state, [block], post_state - - -def test_deposit_top_up(state): - pre_state = deepcopy(state) - test_deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry) - - validator_index = 0 - amount = spec.MAX_EFFECTIVE_BALANCE // 4 - pubkey = pubkeys[validator_index] - privkey = privkeys[validator_index] - deposit_data = build_deposit_data(pre_state, pubkey, privkey, amount) - - merkle_index = len(test_deposit_data_leaves) - item = deposit_data.hash_tree_root() - test_deposit_data_leaves.append(item) - tree = calc_merkle_tree_from_leaves(tuple(test_deposit_data_leaves)) - root = get_merkle_root((tuple(test_deposit_data_leaves))) - proof = list(get_merkle_proof(tree, item_index=merkle_index)) - assert verify_merkle_branch(item, proof, spec.DEPOSIT_CONTRACT_TREE_DEPTH, merkle_index, root) - - deposit = Deposit( - proof=list(proof), - index=merkle_index, - data=deposit_data, - ) - - pre_state.latest_eth1_data.deposit_root = root - pre_state.latest_eth1_data.deposit_count = len(test_deposit_data_leaves) - block = build_empty_block_for_next_slot(pre_state) - block.body.deposits.append(deposit) - - pre_balance = get_balance(pre_state, validator_index) - post_state = deepcopy(pre_state) - state_transition(post_state, block) - assert len(post_state.validator_registry) == len(pre_state.validator_registry) - assert len(post_state.balances) == len(pre_state.balances) - assert get_balance(post_state, validator_index) == pre_balance + amount - - return pre_state, [block], post_state - - -def test_attestation(state): - state.slot = spec.SLOTS_PER_EPOCH - test_state = deepcopy(state) - attestation = get_valid_attestation(state) - - # - # Add to state via block transition - # - attestation_block = build_empty_block_for_next_slot(test_state) - attestation_block.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY - attestation_block.body.attestations.append(attestation) - state_transition(test_state, attestation_block) - - assert len(test_state.current_epoch_attestations) == len(state.current_epoch_attestations) + 1 - - - # - # Epoch transition should move to previous_epoch_attestations - # - pre_current_epoch_attestations = deepcopy(test_state.current_epoch_attestations) - - epoch_block = build_empty_block_for_next_slot(test_state) - epoch_block.slot += spec.SLOTS_PER_EPOCH - state_transition(test_state, epoch_block) - - assert len(test_state.current_epoch_attestations) == 0 - assert test_state.previous_epoch_attestations == pre_current_epoch_attestations - - return state, [attestation_block, epoch_block], test_state - - -def test_voluntary_exit(state): - pre_state = deepcopy(state) - validator_index = get_active_validator_indices( - pre_state, - get_current_epoch(pre_state) - )[-1] - - # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit - pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH - - post_state = deepcopy(pre_state) - - voluntary_exit = VoluntaryExit( - epoch=get_current_epoch(pre_state), - validator_index=validator_index, - ) - voluntary_exit.signature = bls.sign( - message_hash=signing_root(voluntary_exit), - privkey=privkeys[validator_index], - domain=get_domain( - state=pre_state, - domain_type=spec.DOMAIN_VOLUNTARY_EXIT, - ) - ) - - # - # Add to state via block transition - # - initiate_exit_block = build_empty_block_for_next_slot(post_state) - initiate_exit_block.body.voluntary_exits.append(voluntary_exit) - state_transition(post_state, initiate_exit_block) - - assert post_state.validator_registry[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH - - # - # Process within epoch transition - # - exit_block = build_empty_block_for_next_slot(post_state) - exit_block.slot += spec.SLOTS_PER_EPOCH - state_transition(post_state, exit_block) - - assert post_state.validator_registry[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH - - return pre_state, [initiate_exit_block, exit_block], post_state - - -def test_transfer(state): - # overwrite default 0 to test - spec.MAX_TRANSFERS = 1 - - pre_state = deepcopy(state) - current_epoch = get_current_epoch(pre_state) - sender_index = get_active_validator_indices(pre_state, current_epoch)[-1] - recipient_index = get_active_validator_indices(pre_state, current_epoch)[0] - transfer_pubkey = pubkeys[-1] - transfer_privkey = privkeys[-1] - amount = get_balance(pre_state, sender_index) - pre_transfer_recipient_balance = get_balance(pre_state, recipient_index) - transfer = Transfer( - sender=sender_index, - recipient=recipient_index, - amount=amount, - fee=0, - slot=pre_state.slot + 1, - pubkey=transfer_pubkey, - ) - transfer.signature = bls.sign( - message_hash=signing_root(transfer), - privkey=transfer_privkey, - domain=get_domain( - state=pre_state, - domain_type=spec.DOMAIN_TRANSFER, - ) - ) - - # ensure withdrawal_credentials reproducable - pre_state.validator_registry[sender_index].withdrawal_credentials = ( - spec.BLS_WITHDRAWAL_PREFIX_BYTE + hash(transfer_pubkey)[1:] - ) - # un-activate so validator can transfer - pre_state.validator_registry[sender_index].activation_eligibility_epoch = spec.FAR_FUTURE_EPOCH - - post_state = deepcopy(pre_state) - # - # Add to state via block transition - # - block = build_empty_block_for_next_slot(post_state) - block.body.transfers.append(transfer) - state_transition(post_state, block) - - sender_balance = get_balance(post_state, sender_index) - recipient_balance = get_balance(post_state, recipient_index) - assert sender_balance == 0 - assert recipient_balance == pre_transfer_recipient_balance + amount - - return pre_state, [block], post_state - - -def test_balance_driven_status_transitions(state): - current_epoch = get_current_epoch(state) - validator_index = get_active_validator_indices(state, current_epoch)[-1] - - assert state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH - - # set validator balance to below ejection threshold - state.validator_registry[validator_index].effective_balance = spec.EJECTION_BALANCE - - post_state = deepcopy(state) - # - # trigger epoch transition - # - block = build_empty_block_for_next_slot(post_state) - block.slot += spec.SLOTS_PER_EPOCH - state_transition(post_state, block) - - assert post_state.validator_registry[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH - - return state, [block], post_state - - -def test_historical_batch(state): - state.slot += spec.SLOTS_PER_HISTORICAL_ROOT - (state.slot % spec.SLOTS_PER_HISTORICAL_ROOT) - 1 - - post_state = deepcopy(state) - - block = build_empty_block_for_next_slot(post_state) - - state_transition(post_state, block) - - assert post_state.slot == block.slot - assert get_current_epoch(post_state) % (spec.SLOTS_PER_HISTORICAL_ROOT // spec.SLOTS_PER_EPOCH) == 0 - assert len(post_state.historical_roots) == len(state.historical_roots) + 1 - - return state, [block], post_state - - -def test_eth1_data_votes(state): - post_state = deepcopy(state) - - expected_votes = 0 - assert len(state.eth1_data_votes) == expected_votes - - blocks = [] - for _ in range(spec.SLOTS_PER_ETH1_VOTING_PERIOD - 1): - block = build_empty_block_for_next_slot(post_state) - state_transition(post_state, block) - expected_votes += 1 - assert len(post_state.eth1_data_votes) == expected_votes - blocks.append(block) - - block = build_empty_block_for_next_slot(post_state) - state_transition(post_state, block) - blocks.append(block) - - assert post_state.slot % spec.SLOTS_PER_ETH1_VOTING_PERIOD == 0 - assert len(post_state.eth1_data_votes) == 1 - - return state, blocks, post_state