diff --git a/Makefile b/Makefile index a6b379b71..401e4bdc9 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/README.md b/README.md index 4c4808059..2f32df03f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Core specifications for Eth 2.0 client validation can be found in [specs/core](s * [General test format](specs/test_formats/README.md) * [Merkle proof formats](specs/light_client/merkle_proofs.md) * [Light client syncing protocol](specs/light_client/sync_protocol.md) +* [Beacon node API for validator](specs/validator/0_beacon-node-validator-api.md) ## Design goals diff --git a/scripts/phase0/build_spec.py b/scripts/phase0/build_spec.py index d739ada70..c7a377297 100644 --- a/scripts/phase0/build_spec.py +++ b/scripts/phase0/build_spec.py @@ -21,12 +21,12 @@ from eth2spec.utils.ssz.ssz_typing import ( uint8, uint16, uint32, uint64, uint128, uint256, Container, Vector, BytesN ) -from eth2spec.utils.bls_stub import ( +from eth2spec.utils.hash_function import hash +from eth2spec.utils.bls import ( bls_aggregate_pubkeys, bls_verify, bls_verify_multiple, ) -from eth2spec.utils.hash_function import hash # Note: 'int' type defaults to being interpreted as a uint64 by SSZ implementation. Slot = NewType('Slot', int) # uint64 diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 4e0408938..bc0b79f47 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -17,7 +17,7 @@ - [Initial values](#initial-values) - [Time parameters](#time-parameters) - [State list lengths](#state-list-lengths) - - [Reward and penalty quotients](#reward-and-penalty-quotients) + - [Rewards and penalties](#rewards-and-penalties) - [Max operations per block](#max-operations-per-block) - [Signature domains](#signature-domains) - [Data structures](#data-structures) @@ -103,7 +103,7 @@ - [Helper functions](#helper-functions-1) - [Justification and finalization](#justification-and-finalization) - [Crosslinks](#crosslinks) - - [Rewards and penalties](#rewards-and-penalties) + - [Rewards and penalties](#rewards-and-penalties-1) - [Registry updates](#registry-updates) - [Slashings](#slashings) - [Final updates](#final-updates) @@ -220,17 +220,17 @@ These configurations are updated for releases, but may be out of sync during `de | `LATEST_ACTIVE_INDEX_ROOTS_LENGTH` | `2**13` (= 8,192) | epochs | ~36 days | | `LATEST_SLASHED_EXIT_LENGTH` | `2**13` (= 8,192) | epochs | ~36 days | -### Reward and penalty quotients +### Rewards and penalties | Name | Value | | - | - | -| `BASE_REWARD_QUOTIENT` | `2**5` (= 32) | +| `BASE_REWARD_FACTOR` | `2**5` (= 32) | | `WHISTLEBLOWING_REWARD_QUOTIENT` | `2**9` (= 512) | | `PROPOSER_REWARD_QUOTIENT` | `2**3` (= 8) | | `INACTIVITY_PENALTY_QUOTIENT` | `2**25` (= 33,554,432) | | `MIN_SLASHING_PENALTY_QUOTIENT` | `2**5` (= 32) | -* **The `BASE_REWARD_QUOTIENT` is NOT final. Once all other protocol details are finalized, it will be adjusted to target a theoretical maximum total issuance of `2**21` ETH per year if `2**27` ETH is validating (and therefore `2**20` per year if `2**25` ETH is validating, etc.)** +* **The `BASE_REWARD_FACTOR` is NOT final. Once all other protocol details are finalized, it will be adjusted to target a theoretical maximum total issuance of `2**21` ETH per year if `2**27` ETH is validating (and therefore `2**20` per year if `2**25` ETH is validating, etc.)** * The `INACTIVITY_PENALTY_QUOTIENT` equals `INVERSE_SQRT_E_DROP_TIME**2` where `INVERSE_SQRT_E_DROP_TIME := 2**12 epochs` (~18 days) is the time it takes the inactivity penalty to reduce the balance of non-participating [validators](#dfn-validator) to about `1/sqrt(e) ~= 60.6%`. Indeed, the balance retained by offline [validators](#dfn-validator) after `n` epochs is about `(1 - 1/INACTIVITY_PENALTY_QUOTIENT)**(n**2/2)` so after `INVERSE_SQRT_E_DROP_TIME` epochs it is roughly `(1 - 1/INACTIVITY_PENALTY_QUOTIENT)**(INACTIVITY_PENALTY_QUOTIENT/2) ~= 1/sqrt(e)`. ### Max operations per block @@ -457,8 +457,6 @@ class Attestation(Container): class Deposit(Container): # Branch in the deposit tree proof: Vector[Bytes32, DEPOSIT_CONTRACT_TREE_DEPTH] - # Index in the deposit tree - index: uint64 # Data data: DepositData ``` @@ -943,9 +941,9 @@ def bytes_to_int(data: bytes) -> int: ```python def get_total_balance(state: BeaconState, indices: List[ValidatorIndex]) -> Gwei: """ - Return the combined effective balance of an array of ``validators``. + Return the combined effective balance of the ``indices``. (1 Gwei minimum to avoid divisions by zero.) """ - return sum([state.validator_registry[index].effective_balance for index in indices]) + return max(sum([state.validator_registry[index].effective_balance for index in indices]), 1) ``` ### `get_domain` @@ -1228,7 +1226,7 @@ def state_transition(state: BeaconState, block: BeaconBlock, validate_state_root ```python def process_slots(state: BeaconState, slot: Slot) -> None: - assert state.slot < slot + assert state.slot <= slot while state.slot < slot: process_slot(state) # Process epoch on the first slot of the next epoch @@ -1393,10 +1391,9 @@ def process_crosslinks(state: BeaconState) -> None: ```python def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: - adjusted_quotient = integer_squareroot(get_total_active_balance(state)) // BASE_REWARD_QUOTIENT - if adjusted_quotient == 0: - return 0 - return state.validator_registry[index].effective_balance // adjusted_quotient // BASE_REWARDS_PER_EPOCH + total_balance = get_total_active_balance(state) + effective_balance = state.validator_registry[index].effective_balance + return effective_balance * BASE_REWARD_FACTOR // integer_squareroot(total_balance) // BASE_REWARDS_PER_EPOCH ``` ```python @@ -1511,10 +1508,9 @@ def process_registry_updates(state: BeaconState) -> None: ```python def process_slashings(state: BeaconState) -> None: current_epoch = get_current_epoch(state) - active_validator_indices = get_active_validator_indices(state, current_epoch) - total_balance = get_total_balance(state, active_validator_indices) + total_balance = get_total_active_balance(state) - # Compute `total_penalties` + # Compute slashed balances in the current epoch total_at_start = state.latest_slashed_balances[(current_epoch + 1) % LATEST_SLASHED_EXIT_LENGTH] total_at_end = state.latest_slashed_balances[current_epoch % LATEST_SLASHED_EXIT_LENGTH] total_penalties = total_at_end - total_at_start @@ -1743,19 +1739,20 @@ def process_deposit(state: BeaconState, deposit: Deposit) -> None: leaf=hash_tree_root(deposit.data), proof=deposit.proof, depth=DEPOSIT_CONTRACT_TREE_DEPTH, - index=deposit.index, + index=state.deposit_index, root=state.latest_eth1_data.deposit_root, ) # Deposits must be processed in order - assert deposit.index == state.deposit_index state.deposit_index += 1 pubkey = deposit.data.pubkey amount = deposit.data.amount validator_pubkeys = [v.pubkey for v in state.validator_registry] if pubkey not in validator_pubkeys: - # Verify the deposit signature (proof of possession) + # Verify the deposit signature (proof of possession). + # Invalid signatures are allowed by the deposit contract, + # and hence included on-chain, but must not be processed. # Note: deposits are valid across forks, hence the deposit domain is retrieved directly from `bls_domain` if not bls_verify( pubkey, signing_root(deposit.data), deposit.data.signature, bls_domain(DOMAIN_DEPOSIT) diff --git a/specs/test_formats/README.md b/specs/test_formats/README.md index 4783dc475..277e986d5 100644 --- a/specs/test_formats/README.md +++ b/specs/test_formats/README.md @@ -35,7 +35,8 @@ Test formats: - [`bls`](./bls/README.md) - [`operations`](./operations/README.md) - [`shuffling`](./shuffling/README.md) -- [`ssz`](./ssz/README.md) +- [`ssz_generic`](./ssz_generic/README.md) +- [`ssz_static`](./ssz_static/README.md) - More formats are planned, see tracking issues for CI/testing ## Glossary @@ -186,6 +187,18 @@ To prevent parsing of hundreds of different YAML files to test a specific test t ... <--- more test types ``` +## Common test-case properties + +Some test-case formats share some common key-value pair patterns, and these are documented here: + +``` +bls_setting: int -- optional, can have 3 different values: + 0: (default, applies if key-value pair is absent). Free to choose either BLS ON or OFF. + Tests are generated with valid BLS data in this case, + but there is no change of outcome when running the test if BLS is ON or OFF. + 1: known as "BLS required" - if the test validity is strictly dependent on BLS being ON + 2: known as "BLS ignored" - if the test validity is strictly dependent on BLS being OFF +``` ## Note for implementers diff --git a/specs/test_formats/epoch_processing/README.md b/specs/test_formats/epoch_processing/README.md new file mode 100644 index 000000000..6384a0eda --- /dev/null +++ b/specs/test_formats/epoch_processing/README.md @@ -0,0 +1,29 @@ +# Epoch processing tests + +The different epoch sub-transitions are tested individually with test handlers. +The format is similar to block-processing state-transition tests. +There is no "change" factor however, the transitions are pure functions with just the pre-state as input. +Hence, the format is shared between each test-handler. (See test condition documentation on how to run the tests.) + +## Test case format + +```yaml +description: string -- description of test case, purely for debugging purposes +bls_setting: int -- see general test-format spec. +pre: BeaconState -- state before running the sub-transition +post: BeaconState -- state after applying the epoch sub-transition. +``` + +## Condition + +A handler of the `epoch_processing` test-runner should process these cases, + calling the corresponding processing implementation. + +Sub-transitions: + +| *`sub-transition-name`* | *`processing call`* | +|-------------------------|-----------------------------------| +| `crosslinks` | `process_crosslinks(state)` | +| `registry_updates` | `process_registry_updates(state)` | + +The resulting state should match the expected `post` state. diff --git a/specs/test_formats/operations/README.md b/specs/test_formats/operations/README.md index 842dc3615..32cf880b3 100644 --- a/specs/test_formats/operations/README.md +++ b/specs/test_formats/operations/README.md @@ -2,9 +2,34 @@ The different kinds of operations ("transactions") are tested individually with test handlers. -The tested operation kinds are: -- [`deposits`](./deposits.md) -- More tests are work-in-progress. +## Test case format +```yaml +description: string -- description of test case, purely for debugging purposes +bls_setting: int -- see general test-format spec. +pre: BeaconState -- state before applying the operation +: -- the YAML encoded operation, e.g. a "ProposerSlashing", or "Deposit". +post: BeaconState -- state after applying the operation. No value if operation processing is aborted. +``` +## Condition +A handler of the `operations` test-runner should process these cases, + calling the corresponding processing implementation. + +Operations: + +| *`operation-name`* | *`operation-object`* | *`input name`* | *`processing call`* | +|-------------------------|----------------------|----------------------|--------------------------------------------------------| +| `attestation` | `Attestation` | `attestation` | `process_attestation(state, attestation)` | +| `attester_slashing` | `AttesterSlashing` | `attester_slashing` | `process_attester_slashing(state, attester_slashing)` | +| `block_header` | `Block` | `block` | `process_block_header(state, block)` | +| `deposit` | `Deposit` | `deposit` | `process_deposit(state, deposit)` | +| `proposer_slashing` | `ProposerSlashing` | `proposer_slashing` | `process_proposer_slashing(state, proposer_slashing)` | +| `transfer` | `Transfer` | `transfer` | `process_transfer(state, transfer)` | +| `voluntary_exit` | `VoluntaryExit` | `voluntary_exit` | `process_voluntary_exit(state, voluntary_exit)` | + +Note that `block_header` is not strictly an operation (and is a full `Block`), but processed in the same manner, and hence included here. + +The resulting state should match the expected `post` state, or if the `post` state is left blank, + the handler should reject the input operation as invalid. diff --git a/specs/test_formats/operations/deposits.md b/specs/test_formats/operations/deposits.md deleted file mode 100644 index 8f44ebb22..000000000 --- a/specs/test_formats/operations/deposits.md +++ /dev/null @@ -1,18 +0,0 @@ -# Test format: Deposit operations - -A deposit is a form of an operation (or "transaction"), modifying the state. - -## Test case format - -```yaml -description: string -- description of test case, purely for debugging purposes -pre: BeaconState -- state before applying the deposit -deposit: Deposit -- the deposit -post: BeaconState -- state after applying the deposit. No value if deposit processing is aborted. -``` - -## Condition - -A `deposits` handler of the `operations` should process these cases, - calling the implementation of the `process_deposit(state, deposit)` functionality described in the spec. -The resulting state should match the expected `post` state, or if the `post` state is left blank, the handler should reject the inputs as invalid. diff --git a/specs/test_formats/sanity/README.md b/specs/test_formats/sanity/README.md new file mode 100644 index 000000000..20b36208a --- /dev/null +++ b/specs/test_formats/sanity/README.md @@ -0,0 +1,7 @@ +# Sanity tests + +The aim of the sanity tests is to set a base-line on what really needs to pass, i.e. the essentials. + +There are two handlers, documented individually: +- [`slots`](./slots.md): transitions of one or more slots (and epoch transitions within) +- [`blocks`](./blocks.md): transitions triggered by one or more blocks diff --git a/specs/test_formats/sanity/blocks.md b/specs/test_formats/sanity/blocks.md new file mode 100644 index 000000000..3004a6de7 --- /dev/null +++ b/specs/test_formats/sanity/blocks.md @@ -0,0 +1,18 @@ +# Sanity blocks testing + +Sanity tests to cover a series of one or more blocks being processed, aiming to cover common changes. + +## Test case format + +```yaml +description: string -- description of test case, purely for debugging purposes +bls_setting: int -- see general test-format spec. +pre: BeaconState -- state before running through the transitions triggered by the blocks. +blocks: [BeaconBlock] -- blocks to process, in given order, following the main transition function (i.e. process slot and epoch transitions in between blocks as normal) +post: BeaconState -- state after applying all the transitions triggered by the blocks. +``` + +## Condition + +The resulting state should match the expected `post` state, or if the `post` state is left blank, + the handler should reject the series of blocks as invalid. diff --git a/specs/test_formats/sanity/slots.md b/specs/test_formats/sanity/slots.md new file mode 100644 index 000000000..81866d47b --- /dev/null +++ b/specs/test_formats/sanity/slots.md @@ -0,0 +1,23 @@ +# Sanity slots testing + +Sanity tests to cover a series of one or more empty-slot transitions being processed, aiming to cover common changes. + +## Test case format + +```yaml +description: string -- description of test case, purely for debugging purposes +bls_setting: int -- see general test-format spec. +pre: BeaconState -- state before running through the transitions. +slots: N -- amount of slots to process, N being a positive numer. +post: BeaconState -- state after applying all the transitions. +``` + +The transition with pure time, no blocks, is known as `state_transition_to(state, slot)` in the spec. +This runs state-caching (pure slot transition) and epoch processing (every E slots). + +To process the data, call `state_transition_to(pre, pre.slot + N)`. And see if `pre` mutated into the equivalent of `post`. + + +## Condition + +The resulting state should match the expected `post` state. diff --git a/specs/test_formats/ssz_static/core.md b/specs/test_formats/ssz_static/core.md index 64b09a329..f24a225b0 100644 --- a/specs/test_formats/ssz_static/core.md +++ b/specs/test_formats/ssz_static/core.md @@ -9,11 +9,11 @@ This test-format ensures these direct serializations are covered. ## Test case format ```yaml -type_name: string -- string, object name, formatted as in spec. E.g. "BeaconBlock" -value: dynamic -- the YAML-encoded value, of the type specified by type_name. -serialized: bytes -- string, SSZ-serialized data, hex encoded, with prefix 0x -root: bytes32 -- string, hash-tree-root of the value, hex encoded, with prefix 0x -signing_root: bytes32 -- string, signing-root of the value, hex encoded, with prefix 0x. Optional, present if type contains ``signature`` field +SomeObjectName: -- key, object name, formatted as in spec. E.g. "BeaconBlock". + value: dynamic -- the YAML-encoded value, of the type specified by type_name. + serialized: bytes -- string, SSZ-serialized data, hex encoded, with prefix 0x + root: bytes32 -- string, hash-tree-root of the value, hex encoded, with prefix 0x + signing_root: bytes32 -- string, signing-root of the value, hex encoded, with prefix 0x. Optional, present if type contains ``signature`` field ``` ## Condition diff --git a/specs/validator/0_beacon-chain-validator.md b/specs/validator/0_beacon-chain-validator.md index f8272d446..1bb89f8e7 100644 --- a/specs/validator/0_beacon-chain-validator.md +++ b/specs/validator/0_beacon-chain-validator.md @@ -224,7 +224,7 @@ epoch_signature = bls_sign( `block.eth1_data` is a mechanism used by block proposers vote on a recent Ethereum 1.0 block hash and an associated deposit root found in the Ethereum 1.0 deposit contract. When consensus is formed, `state.latest_eth1_data` is updated, and validator deposits up to this root can be processed. The deposit root can be calculated by calling the `get_deposit_root()` function of the deposit contract using the post-state of the block hash. -* Let `D` be the set of `Eth1DataVote` objects `vote` in `state.eth1_data_votes` where: +* Let `D` be the list of `Eth1DataVote` objects `vote` in `state.eth1_data_votes` where: * `vote.eth1_data.block_hash` is the hash of an Eth 1.0 block that is (i) part of the canonical chain, (ii) >= `ETH1_FOLLOW_DISTANCE` blocks behind the head, and (iii) newer than `state.latest_eth1_data.block_hash`. * `vote.eth1_data.deposit_count` is the deposit count of the Eth 1.0 deposit contract at the block defined by `vote.eth1_data.block_hash`. * `vote.eth1_data.deposit_root` is the deposit root of the Eth 1.0 deposit contract at the block defined by `vote.eth1_data.block_hash`. @@ -233,7 +233,7 @@ epoch_signature = bls_sign( * Let `deposit_root` and `deposit_count` be the deposit root and deposit count of the Eth 1.0 deposit contract in the post-state of the block referenced by `block_hash` * Let `best_vote_data = Eth1Data(block_hash=block_hash, deposit_root=deposit_root, deposit_count=deposit_count)`. * If `D` is nonempty: - * Let `best_vote_data` be the `eth1_data` of the member of `D` that has the highest `vote.vote_count`, breaking ties by favoring block hashes with higher associated block height. + * Let `best_vote_data` be the `eth1_data` member of `D` that has the highest vote count (`D.count(eth1_data)`), breaking ties by favoring block hashes with higher associated block height. * Set `block.eth1_data = best_vote_data`. ##### Signature diff --git a/specs/validator/0_beacon-node-validator-api.md b/specs/validator/0_beacon-node-validator-api.md new file mode 100644 index 000000000..2a5fe7fcd --- /dev/null +++ b/specs/validator/0_beacon-node-validator-api.md @@ -0,0 +1,28 @@ +# Ethereum 2.0 Phase 0 -- Beacon Node API for Validator + +__NOTICE__: This document is a work-in-progress for researchers and implementers. This is an accompanying document to [Ethereum 2.0 Phase 0 -- Honest Validator](0_beacon-chain-validator.md) that describes an API exposed by the beacon node, which enables the validator client to participate in the Ethereum 2.0 protocol. + +## Outline + +This document outlines a minimal application programming interface (API) which is exposed by a beacon node for use by a validator client implementation which aims to facilitate [_phase 0_](../../README.md#phase-0) of Ethereum 2.0. + +The API is a REST interface, accessed via HTTP, designed for use as a local communications protocol between binaries. The only supported return data type is currently JSON. + +### Background +The beacon node maintains the state of the beacon chain by communicating with other beacon nodes in the Ethereum Serenity network. Conceptually, it does not maintain keypairs that participate with the beacon chain. + +The validator client is a conceptually separate entity which utilizes private keys to perform validator related tasks on the beacon chain, which we call validator "duties". These duties include the production of beacon blocks and signing of attestations. + +Since it is recommended to separate these concerns in the client implementations, we must clearly define the communication between them. + +The goal of this specification is to promote interoperability between beacon nodes and validator clients derived from different projects and to encourage innovation in validator client implementations, independently from beacon node development. For example, the validator client from Lighthouse could communicate with a running instance of the beacon node from Prysm, or a staking pool might create a decentrally managed validator client which utilises the same API. + +This specification is derived from a proposal and discussion on Issues [#1011](https://github.com/ethereum/eth2.0-specs/issues/1011) and [#1012](https://github.com/ethereum/eth2.0-specs/issues/1012) + + +## Specification + +The API specification has been written in [OpenAPI 3.0](https://swagger.io/docs/specification/about/) and is provided in the [beacon_node_oapi.yaml](beacon_node_oapi.yaml) file alongside this document. + +For convenience, this specification has been uploaded to [SwaggerHub](https://swagger.io/tools/swaggerhub/) at the following URL: +[https://app.swaggerhub.com/apis/spble/beacon_node_api_for_validator](https://app.swaggerhub.com/apis/spble/beacon_node_api_for_validator) diff --git a/specs/validator/beacon_node_oapi.yaml b/specs/validator/beacon_node_oapi.yaml new file mode 100644 index 000000000..74be21fac --- /dev/null +++ b/specs/validator/beacon_node_oapi.yaml @@ -0,0 +1,641 @@ +openapi: "3.0.2" +info: + title: "Minimal Beacon Node API for Validator" + description: "A minimal API specification for the beacon node, which enables a validator to connect and perform its obligations on the Ethereum 2.0 phase 0 beacon chain." + version: "0.2.0" + license: + name: "Apache 2.0" + url: "https://www.apache.org/licenses/LICENSE-2.0.html" +tags: + - name: MinimalSet + description: The minimal set of endpoints to enable a working validator implementation. + - name: OptionalSet + description: Extra endpoints which are nice-to-haves. +paths: + /node/version: + get: + tags: + - MinimalSet + summary: "Get version string of the running beacon node." + description: "Requests that the beacon node identify information about its implementation in a format similar to a [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3) field." + responses: + 200: + description: Request successful + content: + application/json: + schema: + $ref: '#/components/schemas/version' + 500: + $ref: '#/components/responses/InternalError' + /node/genesis_time: + get: + tags: + - MinimalSet + summary: "Get the genesis_time parameter from beacon node configuration." + description: "Requests the genesis_time parameter from the beacon node, which should be consistent across all beacon nodes that follow the same beacon chain." + responses: + 200: + description: Request successful + content: + application/json: + schema: + $ref: '#/components/schemas/genesis_time' + 500: + $ref: '#/components/responses/InternalError' + + /node/syncing: + get: + tags: + - MinimalSet + summary: "Poll to see if the the beacon node is syncing." + description: "Requests the beacon node to describe if it's currently syncing or not, and if it is, what block it is up to. This is modelled after the Eth1.0 JSON-RPC eth_syncing call.." + responses: + 200: + description: Request successful + content: + application/json: + schema: + type: object + properties: + is_syncing: + type: boolean + description: "A boolean of whether the node is currently syncing or not." + sync_status: + $ref: '#/components/schemas/SyncingStatus' + 500: + $ref: '#/components/responses/InternalError' + /node/fork: + get: + tags: + - OptionalSet + summary: "Get fork information from running beacon node." + description: "Requests the beacon node to provide which fork version it is currently on." + responses: + 200: + description: Request successful + content: + application/json: + schema: + type: object + properties: + fork: + $ref: '#/components/schemas/Fork' + chain_id: + type: integer + format: uint64 + description: "Sometimes called the network id, this number discerns the active chain for the beacon node. Analogous to Eth1.0 JSON-RPC net_version." + 500: + $ref: '#/components/responses/InternalError' + + /validator/duties: + get: + tags: + - MinimalSet + summary: "Get validator duties for the requested validators." + description: "Requests the beacon node to provide a set of _duties_, which are actions that should be performed by validators, for a particular epoch. Duties should only need to be checked once per epoch, however a chain reorganization (of > MIN_SEED_LOOKAHEAD epochs) could occur, resulting in a change of duties. For full safety, this API call should be polled at every slot to ensure that chain reorganizations are recognized, and to ensure that the beacon node is properly synchronized." + parameters: + - name: validator_pubkeys + in: query + required: true + description: "An array of hex-encoded BLS public keys" + schema: + type: array + items: + $ref: '#/components/schemas/pubkey' + minItems: 1 + - name: epoch + in: query + required: false + schema: + type: integer + responses: + 200: + description: Success response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ValidatorDuty' + 400: + $ref: '#/components/responses/InvalidRequest' + 406: + description: "Duties cannot be provided for the requested epoch." + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + + /validator/block: + get: + tags: + - MinimalSet + summary: "Produce a new block, without signature." + description: "Requests a beacon node to produce a valid block, which can then be signed by a validator." + parameters: + - name: slot + in: query + required: true + description: "The slot for which the block should be proposed." + schema: + type: integer + format: uint64 + - name: randao_reveal + in: query + required: true + description: "The validator's randao reveal value." + schema: + type: string + format: byte + responses: + 200: + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/BeaconBlock' + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + post: + tags: + - MinimalSet + summary: "Publish a signed block." + description: "Instructs the beacon node to broadcast a newly signed beacon block to the beacon network, to be included in the beacon chain. The beacon node is not required to validate the signed `BeaconBlock`, and a successful response (20X) only indicates that the broadcast has been successful. The beacon node is expected to integrate the new block into its state, and therefore validate the block internally, however blocks which fail the validation are still broadcast but a different status code is returned (202)" + parameters: + - name: beacon_block + in: query + required: true + description: "The `BeaconBlock` object, as sent from the beacon node originally, but now with the signature field completed." + schema: + $ref: '#/components/schemas/BeaconBlock' + responses: + 200: + description: "The block was validated successfully and has been broadcast. It has also been integrated into the beacon node's database." + 202: + description: "The block failed validation, but was successfully broadcast anyway. It was not integrated into the beacon node's database." + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + + /validator/attestation: + get: + tags: + - MinimalSet + summary: "Produce an attestation, without signature." + description: "Requests that the beacon node produce an IndexedAttestation, with a blank signature field, which the validator will then sign." + parameters: + - name: validator_pubkey + in: query + required: true + description: "Uniquely identifying which validator this attestation is to be produced for." + schema: + $ref: '#/components/schemas/pubkey' + - name: poc_bit + in: query + required: true + description: "The proof-of-custody bit that is to be reported by the requesting validator. This bit will be inserted into the appropriate location in the returned `IndexedAttestation`." + schema: + type: integer + format: uint32 + minimum: 0 + maximum: 1 + - name: slot + in: query + required: true + description: "The slot for which the attestation should be proposed." + schema: + type: integer + - name: shard + in: query + required: true + description: "The shard number for which the attestation is to be proposed." + schema: + type: integer + responses: + 200: + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/IndexedAttestation' + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + post: + tags: + - MinimalSet + summary: "Publish a signed attestation." + description: "Instructs the beacon node to broadcast a newly signed IndexedAttestation object to the intended shard subnet. The beacon node is not required to validate the signed IndexedAttestation, and a successful response (20X) only indicates that the broadcast has been successful. The beacon node is expected to integrate the new attestation into its state, and therefore validate the attestation internally, however attestations which fail the validation are still broadcast but a different status code is returned (202)" + parameters: + - name: attestation + in: query + required: true + description: "An `IndexedAttestation` structure, as originally provided by the beacon node, but now with the signature field completed." + schema: + $ref: '#/components/schemas/IndexedAttestation' + responses: + 200: + description: "The attestation was validated successfully and has been broadcast. It has also been integrated into the beacon node's database." + 202: + description: "The attestation failed validation, but was successfully broadcast anyway. It was not integrated into the beacon node's database." + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + +components: + schemas: + pubkey: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{96}$" + description: "The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._" + example: "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc" + version: + type: string + description: "A string which uniquely identifies the client implementation and its version; similar to [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3)." + example: "Lighthouse / v0.1.5 (Linux x86_64)" + genesis_time: + type: integer + format: uint64 + description: "The genesis_time configured for the beacon node, which is the unix time at which the Eth2.0 chain began." + example: 1557716289 + ValidatorDuty: + type: object + properties: + validator_pubkey: + $ref: '#/components/schemas/pubkey' + attestation_slot: + type: integer + format: uint64 + description: "The slot at which the validator must attest." + attestation_shard: + type: integer + format: uint64 + description: "The shard in which the validator must attest." + block_proposal_slot: + type: integer + format: uint64 + nullable: true + description: "The slot in which a validator must propose a block, or `null` if block production is not required." + SyncingStatus: + type: object + nullable: true + properties: + starting_slot: + type: integer + format: uint64 + description: "The slot at which syncing started (will only be reset after the sync reached its head)" + current_slot: + type: integer + format: uint64 + description: "The most recent slot sync'd by the beacon node." + highest_slot: + type: integer + format: uint64 + description: "Globally, the estimated most recent slot number, or current target slot number." + + BeaconBlock: + description: "The [`BeaconBlock`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#beaconblock) object from the Eth2.0 spec." + allOf: + - $ref: '#/components/schemas/BeaconBlockCommon' + - type: object + properties: + body: + $ref: '#/components/schemas/BeaconBlockBody' + BeaconBlockHeader: + description: "The [`BeaconBlockHeader`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#beaconblockheader) object from the Eth2.0 spec." + allOf: + - $ref: '#/components/schemas/BeaconBlockCommon' + - type: object + properties: + body_root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The tree hash merkle root of the `BeaconBlockBody` for the `BeaconBlock`" + BeaconBlockCommon: + # An abstract object to collect the common fields between the BeaconBlockHeader and the BeaconBlock objects + type: object + properties: + slot: + type: integer + format: uint64 + description: "The slot to which this block corresponds." + parent_root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The signing merkle root of the parent `BeaconBlock`." + state_root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The tree hash merkle root of the `BeaconState` for the `BeaconBlock`." + signature: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{192}$" + example: "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + description: "The BLS signature of the `BeaconBlock` made by the validator of the block." + BeaconBlockBody: + type: object + description: "The [`BeaconBlockBody`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#beaconblockbody) object from the Eth2.0 spec." + properties: + randao_reveal: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{192}$" + description: "The RanDAO reveal value provided by the validator." + eth1_data: + title: Eth1Data + type: object + description: "The [`Eth1Data`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#eth1data) object from the Eth2.0 spec." + properties: + deposit_root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Root of the deposit tree." + deposit_count: + type: integer + format: uint64 + description: "Total number of deposits." + block_hash: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Ethereum 1.x block hash." + graffiti: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + proposer_slashings: + type: array + items: + title: ProposerSlashings + type: object + description: "The [`ProposerSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposerslashing) object from the Eth2.0 spec." + properties: + proposer_index: + type: integer + format: uint64 + description: "The index of the proposer to be slashed." + header_1: + $ref: '#/components/schemas/BeaconBlockHeader' + header_2: + $ref: '#/components/schemas/BeaconBlockHeader' + attester_slashings: + type: array + items: + title: AttesterSlashings + type: object + description: "The [`AttesterSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attesterslashing) object from the Eth2.0 spec." + properties: + attestation_1: + $ref: '#/components/schemas/IndexedAttestation' + attestation_2: + $ref: '#/components/schemas/IndexedAttestation' + attestations: + type: array + items: + title: Attestation + type: object + description: "The [`Attestation`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestation) object from the Eth2.0 spec." + properties: + aggregation_bitfield: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]+$" + description: "Attester aggregation bitfield." + custody_bitfield: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]+$" + description: "Custody bitfield." + signature: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{192}$" + description: "BLS aggregate signature." + data: + $ref: '#/components/schemas/AttestationData' + deposits: + type: array + items: + title: Deposit + type: object + description: "The [`Deposit`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#deposit) object from the Eth2.0 spec." + properties: + proof: + type: array + description: "Branch in the deposit tree." + items: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + minItems: 32 + maxItems: 32 + index: + type: integer + format: uint64 + description: "Index in the deposit tree." + data: + title: DepositData + type: object + description: "The [`DepositData`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#depositdata) object from the Eth2.0 spec." + properties: + pubkey: + $ref: '#/components/schemas/pubkey' + withdrawal_credentials: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The withdrawal credentials." + amount: + type: integer + format: uint64 + description: "Amount in Gwei." + signature: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{192}$" + description: "Container self-signature." + voluntary_exits: + type: array + items: + title: VoluntaryExit + type: object + description: "The [`VoluntaryExit`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#voluntaryexit) object from the Eth2.0 spec." + properties: + epoch: + type: integer + format: uint64 + description: "Minimum epoch for processing exit." + validator_index: + type: integer + format: uint64 + description: "Index of the exiting validator." + signature: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{192}$" + description: "Validator signature." + transfers: + type: array + items: + title: Transfer + type: object + description: "The [`Transfer`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#transfer) object from the Eth2.0 spec." + properties: + sender: + type: integer + format: uint64 + description: "Sender index." + recipient: + type: integer + format: uint64 + description: "Recipient index." + amount: + type: integer + format: uint64 + description: "Amount in Gwei." + fee: + type: integer + format: uint64 + description: "Fee in Gwei for block producer." + slot: + type: integer + format: uint64 + description: "Inclusion slot." + pubkey: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{96}$" + description: "Sender withdrawal public key." + signature: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{192}$" + description: "Sender signature." + + Fork: + type: object + description: "The [`Fork`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#Fork) object from the Eth2.0 spec." + properties: + previous_version: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{8}$" + description: "Previous fork version." + current_version: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{8}$" + description: "Current fork version." + epoch: + type: integer + format: uint64 + description: "Fork epoch number." + IndexedAttestation: + type: object + description: "The [`IndexedAttestation`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#indexedattestation) object from the Eth2.0 spec." + properties: + custody_bit_0_indices: + type: array + description: "Validator indices for 0 bits." + items: + type: integer + format: uint64 + custody_bit_1_indices: + type: array + description: "Validator indices for 1 bits." + items: + type: integer + format: uint64 + signature: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{192}$" + example: "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + description: "The BLS signature of the `IndexedAttestation`, created by the validator of the attestation." + data: + $ref: '#/components/schemas/AttestationData' + AttestationData: + type: object + description: "The [`AttestationData`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestationdata) object from the Eth2.0 spec." + properties: + beacon_block_root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "LMD GHOST vote." + source_epoch: + type: integer + format: uint64 + description: "Source epoch from FFG vote." + source_root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Source root from FFG vote." + target_epoch: + type: integer + format: uint64 + description: "Target epoch from FFG vote." + target_root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Target root from FFG vote." + crosslink: + title: CrossLink + type: object + description: "The [`Crosslink`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#crosslink) object from the Eth2.0 spec, contains data from epochs [`start_epoch`, `end_epoch`)." + properties: + shard: + type: integer + format: uint64 + description: "The shard number." + start_epoch: + type: integer + format: uint64 + description: "The first epoch which the crosslinking data references." + end_epoch: + type: integer + format: uint64 + description: "The 'end' epoch referred to by the crosslinking data; no data in this Crosslink should refer to the `end_epoch` since it is not included in the crosslinking data interval." + parent_root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Root of the previous crosslink." + data_root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Root of the crosslinked shard data since the previous crosslink." + + responses: + Success: + description: "Request successful." + InvalidRequest: + description: "Invalid request syntax." + InternalError: + description: "Beacon node internal error." + CurrentlySyncing: + description: "Beacon node is currently syncing, try again later." + NotFound: + description: "The requested API endpoint does not exist." diff --git a/test_generators/README.md b/test_generators/README.md index f8124f9a7..95d7e70a8 100644 --- a/test_generators/README.md +++ b/test_generators/README.md @@ -58,7 +58,7 @@ It's recommended to extend the base-generator. Create a `requirements.txt` in the root of your generator directory: ``` -eth-utils==1.4.1 +eth-utils==1.6.0 ../../test_libs/gen_helpers ../../test_libs/config_helpers ../../test_libs/pyspec diff --git a/test_generators/bls/requirements.txt b/test_generators/bls/requirements.txt index 5eebde29f..6d83bdfb5 100644 --- a/test_generators/bls/requirements.txt +++ b/test_generators/bls/requirements.txt @@ -1,3 +1,3 @@ py-ecc==1.7.0 -eth-utils==1.4.1 +eth-utils==1.6.0 ../../test_libs/gen_helpers diff --git a/test_generators/epoch_processing/README.md b/test_generators/epoch_processing/README.md new file mode 100644 index 000000000..9b57875e2 --- /dev/null +++ b/test_generators/epoch_processing/README.md @@ -0,0 +1,11 @@ +# Epoch processing + +Epoch processing covers the sub-transitions during an epoch change. + +An epoch-processing test-runner can consume these sub-transition test-suites, + and handle different kinds of epoch sub-transitions by processing the cases using the specified test handler. + +Information on the format of the tests can be found in the [epoch-processing test formats documentation](../../specs/test_formats/epoch_processing/README.md). + + + diff --git a/test_generators/epoch_processing/main.py b/test_generators/epoch_processing/main.py new file mode 100644 index 000000000..8f067e4a3 --- /dev/null +++ b/test_generators/epoch_processing/main.py @@ -0,0 +1,38 @@ +from typing import Callable, Iterable + +from eth2spec.phase0 import spec +from eth2spec.test.epoch_processing import ( + test_process_crosslinks, + test_process_registry_updates +) +from gen_base import gen_runner, gen_suite, gen_typing +from gen_from_tests.gen import generate_from_tests +from preset_loader import loader + + +def create_suite(transition_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" % (transition_name, config_name), transition_name, gen_suite.render_suite( + title="%s epoch processing" % transition_name, + summary="Test suite for %s type epoch processing" % transition_name, + forks_timeline="testing", + forks=["phase0"], + config=config_name, + runner="epoch_processing", + handler=transition_name, + test_cases=get_cases())) + + return suite_definition + + +if __name__ == "__main__": + gen_runner.run_generator("epoch_processing", [ + create_suite('crosslinks', 'minimal', lambda: generate_from_tests(test_process_crosslinks)), + create_suite('crosslinks', 'mainnet', lambda: generate_from_tests(test_process_crosslinks)), + create_suite('registry_updates', 'minimal', lambda: generate_from_tests(test_process_registry_updates)), + create_suite('registry_updates', 'mainnet', lambda: generate_from_tests(test_process_registry_updates)), + ]) diff --git a/test_generators/epoch_processing/requirements.txt b/test_generators/epoch_processing/requirements.txt new file mode 100644 index 000000000..595cee69c --- /dev/null +++ b/test_generators/epoch_processing/requirements.txt @@ -0,0 +1,4 @@ +eth-utils==1.6.0 +../../test_libs/gen_helpers +../../test_libs/config_helpers +../../test_libs/pyspec \ No newline at end of file diff --git a/test_generators/operations/README.md b/test_generators/operations/README.md index e0b9d0e18..5cb3afc98 100644 --- a/test_generators/operations/README.md +++ b/test_generators/operations/README.md @@ -3,7 +3,6 @@ Operations (or "transactions" in previous spec iterations), are atomic changes to the state, introduced by embedding in blocks. -This generator provides a series of test suites, divided into handler, for each operation type. An operation test-runner can consume these operation test-suites, and handle different kinds of operations by processing the cases using the specified test handler. diff --git a/test_generators/operations/deposits.py b/test_generators/operations/deposits.py deleted file mode 100644 index 297a27859..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.ssz.ssz_impl 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..0a487ae74 100644 --- a/test_generators/operations/main.py +++ b/test_generators/operations/main.py @@ -1,9 +1,53 @@ -from gen_base import gen_runner +from typing import Callable, Iterable + +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, gen_suite, gen_typing +from gen_from_tests.gen import generate_from_tests +from preset_loader import loader +from eth2spec.phase0 import spec + + +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=operation_name, + test_cases=get_cases())) + return suite_definition -from deposits import mini_deposits_suite, full_deposits_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..595cee69c 100644 --- a/test_generators/operations/requirements.txt +++ b/test_generators/operations/requirements.txt @@ -1,5 +1,4 @@ -eth-utils==1.4.1 +eth-utils==1.6.0 ../../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/sanity/README.md b/test_generators/sanity/README.md new file mode 100644 index 000000000..6d2e2f30d --- /dev/null +++ b/test_generators/sanity/README.md @@ -0,0 +1,8 @@ +# Sanity tests + +Sanity tests cover regular state-transitions in a common block-list format, to ensure the basics work. + +Information on the format of the tests can be found in the [sanity test formats documentation](../../specs/test_formats/sanity/README.md). + + + diff --git a/test_generators/sanity/main.py b/test_generators/sanity/main.py new file mode 100644 index 000000000..bba6ed03d --- /dev/null +++ b/test_generators/sanity/main.py @@ -0,0 +1,35 @@ +from typing import Callable, Iterable + +from eth2spec.test.sanity import test_blocks, test_slots + +from gen_base import gen_runner, gen_suite, gen_typing +from gen_from_tests.gen import generate_from_tests +from preset_loader import loader +from eth2spec.phase0 import spec + + +def create_suite(handler_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 ("%sanity_s_%s" % (handler_name, config_name), handler_name, gen_suite.render_suite( + title="sanity testing", + summary="Sanity test suite, %s type, generated from pytests" % handler_name, + forks_timeline="testing", + forks=["phase0"], + config=config_name, + runner="sanity", + handler=handler_name, + test_cases=get_cases())) + return suite_definition + + +if __name__ == "__main__": + gen_runner.run_generator("sanity", [ + create_suite('blocks', 'minimal', lambda: generate_from_tests(test_blocks)), + create_suite('blocks', 'mainnet', lambda: generate_from_tests(test_blocks)), + create_suite('slots', 'minimal', lambda: generate_from_tests(test_slots)), + create_suite('slots', 'mainnet', lambda: generate_from_tests(test_slots)), + ]) diff --git a/test_generators/sanity/requirements.txt b/test_generators/sanity/requirements.txt new file mode 100644 index 000000000..595cee69c --- /dev/null +++ b/test_generators/sanity/requirements.txt @@ -0,0 +1,4 @@ +eth-utils==1.6.0 +../../test_libs/gen_helpers +../../test_libs/config_helpers +../../test_libs/pyspec \ No newline at end of file diff --git a/test_generators/shuffling/main.py b/test_generators/shuffling/main.py index 9ca3e2d36..711597189 100644 --- a/test_generators/shuffling/main.py +++ b/test_generators/shuffling/main.py @@ -10,7 +10,7 @@ from preset_loader import loader def shuffling_case(seed: spec.Bytes32, count: int): yield 'seed', '0x' + seed.hex() yield 'count', count - yield 'shuffled', [spec.get_permuted_index(i, count, seed) for i in range(count)] + yield 'shuffled', [spec.get_shuffled_index(i, count, seed) for i in range(count)] @to_tuple diff --git a/test_generators/shuffling/requirements.txt b/test_generators/shuffling/requirements.txt index 8f9bede8f..595cee69c 100644 --- a/test_generators/shuffling/requirements.txt +++ b/test_generators/shuffling/requirements.txt @@ -1,4 +1,4 @@ -eth-utils==1.4.1 +eth-utils==1.6.0 ../../test_libs/gen_helpers ../../test_libs/config_helpers ../../test_libs/pyspec \ No newline at end of file diff --git a/test_generators/ssz_generic/requirements.txt b/test_generators/ssz_generic/requirements.txt index 94afc9d91..dcdb0824f 100644 --- a/test_generators/ssz_generic/requirements.txt +++ b/test_generators/ssz_generic/requirements.txt @@ -1,4 +1,4 @@ -eth-utils==1.4.1 +eth-utils==1.6.0 ../../test_libs/gen_helpers ../../test_libs/config_helpers ssz==0.1.0a2 diff --git a/test_generators/ssz_static/main.py b/test_generators/ssz_static/main.py index abb167613..7de5237d1 100644 --- a/test_generators/ssz_static/main.py +++ b/test_generators/ssz_static/main.py @@ -18,10 +18,7 @@ MAX_LIST_LENGTH = 10 @to_dict -def create_test_case(rng: Random, name: str, mode: random_value.RandomizationMode, chaos: bool): - typ = spec.get_ssz_type_by_name(name) - value = random_value.get_random_ssz_object(rng, typ, MAX_BYTES_LENGTH, MAX_LIST_LENGTH, mode, chaos) - yield "type_name", name +def create_test_case_contents(value, typ): yield "value", encode.encode(value, typ) yield "serialized", '0x' + serialize(value).hex() yield "root", '0x' + hash_tree_root(value).hex() @@ -29,6 +26,13 @@ def create_test_case(rng: Random, name: str, mode: random_value.RandomizationMod yield "signing_root", '0x' + signing_root(value).hex() +@to_dict +def create_test_case(rng: Random, name: str, mode: random_value.RandomizationMode, chaos: bool): + typ = spec.get_ssz_type_by_name(name) + value = random_value.get_random_ssz_object(rng, typ, MAX_BYTES_LENGTH, MAX_LIST_LENGTH, mode, chaos) + yield name, create_test_case_contents(value, typ) + + @to_tuple def ssz_static_cases(rng: Random, mode: random_value.RandomizationMode, chaos: bool, count: int): for type_name in spec.ssz_types: diff --git a/test_generators/ssz_static/requirements.txt b/test_generators/ssz_static/requirements.txt index 8f9bede8f..595cee69c 100644 --- a/test_generators/ssz_static/requirements.txt +++ b/test_generators/ssz_static/requirements.txt @@ -1,4 +1,4 @@ -eth-utils==1.4.1 +eth-utils==1.6.0 ../../test_libs/gen_helpers ../../test_libs/config_helpers ../../test_libs/pyspec \ No newline at end of file diff --git a/test_libs/config_helpers/requirements.txt b/test_libs/config_helpers/requirements.txt index e441a474b..f2f208c3f 100644 --- a/test_libs/config_helpers/requirements.txt +++ b/test_libs/config_helpers/requirements.txt @@ -1 +1 @@ -ruamel.yaml==0.15.87 +ruamel.yaml==0.15.96 diff --git a/test_libs/config_helpers/setup.py b/test_libs/config_helpers/setup.py index 90ad94ee4..9f0ea0641 100644 --- a/test_libs/config_helpers/setup.py +++ b/test_libs/config_helpers/setup.py @@ -4,6 +4,6 @@ setup( name='config_helpers', packages=['preset_loader'], install_requires=[ - "ruamel.yaml==0.15.87" + "ruamel.yaml==0.15.96" ] ) diff --git a/test_libs/pyspec/tests/__init__.py b/test_libs/gen_helpers/gen_from_tests/__init__.py similarity index 100% rename from test_libs/pyspec/tests/__init__.py rename to test_libs/gen_helpers/gen_from_tests/__init__.py diff --git a/test_libs/gen_helpers/gen_from_tests/gen.py b/test_libs/gen_helpers/gen_from_tests/gen.py new file mode 100644 index 000000000..e7d801131 --- /dev/null +++ b/test_libs/gen_helpers/gen_from_tests/gen.py @@ -0,0 +1,25 @@ +from inspect import getmembers, isfunction + +def generate_from_tests(src, bls_active=True): + """ + Generate a list of test cases by running tests from the given src in generator-mode. + :param src: to retrieve tests from (discovered using inspect.getmembers) + :param bls_active: optional, to override BLS switch preference. Defaults to True. + :return: the list of test cases. + """ + fn_names = [ + name for (name, _) in getmembers(src, isfunction) + if name.startswith('test_') + ] + out = [] + print("generating test vectors from tests source: %s" % src.__name__) + for name in fn_names: + tfn = getattr(src, name) + try: + test_case = tfn(generator_mode=True, bls_active=bls_active) + # If no test case data is returned, the test is ignored. + if test_case is not None: + out.append(test_case) + except AssertionError: + print("ERROR: failed to generate vector from test: %s (src: %s)" % (name, src.__name__)) + return out diff --git a/test_libs/gen_helpers/requirements.txt b/test_libs/gen_helpers/requirements.txt index 3d6a39458..557cae631 100644 --- a/test_libs/gen_helpers/requirements.txt +++ b/test_libs/gen_helpers/requirements.txt @@ -1,2 +1,2 @@ -ruamel.yaml==0.15.87 -eth-utils==1.4.1 +ruamel.yaml==0.15.96 +eth-utils==1.6.0 diff --git a/test_libs/gen_helpers/setup.py b/test_libs/gen_helpers/setup.py index 5de27a6db..ee2c815c7 100644 --- a/test_libs/gen_helpers/setup.py +++ b/test_libs/gen_helpers/setup.py @@ -2,9 +2,9 @@ from distutils.core import setup setup( name='gen_helpers', - packages=['gen_base'], + packages=['gen_base', 'gen_from_tests'], install_requires=[ - "ruamel.yaml==0.15.87", - "eth-utils==1.4.1" + "ruamel.yaml==0.15.96", + "eth-utils==1.6.0" ] ) diff --git a/test_libs/pyspec/README.md b/test_libs/pyspec/README.md index bb6991a93..2c2226ee7 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/eth2spec/test/__init__.py b/test_libs/pyspec/eth2spec/test/__init__.py new file mode 100644 index 000000000..e69de29bb 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..700d68b53 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_attestation.py @@ -0,0 +1,301 @@ +from copy import deepcopy + +import eth2spec.phase0.spec as spec +from eth2spec.phase0.spec import ( + get_current_epoch, + process_attestation, + process_slots, +) +from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls +from eth2spec.test.helpers.attestations import ( + get_valid_attestation, + sign_attestation, +) +from eth2spec.test.helpers.state import ( + next_epoch, + next_slot, +) +from eth2spec.test.helpers.block import apply_empty_block + + +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, signed=True) + 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, signed=True) + next_epoch(state) + apply_empty_block(state) + + yield from run_attestation_processing(state, attestation) + + +@spec_state_test +def test_success_since_max_epochs_per_crosslink(state): + for _ in range(spec.MAX_EPOCHS_PER_CROSSLINK + 2): + next_epoch(state) + apply_empty_block(state) + + attestation = get_valid_attestation(state, signed=True) + data = attestation.data + # test logic sanity check: make sure the attestation only includes MAX_EPOCHS_PER_CROSSLINK epochs + assert data.crosslink.end_epoch - data.crosslink.start_epoch == spec.MAX_EPOCHS_PER_CROSSLINK + + for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): + next_slot(state) + apply_empty_block(state) + + yield from run_attestation_processing(state, attestation) + + +@always_bls +@spec_state_test +def test_invalid_attestation_signature(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_before_inclusion_delay(state): + attestation = get_valid_attestation(state, signed=True) + # 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, signed=True) + # increment past latest inclusion slot + process_slots(state, state.slot + spec.SLOTS_PER_EPOCH + 1) + apply_empty_block(state) + + 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 + + sign_attestation(state, attestation) + + 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.crosslink.shard += 1 + + sign_attestation(state, attestation) + + 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 + + sign_attestation(state, attestation) + + 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 + + sign_attestation(state, attestation) + + 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 + + sign_attestation(state, attestation) + + 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 + + sign_attestation(state, attestation) + + 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 + + sign_attestation(state, attestation) + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_bad_parent_crosslink(state): + next_epoch(state) + apply_empty_block(state) + + attestation = get_valid_attestation(state, signed=True) + for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): + next_slot(state) + apply_empty_block(state) + + attestation.data.crosslink.parent_root = b'\x27' * 32 + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_bad_crosslink_start_epoch(state): + next_epoch(state) + apply_empty_block(state) + + attestation = get_valid_attestation(state, signed=True) + for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): + next_slot(state) + apply_empty_block(state) + + attestation.data.crosslink.start_epoch += 1 + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_bad_crosslink_end_epoch(state): + next_epoch(state) + apply_empty_block(state) + + attestation = get_valid_attestation(state, signed=True) + for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): + next_slot(state) + apply_empty_block(state) + + attestation.data.crosslink.end_epoch += 1 + + yield from run_attestation_processing(state, attestation, False) + + +@spec_state_test +def test_inconsistent_bitfields(state): + attestation = get_valid_attestation(state) + state.slot += spec.MIN_ATTESTATION_INCLUSION_DELAY + + attestation.custody_bitfield = deepcopy(attestation.aggregation_bitfield) + b'\x00' + + sign_attestation(state, attestation) + + 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) + + sign_attestation(state, attestation) + + 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) + + sign_attestation(state, attestation) + + yield from run_attestation_processing(state, attestation) diff --git a/test_libs/pyspec/eth2spec/test/block_processing/test_process_attester_slashing.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_attester_slashing.py new file mode 100644 index 000000000..28e232277 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_attester_slashing.py @@ -0,0 +1,149 @@ +import eth2spec.phase0.spec as spec +from eth2spec.phase0.spec import ( + get_beacon_proposer_index, + process_attester_slashing, +) +from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls +from eth2spec.test.helpers.attestations import sign_indexed_attestation +from eth2spec.test.helpers.attester_slashings import get_valid_attester_slashing +from eth2spec.test.helpers.block import apply_empty_block +from eth2spec.test.helpers.state import ( + get_balance, + next_epoch, +) + + +def run_attester_slashing_processing(state, attester_slashing, valid=True): + """ + Run ``process_attester_slashing``, yielding: + - pre-state ('pre') + - attester_slashing ('attester_slashing') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + + yield 'pre', state + yield 'attester_slashing', attester_slashing + + if not valid: + 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] + 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 + + if slashed_index != proposer_index: + # lost whistleblower reward + assert get_balance(state, slashed_index) < pre_slashed_balance + # gained whistleblower reward + assert get_balance(state, proposer_index) > pre_proposer_balance + else: + # gained rewards for all slashings, which may include others. And only lost that of themselves. + # Netto at least 0, if more people where slashed, a balance increase. + assert get_balance(state, slashed_index) >= pre_slashed_balance + + yield 'post', state + + +@spec_state_test +def test_success_double(state): + attester_slashing = get_valid_attester_slashing(state, signed_1=True, signed_2=True) + + yield from run_attester_slashing_processing(state, attester_slashing) + + +@spec_state_test +def test_success_surround(state): + next_epoch(state) + apply_empty_block(state) + + state.current_justified_epoch += 1 + attester_slashing = get_valid_attester_slashing(state, signed_1=False, signed_2=True) + + # set attestion1 to surround attestation 2 + 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 + + sign_indexed_attestation(state, attester_slashing.attestation_1) + + yield from run_attester_slashing_processing(state, attester_slashing) + + +@always_bls +@spec_state_test +def test_invalid_sig_1(state): + attester_slashing = get_valid_attester_slashing(state, signed_1=False, signed_2=True) + yield from run_attester_slashing_processing(state, attester_slashing, False) + + +@always_bls +@spec_state_test +def test_invalid_sig_2(state): + attester_slashing = get_valid_attester_slashing(state, signed_1=True, signed_2=False) + yield from run_attester_slashing_processing(state, attester_slashing, False) + + +@always_bls +@spec_state_test +def test_invalid_sig_1_and_2(state): + attester_slashing = get_valid_attester_slashing(state, signed_1=False, signed_2=False) + yield from run_attester_slashing_processing(state, attester_slashing, False) + + +@spec_state_test +def test_same_data(state): + attester_slashing = get_valid_attester_slashing(state, signed_1=False, signed_2=True) + + attester_slashing.attestation_1.data = attester_slashing.attestation_2.data + sign_indexed_attestation(state, attester_slashing.attestation_1) + + 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, signed_1=False, signed_2=True) + + attester_slashing.attestation_1.data.target_epoch += 1 + sign_indexed_attestation(state, attester_slashing.attestation_1) + + 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, signed_1=True, signed_2=True) + + # set all indices to slashed + attestation_1 = attester_slashing.attestation_1 + validator_indices = attestation_1.custody_bit_0_indices + attestation_1.custody_bit_1_indices + for index in validator_indices: + state.validator_registry[index].slashed = True + + 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, signed_1=False, signed_2=True) + + attester_slashing.attestation_1.custody_bit_1_indices = ( + attester_slashing.attestation_1.custody_bit_0_indices + ) + sign_indexed_attestation(state, attester_slashing.attestation_1) + + yield from run_attester_slashing_processing(state, attester_slashing, False) diff --git a/test_libs/pyspec/eth2spec/test/block_processing/test_process_block_header.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_block_header.py new file mode 100644 index 000000000..8a67be741 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_block_header.py @@ -0,0 +1,85 @@ +from copy import deepcopy + +from eth2spec.phase0.spec import ( + get_beacon_proposer_index, + process_slots, + process_block_header, +) +from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, + sign_block +) +from eth2spec.test.helpers.state import next_slot + + +def prepare_state_for_header_processing(state): + process_slots(state, state.slot + 1) + + +def run_block_header_processing(state, block, valid=True): + """ + 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) + + yield 'pre', state + yield 'block', block + + if not valid: + expect_assertion_error(lambda: process_block_header(state, block)) + yield 'post', None + return + + process_block_header(state, block) + yield 'post', state + + +@spec_state_test +def test_success_block_header(state): + block = build_empty_block_for_next_slot(state, signed=True) + yield from run_block_header_processing(state, block) + + +@always_bls +@spec_state_test +def test_invalid_sig_block_header(state): + block = build_empty_block_for_next_slot(state) + yield from run_block_header_processing(state, block, valid=False) + + +@spec_state_test +def test_invalid_slot_block_header(state): + block = build_empty_block_for_next_slot(state) + block.slot = state.slot + 2 # invalid slot + sign_block(state, block) + + yield from run_block_header_processing(state, block, valid=False) + + +@spec_state_test +def test_invalid_parent_root(state): + block = build_empty_block_for_next_slot(state) + block.parent_root = b'\12' * 32 # invalid prev root + sign_block(state, block) + + 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) + next_slot(stub_state) + proposer_index = get_beacon_proposer_index(stub_state) + + # set proposer to slashed + state.validator_registry[proposer_index].slashed = True + + block = build_empty_block_for_next_slot(state, signed=True) + + 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..0430dd12f --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_deposit.py @@ -0,0 +1,181 @@ +import eth2spec.phase0.spec as spec +from eth2spec.phase0.spec import process_deposit +from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls +from eth2spec.test.helpers.deposits import ( + build_deposit, + prepare_state_and_deposit, + sign_deposit_data, +) +from eth2spec.test.helpers.state import get_balance +from eth2spec.test.helpers.keys import privkeys, pubkeys + + +def run_deposit_processing(state, deposit, validator_index, valid=True, effective=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) + + 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 not effective: + assert len(state.validator_registry) == pre_validator_count + assert len(state.balances) == pre_validator_count + if validator_index < pre_validator_count: + assert get_balance(state, validator_index) == pre_balance + else: + 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 get_balance(state, validator_index) == pre_balance + deposit.data.amount + + assert state.deposit_index == state.latest_eth1_data.deposit_count + + +@spec_state_test +def test_new_deposit(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, signed=True) + + yield from run_deposit_processing(state, deposit, validator_index) + + +@always_bls +@spec_state_test +def test_invalid_sig_new_deposit(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, valid=True, effective=False) + + +@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, signed=True) + + yield from run_deposit_processing(state, deposit, validator_index) + + +@always_bls +@spec_state_test +def test_invalid_sig_top_up(state): + validator_index = 0 + amount = spec.MAX_EFFECTIVE_BALANCE // 4 + deposit = prepare_state_and_deposit(state, validator_index, amount) + + # invalid signatures, in top-ups, are allowed! + yield from run_deposit_processing(state, deposit, validator_index, valid=True, effective=True) + + +@spec_state_test +def test_invalid_withdrawal_credentials_top_up(state): + validator_index = 0 + amount = spec.MAX_EFFECTIVE_BALANCE // 4 + withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX_BYTE + spec.hash(b"junk")[1:] + deposit = prepare_state_and_deposit( + state, + validator_index, + amount, + withdrawal_credentials=withdrawal_credentials + ) + + # inconsistent withdrawal credentials, in top-ups, are allowed! + yield from run_deposit_processing(state, deposit, validator_index, valid=True, effective=True) + + +@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 + + sign_deposit_data(state, deposit.data, privkeys[validator_index]) + + yield from run_deposit_processing(state, deposit, validator_index, valid=False) + + +@spec_state_test +def test_wrong_deposit_for_deposit_count(state): + deposit_data_leaves = [spec.ZERO_HASH] * len(state.validator_registry) + + # build root for deposit_1 + index_1 = len(deposit_data_leaves) + pubkey_1 = pubkeys[index_1] + privkey_1 = privkeys[index_1] + deposit_1, root_1, deposit_data_leaves = build_deposit( + state, + deposit_data_leaves, + pubkey_1, + privkey_1, + spec.MAX_EFFECTIVE_BALANCE, + withdrawal_credentials=b'\x00'*32, + signed=True, + ) + deposit_count_1 = len(deposit_data_leaves) + + # build root for deposit_2 + index_2 = len(deposit_data_leaves) + pubkey_2 = pubkeys[index_2] + privkey_2 = privkeys[index_2] + deposit_2, root_2, deposit_data_leaves = build_deposit( + state, + deposit_data_leaves, + pubkey_2, + privkey_2, + spec.MAX_EFFECTIVE_BALANCE, + withdrawal_credentials=b'\x00'*32, + signed=True, + ) + + # state has root for deposit_2 but is at deposit_count for deposit_1 + state.latest_eth1_data.deposit_root = root_2 + state.latest_eth1_data.deposit_count = deposit_count_1 + + yield from run_deposit_processing(state, deposit_2, index_2, 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 + + sign_deposit_data(state, deposit.data, privkeys[validator_index]) + + 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..07ccc25f1 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_proposer_slashing.py @@ -0,0 +1,137 @@ +import eth2spec.phase0.spec as spec +from eth2spec.phase0.spec import ( + get_current_epoch, + process_proposer_slashing, +) +from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls +from eth2spec.test.helpers.block_header import sign_block_header +from eth2spec.test.helpers.keys import privkeys +from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing +from eth2spec.test.helpers.state import get_balance + + +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, signed_1=True, signed_2=True) + + yield from run_proposer_slashing_processing(state, proposer_slashing) + + +@always_bls +@spec_state_test +def test_invalid_sig_1(state): + proposer_slashing = get_valid_proposer_slashing(state, signed_1=False, signed_2=True) + yield from run_proposer_slashing_processing(state, proposer_slashing, False) + + +@always_bls +@spec_state_test +def test_invalid_sig_2(state): + proposer_slashing = get_valid_proposer_slashing(state, signed_1=True, signed_2=False) + yield from run_proposer_slashing_processing(state, proposer_slashing, False) + + +@always_bls +@spec_state_test +def test_invalid_sig_1_and_2(state): + proposer_slashing = get_valid_proposer_slashing(state, signed_1=False, signed_2=False) + yield from run_proposer_slashing_processing(state, proposer_slashing, False) + + +@spec_state_test +def test_invalid_proposer_index(state): + proposer_slashing = get_valid_proposer_slashing(state, signed_1=True, signed_2=True) + # 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, signed_1=True, signed_2=False) + + # set slots to be in different epochs + proposer_slashing.header_2.slot += spec.SLOTS_PER_EPOCH + sign_block_header(state, proposer_slashing.header_2, privkeys[proposer_slashing.proposer_index]) + + 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, signed_1=True, signed_2=False) + + # 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, signed_1=True, signed_2=True) + + # 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, signed_1=True, signed_2=True) + + # 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, signed_1=True, signed_2=True) + + # move 1 epoch into future, to allow for past withdrawable epoch + state.slot += spec.SLOTS_PER_EPOCH + # 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/eth2spec/test/block_processing/test_process_transfer.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_transfer.py new file mode 100644 index 000000000..bd435d67a --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_transfer.py @@ -0,0 +1,178 @@ +import eth2spec.phase0.spec as spec +from eth2spec.phase0.spec import ( + get_active_validator_indices, + get_beacon_proposer_index, + get_current_epoch, + process_transfer, +) +from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls +from eth2spec.test.helpers.state import next_epoch +from eth2spec.test.helpers.block import apply_empty_block +from eth2spec.test.helpers.transfers import get_valid_transfer + + +def run_transfer_processing(state, transfer, valid=True): + """ + Run ``process_transfer``, yielding: + - pre-state ('pre') + - transfer ('transfer') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + + 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] + + 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 state.balances[proposer_index] == pre_transfer_proposer_balance + transfer.fee + + +@spec_state_test +def test_success_non_activated(state): + transfer = get_valid_transfer(state, signed=True) + # un-activate so validator can transfer + state.validator_registry[transfer.sender].activation_eligibility_epoch = spec.FAR_FUTURE_EPOCH + + yield from run_transfer_processing(state, transfer) + + +@spec_state_test +def test_success_withdrawable(state): + next_epoch(state) + apply_empty_block(state) + + transfer = get_valid_transfer(state, signed=True) + + # withdrawable_epoch in past so can transfer + state.validator_registry[transfer.sender].withdrawable_epoch = get_current_epoch(state) - 1 + + 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] + state.balances[sender_index] = spec.MAX_EFFECTIVE_BALANCE + 1 + transfer = get_valid_transfer(state, sender_index=sender_index, amount=1, fee=0, signed=True) + + 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, signed=True) + + yield from run_transfer_processing(state, transfer) + + +@always_bls +@spec_state_test +def test_invalid_signature(state): + transfer = get_valid_transfer(state) + # un-activate so validator can transfer + state.validator_registry[transfer.sender].activation_eligibility_epoch = spec.FAR_FUTURE_EPOCH + + yield from run_transfer_processing(state, transfer, False) + + +@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, signed=True) + + 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, signed=True) + # 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_insufficient_balance_for_fee(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=0, fee=1, signed=True) + + # 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_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, signed=True) + + # 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, + signed=True, + ) + + # 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_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, signed=True) + 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, signed=True) + state.validator_registry[transfer.sender].withdrawal_credentials = spec.ZERO_HASH + + # 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) diff --git a/test_libs/pyspec/eth2spec/test/block_processing/test_process_voluntary_exit.py b/test_libs/pyspec/eth2spec/test/block_processing/test_process_voluntary_exit.py new file mode 100644 index 000000000..53fb4e3f7 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/block_processing/test_process_voluntary_exit.py @@ -0,0 +1,225 @@ +import eth2spec.phase0.spec as spec +from eth2spec.phase0.spec import ( + get_active_validator_indices, + get_churn_limit, + get_current_epoch, + process_voluntary_exit, +) +from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls +from eth2spec.test.helpers.keys import pubkey_to_privkey +from eth2spec.test.helpers.voluntary_exits import build_voluntary_exit, sign_voluntary_exit + + +def run_voluntary_exit_processing(state, voluntary_exit, valid=True): + """ + Run ``process_voluntary_exit``, yielding: + - pre-state ('pre') + - voluntary_exit ('voluntary_exit') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + validator_index = voluntary_exit.validator_index + + yield 'pre', state + yield 'voluntary_exit', voluntary_exit + + if not valid: + expect_assertion_error(lambda: process_voluntary_exit(state, voluntary_exit)) + yield 'post', None + return + + pre_exit_epoch = state.validator_registry[validator_index].exit_epoch + + process_voluntary_exit(state, voluntary_exit) + + 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 + + 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, signed=True) + + yield from run_voluntary_exit_processing(state, voluntary_exit) + + +@always_bls +@spec_state_test +def test_invalid_signature(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) + + yield from run_voluntary_exit_processing(state, voluntary_exit, False) + + +@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 + + current_epoch = get_current_epoch(state) + + # exit `MAX_EXITS_PER_EPOCH` + initial_indices = get_active_validator_indices(state, current_epoch)[:get_churn_limit(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] + exit_queue.append(build_voluntary_exit( + state, + current_epoch, + index, + privkey, + signed=True, + )) + + # 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] + privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] + voluntary_exit = build_voluntary_exit( + state, + current_epoch, + validator_index, + privkey, + signed=True, + ) + + # 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 ( + state.validator_registry[validator_index].exit_epoch == + state.validator_registry[initial_indices[0]].exit_epoch + 1 + ) + + +@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, + signed=False, + ) + voluntary_exit.epoch += 1 + sign_voluntary_exit(state, voluntary_exit, privkey) + + 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, + signed=False, + ) + voluntary_exit.validator_index = len(state.validator_registry) + sign_voluntary_exit(state, voluntary_exit, privkey) + + 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] + privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] + + state.validator_registry[validator_index].activation_epoch = spec.FAR_FUTURE_EPOCH + + # build and test voluntary exit + voluntary_exit = build_voluntary_exit( + state, + current_epoch, + validator_index, + privkey, + signed=True, + ) + + 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 + + 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] + + # but validator already has exited + state.validator_registry[validator_index].exit_epoch = current_epoch + 2 + + voluntary_exit = build_voluntary_exit( + state, + current_epoch, + validator_index, + privkey, + signed=True, + ) + + 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] + privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] + + voluntary_exit = build_voluntary_exit( + state, + current_epoch, + validator_index, + privkey, + signed=True, + ) + + assert ( + current_epoch - state.validator_registry[validator_index].activation_epoch < + spec.PERSISTENT_COMMITTEE_PERIOD + ) + + 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..5e8ec708a --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/conftest.py @@ -0,0 +1,37 @@ +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..2be9322de --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/context.py @@ -0,0 +1,82 @@ +from eth2spec.phase0 import spec +from eth2spec.utils import bls + +from .helpers.genesis import create_genesis_state + +from .utils import spectest, with_args, with_tags + +# 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)]) + + +# BLS is turned off by default *for performance purposes during TESTING*. +# The runner of the test can indicate the preferred setting (test generators prefer BLS to be ON). +# - Some tests are marked as BLS-requiring, and ignore this setting. +# (tests that express differences caused by BLS, e.g. invalid signatures being rejected) +# - Some other tests are marked as BLS-ignoring, and ignore this setting. +# (tests that are heavily performance impacted / require unsigned state transitions) +# - Most tests respect the BLS setting. +DEFAULT_BLS_ACTIVE = False + + +# shorthand for decorating @with_state @spectest() +def spec_state_test(fn): + return with_state(bls_switch(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.') + + +# Tags a test to be ignoring BLS for it to pass. +bls_ignored = with_tags({'bls_setting': 2}) + + +def never_bls(fn): + """ + Decorator to apply on ``bls_switch`` decorator to force BLS de-activation. Useful to mark tests as BLS-ignorant. + """ + def entry(*args, **kw): + # override bls setting + kw['bls_active'] = False + return fn(*args, **kw) + return bls_ignored(entry) + + +# Tags a test to be requiring BLS for it to pass. +bls_required = with_tags({'bls_setting': 1}) + + +def always_bls(fn): + """ + Decorator to apply on ``bls_switch`` decorator to force BLS activation. Useful to mark tests as BLS-dependent. + """ + def entry(*args, **kw): + # override bls setting + kw['bls_active'] = True + return fn(*args, **kw) + return bls_required(entry) + + +def bls_switch(fn): + """ + Decorator to make a function execute with BLS ON, or BLS off. + Based on an optional bool argument ``bls_active``, passed to the function at runtime. + """ + def entry(*args, **kw): + old_state = bls.bls_active + bls.bls_active = kw.pop('bls_active', DEFAULT_BLS_ACTIVE) + out = fn(*args, **kw) + bls.bls_active = old_state + return out + return entry 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 62% 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 347a507ef..bb308a9f0 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 @@ -9,106 +8,123 @@ from eth2spec.phase0.spec import ( process_crosslinks, state_transition, ) -from tests.helpers import ( +from eth2spec.test.context import spec_state_test +from eth2spec.test.helpers.state import ( + next_epoch, + next_slot +) +from eth2spec.test.helpers.block import apply_empty_block, sign_block +from eth2spec.test.helpers.attestations import ( add_attestation_to_state, build_empty_block_for_next_slot, fill_aggregate_attestation, get_crosslink_committee, get_valid_attestation, - next_epoch, - next_slot, - set_bitfield_bit, + sign_attestation, ) -# mark entire file as 'crosslinks' -pytestmark = pytest.mark.crosslinks - - 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) block.slot = slot + sign_block(state, block) state_transition(state, block) # cache state before epoch transition process_slot(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) - attestation = get_valid_attestation(state) + attestation = get_valid_attestation(state, signed=True) fill_aggregate_attestation(state, attestation) add_attestation_to_state(state, attestation, state.slot + spec.MIN_ATTESTATION_INCLUSION_DELAY) assert len(state.current_epoch_attestations) == 1 - pre_state, post_state = run_process_crosslinks(state) - shard = attestation.data.crosslink.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) - attestation = get_valid_attestation(state) + attestation = get_valid_attestation(state, signed=True) fill_aggregate_attestation(state, attestation) add_attestation_to_state(state, attestation, state.slot + spec.SLOTS_PER_EPOCH) assert len(state.previous_epoch_attestations) == 1 - pre_state, post_state = run_process_crosslinks(state) + shard = attestation.data.crosslink.shard + pre_crosslink = deepcopy(state.current_crosslinks[shard]) + crosslink_deltas = get_crosslink_deltas(state) - shard = attestation.data.crosslink.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.crosslink.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): + if spec.get_epoch_committee_count(state, spec.get_current_epoch(state)) < spec.SHARD_COUNT: + print("warning: ignoring test, test-assumptions are incompatible with configuration") + return + next_epoch(state) state.slot += 4 - attestation_1 = get_valid_attestation(state) + attestation_1 = get_valid_attestation(state, signed=True) fill_aggregate_attestation(state, attestation_1) - # add attestation_1 in the next epoch + # add attestation_1 to next epoch next_epoch(state) add_attestation_to_state(state, attestation_1, state.slot + 1) for slot in range(spec.SLOTS_PER_EPOCH): attestation_2 = get_valid_attestation(state) if attestation_2.data.crosslink.shard == attestation_1.data.crosslink.shard: + sign_attestation(state, attestation_2) break next_slot(state) + apply_empty_block(state) + fill_aggregate_attestation(state, attestation_2) # add attestation_2 in the next epoch after attestation_1 has @@ -119,16 +135,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.crosslink.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.crosslink.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 54% 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..8f6e35088 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,44 @@ -from copy import deepcopy - -import pytest - import eth2spec.phase0.spec as spec from eth2spec.phase0.spec import ( get_current_epoch, is_active_validator, + process_registry_updates ) -from tests.helpers import ( - next_epoch, -) - -# mark entire file as 'state' -pytestmark = pytest.mark.state +from eth2spec.phase0.spec import state_transition +from eth2spec.test.helpers.block import build_empty_block_for_next_slot, sign_block +from eth2spec.test.helpers.state import next_epoch +from eth2spec.test.context import spec_state_test +def run_process_registry_updates(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) + block.slot = slot + sign_block(state, block) + state_transition(state, block) + + # cache state before epoch transition + spec.process_slot(state) + + # process components of epoch transition before registry update + spec.process_justification_and_finalization(state) + spec.process_crosslinks(state) + spec.process_rewards_and_penalties(state) + + yield 'pre', state + process_registry_updates(state) + yield 'post', state + + +@spec_state_test def test_activation(state): index = 0 assert is_active_validator(state.validator_registry[index], get_current_epoch(state)) @@ -26,12 +49,10 @@ 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) - - blocks = [] for _ in range(spec.ACTIVATION_EXIT_DELAY + 1): - block = next_epoch(state) - blocks.append(block) + next_epoch(state) + + yield from run_process_registry_updates(state) assert state.validator_registry[index].activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH assert state.validator_registry[index].activation_epoch != spec.FAR_FUTURE_EPOCH @@ -40,9 +61,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 +71,13 @@ def test_ejection(state): # Mock an ejection state.validator_registry[index].effective_balance = spec.EJECTION_BALANCE - pre_state = deepcopy(state) - - blocks = [] for _ in range(spec.ACTIVATION_EXIT_DELAY + 1): - block = next_epoch(state) - blocks.append(block) + next_epoch(state) + + yield from run_process_registry_updates(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/eth2spec/test/helpers/__init__.py b/test_libs/pyspec/eth2spec/test/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_libs/pyspec/eth2spec/test/helpers/attestations.py b/test_libs/pyspec/eth2spec/test/helpers/attestations.py new file mode 100644 index 000000000..6ac0b994e --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/attestations.py @@ -0,0 +1,161 @@ +from typing import List + +# Access constants from spec pkg reference. +import eth2spec.phase0.spec as spec +from eth2spec.phase0.spec import ( + Attestation, + AttestationData, + AttestationDataAndCustodyBit, + Crosslink, + get_epoch_start_slot, get_block_root, get_current_epoch, get_previous_epoch, slot_to_epoch, + get_crosslink_committee, get_domain, IndexedAttestation, get_attesting_indices, BeaconState, get_block_root_at_slot, + get_epoch_start_shard, get_epoch_committee_count, + state_transition, process_slots, +) +from eth2spec.test.helpers.bitfields import set_bitfield_bit +from eth2spec.test.helpers.block import build_empty_block_for_next_slot, sign_block +from eth2spec.test.helpers.keys import privkeys +from eth2spec.utils.bls import bls_sign, bls_aggregate_signatures +from eth2spec.utils.minimal_ssz import hash_tree_root + + +def build_attestation_data(state, slot, shard): + assert state.slot >= slot + + if slot == state.slot: + block_root = build_empty_block_for_next_slot(state).parent_root + else: + block_root = get_block_root_at_slot(state, slot) + + current_epoch_start_slot = get_epoch_start_slot(get_current_epoch(state)) + if slot < current_epoch_start_slot: + epoch_boundary_root = get_block_root(state, get_previous_epoch(state)) + elif slot == current_epoch_start_slot: + epoch_boundary_root = block_root + else: + epoch_boundary_root = get_block_root(state, get_current_epoch(state)) + + if slot < current_epoch_start_slot: + justified_epoch = state.previous_justified_epoch + justified_block_root = state.previous_justified_root + else: + justified_epoch = state.current_justified_epoch + justified_block_root = state.current_justified_root + + if slot_to_epoch(slot) == get_current_epoch(state): + parent_crosslink = state.current_crosslinks[shard] + else: + parent_crosslink = state.previous_crosslinks[shard] + + return AttestationData( + beacon_block_root=block_root, + source_epoch=justified_epoch, + source_root=justified_block_root, + target_epoch=slot_to_epoch(slot), + target_root=epoch_boundary_root, + crosslink=Crosslink( + shard=shard, + start_epoch=parent_crosslink.end_epoch, + end_epoch=min(slot_to_epoch(slot), parent_crosslink.end_epoch + spec.MAX_EPOCHS_PER_CROSSLINK), + data_root=spec.ZERO_HASH, + parent_root=hash_tree_root(parent_crosslink), + ), + ) + + +def get_valid_attestation(state, slot=None, signed=False): + if slot is None: + slot = state.slot + + epoch = slot_to_epoch(slot) + epoch_start_shard = get_epoch_start_shard(state, epoch) + committees_per_slot = get_epoch_committee_count(state, epoch) // spec.SLOTS_PER_EPOCH + shard = (epoch_start_shard + committees_per_slot * (slot % spec.SLOTS_PER_EPOCH)) % spec.SHARD_COUNT + + attestation_data = build_attestation_data(state, slot, shard) + + crosslink_committee = get_crosslink_committee( + state, + attestation_data.target_epoch, + attestation_data.crosslink.shard + ) + + committee_size = len(crosslink_committee) + bitfield_length = (committee_size + 7) // 8 + aggregation_bitfield = b'\x00' * bitfield_length + custody_bitfield = b'\x00' * bitfield_length + attestation = Attestation( + aggregation_bitfield=aggregation_bitfield, + data=attestation_data, + custody_bitfield=custody_bitfield, + ) + fill_aggregate_attestation(state, attestation) + if signed: + sign_attestation(state, attestation) + return attestation + + +def sign_aggregate_attestation(state: BeaconState, data: AttestationData, participants: List[int]): + signatures = [] + for validator_index in participants: + privkey = privkeys[validator_index] + signatures.append( + get_attestation_signature( + state, + data, + privkey + ) + ) + + return bls_aggregate_signatures(signatures) + + +def sign_indexed_attestation(state, indexed_attestation: IndexedAttestation): + participants = indexed_attestation.custody_bit_0_indices + indexed_attestation.custody_bit_1_indices + indexed_attestation.signature = sign_aggregate_attestation(state, indexed_attestation.data, participants) + + +def sign_attestation(state, attestation: Attestation): + participants = get_attesting_indices( + state, + attestation.data, + attestation.aggregation_bitfield, + ) + + attestation.signature = sign_aggregate_attestation(state, attestation.data, participants) + + +def get_attestation_signature(state, attestation_data, privkey, custody_bit=0b0): + message_hash = AttestationDataAndCustodyBit( + data=attestation_data, + custody_bit=custody_bit, + ).hash_tree_root() + + return bls_sign( + message_hash=message_hash, + privkey=privkey, + domain=get_domain( + state=state, + domain_type=spec.DOMAIN_ATTESTATION, + message_epoch=attestation_data.target_epoch, + ) + ) + + +def fill_aggregate_attestation(state, attestation): + crosslink_committee = get_crosslink_committee( + state, + attestation.data.target_epoch, + attestation.data.crosslink.shard, + ) + for i in range(len(crosslink_committee)): + attestation.aggregation_bitfield = set_bitfield_bit(attestation.aggregation_bitfield, i) + + +def add_attestation_to_state(state, attestation, slot): + block = build_empty_block_for_next_slot(state) + block.slot = slot + block.body.attestations.append(attestation) + process_slots(state, block.slot) + sign_block(state, block) + state_transition(state, block) diff --git a/test_libs/pyspec/eth2spec/test/helpers/attester_slashings.py b/test_libs/pyspec/eth2spec/test/helpers/attester_slashings.py new file mode 100644 index 000000000..d19b41dfe --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/attester_slashings.py @@ -0,0 +1,19 @@ +from copy import deepcopy + +from eth2spec.phase0.spec import AttesterSlashing, convert_to_indexed +from eth2spec.test.helpers.attestations import get_valid_attestation, sign_attestation + + +def get_valid_attester_slashing(state, signed_1=False, signed_2=False): + attestation_1 = get_valid_attestation(state, signed=signed_1) + + attestation_2 = deepcopy(attestation_1) + attestation_2.data.target_root = b'\x01' * 32 + + if signed_2: + sign_attestation(state, attestation_2) + + return AttesterSlashing( + attestation_1=convert_to_indexed(state, attestation_1), + attestation_2=convert_to_indexed(state, attestation_2), + ) diff --git a/test_libs/pyspec/eth2spec/test/helpers/bitfields.py b/test_libs/pyspec/eth2spec/test/helpers/bitfields.py new file mode 100644 index 000000000..7c25d073a --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/bitfields.py @@ -0,0 +1,11 @@ +def set_bitfield_bit(bitfield, i): + """ + Set the bit in ``bitfield`` at position ``i`` to ``1``. + """ + byte_index = i // 8 + bit_index = i % 8 + return ( + bitfield[:byte_index] + + bytes([bitfield[byte_index] | (1 << bit_index)]) + + bitfield[byte_index + 1:] + ) diff --git a/test_libs/pyspec/eth2spec/test/helpers/block.py b/test_libs/pyspec/eth2spec/test/helpers/block.py new file mode 100644 index 000000000..715cf82db --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/block.py @@ -0,0 +1,79 @@ +from copy import deepcopy + +from eth2spec.phase0 import spec +from eth2spec.phase0.spec import ( + BeaconBlock, + get_beacon_proposer_index, slot_to_epoch, get_domain, + process_slots, state_transition, +) +from eth2spec.test.helpers.keys import privkeys +from eth2spec.utils.bls import bls_sign, only_with_bls +from eth2spec.utils.minimal_ssz import signing_root, hash_tree_root + + +# Fully ignore the function if BLS is off, beacon-proposer index calculation is slow. +@only_with_bls() +def sign_block(state, block, proposer_index=None): + assert state.slot <= block.slot + + if proposer_index is None: + if block.slot == state.slot: + proposer_index = get_beacon_proposer_index(state) + else: + if slot_to_epoch(state.slot) + 1 > slot_to_epoch(block.slot): + print("warning: block slot far away, and no proposer index manually given." + " Signing block is slow due to transition for proposer index calculation.") + # use stub state to get proposer index of future slot + stub_state = deepcopy(state) + process_slots(stub_state, block.slot) + proposer_index = get_beacon_proposer_index(stub_state) + + privkey = privkeys[proposer_index] + + block.body.randao_reveal = bls_sign( + privkey=privkey, + message_hash=hash_tree_root(slot_to_epoch(block.slot)), + domain=get_domain( + state, + message_epoch=slot_to_epoch(block.slot), + domain_type=spec.DOMAIN_RANDAO, + ) + ) + block.signature = bls_sign( + message_hash=signing_root(block), + privkey=privkey, + domain=get_domain( + state, + spec.DOMAIN_BEACON_PROPOSER, + slot_to_epoch(block.slot))) + + +def apply_empty_block(state): + """ + Transition via an empty block (on current slot, assuming no block has been applied yet). + :return: the empty block that triggered the transition. + """ + block = build_empty_block(state, signed=True) + state_transition(state, block) + return block + + +def build_empty_block(state, slot=None, signed=False): + if slot is None: + slot = state.slot + empty_block = BeaconBlock() + empty_block.slot = slot + empty_block.body.eth1_data.deposit_count = state.deposit_index + previous_block_header = deepcopy(state.latest_block_header) + if previous_block_header.state_root == spec.ZERO_HASH: + previous_block_header.state_root = state.hash_tree_root() + empty_block.parent_root = signing_root(previous_block_header) + + if signed: + sign_block(state, empty_block) + + return empty_block + + +def build_empty_block_for_next_slot(state, signed=False): + return build_empty_block(state, state.slot + 1, signed=signed) diff --git a/test_libs/pyspec/eth2spec/test/helpers/block_header.py b/test_libs/pyspec/eth2spec/test/helpers/block_header.py new file mode 100644 index 000000000..9aba62d37 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/block_header.py @@ -0,0 +1,18 @@ +# Access constants from spec pkg reference. +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.spec import get_domain +from eth2spec.utils.bls import bls_sign +from eth2spec.utils.minimal_ssz import signing_root + + +def sign_block_header(state, header, privkey): + domain = get_domain( + state=state, + domain_type=spec.DOMAIN_BEACON_PROPOSER, + ) + header.signature = bls_sign( + message_hash=signing_root(header), + privkey=privkey, + domain=domain, + ) diff --git a/test_libs/pyspec/eth2spec/test/helpers/deposits.py b/test_libs/pyspec/eth2spec/test/helpers/deposits.py new file mode 100644 index 000000000..2db3ae03c --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/deposits.py @@ -0,0 +1,87 @@ +# Access constants from spec pkg reference. +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.spec import get_domain, DepositData, verify_merkle_branch, Deposit, ZERO_HASH +from eth2spec.test.helpers.keys import pubkeys, privkeys +from eth2spec.utils.bls import bls_sign +from eth2spec.utils.merkle_minimal import calc_merkle_tree_from_leaves, get_merkle_root, get_merkle_proof +from eth2spec.utils.minimal_ssz import signing_root + + +def build_deposit_data(state, pubkey, privkey, amount, withdrawal_credentials, signed=False): + deposit_data = DepositData( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + amount=amount, + ) + if signed: + sign_deposit_data(state, deposit_data, privkey) + return deposit_data + + +def sign_deposit_data(state, deposit_data, privkey): + signature = bls_sign( + message_hash=signing_root(deposit_data), + privkey=privkey, + domain=get_domain( + state, + spec.DOMAIN_DEPOSIT, + ) + ) + deposit_data.signature = signature + + +def build_deposit(state, + deposit_data_leaves, + pubkey, + privkey, + amount, + withdrawal_credentials, + signed): + deposit_data = build_deposit_data(state, pubkey, privkey, amount, withdrawal_credentials, signed) + + 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)) + root = get_merkle_root((tuple(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, + ) + + return deposit, root, deposit_data_leaves + + +def prepare_state_and_deposit(state, validator_index, amount, withdrawal_credentials=None, signed=False): + """ + 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] + + # insecurely use pubkey as withdrawal key if no credentials provided + if withdrawal_credentials is None: + withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX_BYTE + spec.hash(pubkey)[1:] + + deposit, root, deposit_data_leaves = build_deposit( + state, + deposit_data_leaves, + pubkey, + privkey, + amount, + withdrawal_credentials, + signed + ) + + 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/eth2spec/test/helpers/genesis.py b/test_libs/pyspec/eth2spec/test/helpers/genesis.py new file mode 100644 index 000000000..01011cacd --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/genesis.py @@ -0,0 +1,51 @@ +# Access constants from spec pkg reference. +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.spec import Eth1Data, ZERO_HASH, get_active_validator_indices +from eth2spec.test.helpers.keys import pubkeys +from eth2spec.utils.minimal_ssz import hash_tree_root + + +def build_mock_validator(i: int, balance: int): + pubkey = pubkeys[i] + # insecurely use pubkey as withdrawal key as well + withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX_BYTE + spec.hash(pubkey)[1:] + return spec.Validator( + pubkey=pubkeys[i], + withdrawal_credentials=withdrawal_credentials, + activation_eligibility_epoch=spec.FAR_FUTURE_EPOCH, + activation_epoch=spec.FAR_FUTURE_EPOCH, + exit_epoch=spec.FAR_FUTURE_EPOCH, + withdrawable_epoch=spec.FAR_FUTURE_EPOCH, + effective_balance=min(balance - balance % spec.EFFECTIVE_BALANCE_INCREMENT, spec.MAX_EFFECTIVE_BALANCE) + ) + + +def create_genesis_state(num_validators): + deposit_root = b'\x42' * 32 + + state = spec.BeaconState( + genesis_time=0, + deposit_index=num_validators, + latest_eth1_data=Eth1Data( + deposit_root=deposit_root, + deposit_count=num_validators, + block_hash=ZERO_HASH, + )) + + # We "hack" in the initial validators, + # as it is much faster than creating and processing genesis deposits for every single test case. + state.balances = [spec.MAX_EFFECTIVE_BALANCE] * num_validators + state.validator_registry = [build_mock_validator(i, state.balances[i]) for i in range(num_validators)] + + # Process genesis activations + for validator in state.validator_registry: + if validator.effective_balance >= spec.MAX_EFFECTIVE_BALANCE: + validator.activation_eligibility_epoch = spec.GENESIS_EPOCH + validator.activation_epoch = spec.GENESIS_EPOCH + + genesis_active_index_root = hash_tree_root(get_active_validator_indices(state, spec.GENESIS_EPOCH)) + for index in range(spec.LATEST_ACTIVE_INDEX_ROOTS_LENGTH): + state.latest_active_index_roots[index] = genesis_active_index_root + + return state diff --git a/test_libs/pyspec/eth2spec/test/helpers/keys.py b/test_libs/pyspec/eth2spec/test/helpers/keys.py new file mode 100644 index 000000000..f47cd7c10 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/keys.py @@ -0,0 +1,6 @@ +from py_ecc import bls +from eth2spec.phase0 import spec + +privkeys = [i + 1 for i in range(spec.SLOTS_PER_EPOCH * 16)] +pubkeys = [bls.privtopub(privkey) for privkey in privkeys] +pubkey_to_privkey = {pubkey: privkey for privkey, pubkey in zip(privkeys, pubkeys)} diff --git a/test_libs/pyspec/eth2spec/test/helpers/proposer_slashings.py b/test_libs/pyspec/eth2spec/test/helpers/proposer_slashings.py new file mode 100644 index 000000000..02629f7da --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/proposer_slashings.py @@ -0,0 +1,35 @@ +from copy import deepcopy + +from eth2spec.phase0.spec import ( + get_current_epoch, get_active_validator_indices, BeaconBlockHeader, ProposerSlashing +) +from eth2spec.test.helpers.block_header import sign_block_header +from eth2spec.test.helpers.keys import pubkey_to_privkey + + +def get_valid_proposer_slashing(state, signed_1=False, signed_2=False): + current_epoch = get_current_epoch(state) + validator_index = get_active_validator_indices(state, current_epoch)[-1] + privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] + slot = state.slot + + header_1 = BeaconBlockHeader( + slot=slot, + parent_root=b'\x33' * 32, + state_root=b'\x44' * 32, + block_body_root=b'\x55' * 32, + ) + header_2 = deepcopy(header_1) + header_2.parent_root = b'\x99' * 32 + header_2.slot = slot + 1 + + if signed_1: + sign_block_header(state, header_1, privkey) + if signed_2: + sign_block_header(state, header_2, privkey) + + return ProposerSlashing( + proposer_index=validator_index, + header_1=header_1, + header_2=header_2, + ) diff --git a/test_libs/pyspec/eth2spec/test/helpers/state.py b/test_libs/pyspec/eth2spec/test/helpers/state.py new file mode 100644 index 000000000..1137561f1 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/state.py @@ -0,0 +1,31 @@ +# Access constants from spec pkg reference. +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.spec import process_slots + + +def get_balance(state, index): + return state.balances[index] + + +def next_slot(state): + """ + Transition to the next slot. + """ + process_slots(state, state.slot + 1) + + +def next_epoch(state): + """ + Transition to the start slot of the next epoch + """ + slot = state.slot + spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) + process_slots(state, slot) + + +def get_state_root(state, slot) -> bytes: + """ + Return the state root at a recent ``slot``. + """ + assert slot < state.slot <= slot + spec.SLOTS_PER_HISTORICAL_ROOT + return state.latest_state_roots[slot % spec.SLOTS_PER_HISTORICAL_ROOT] diff --git a/test_libs/pyspec/eth2spec/test/helpers/transfers.py b/test_libs/pyspec/eth2spec/test/helpers/transfers.py new file mode 100644 index 000000000..2045f48ad --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/transfers.py @@ -0,0 +1,55 @@ +# Access constants from spec pkg reference. +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.spec import get_current_epoch, get_active_validator_indices, Transfer, get_domain +from eth2spec.test.helpers.keys import pubkeys, privkeys +from eth2spec.test.helpers.state import get_balance +from eth2spec.utils.bls import bls_sign +from eth2spec.utils.minimal_ssz import signing_root + + +def get_valid_transfer(state, slot=None, sender_index=None, amount=None, fee=None, signed=False): + if slot is None: + slot = state.slot + current_epoch = get_current_epoch(state) + if sender_index is None: + sender_index = get_active_validator_indices(state, current_epoch)[-1] + recipient_index = get_active_validator_indices(state, current_epoch)[0] + transfer_pubkey = pubkeys[-1] + transfer_privkey = privkeys[-1] + + if fee is None: + fee = get_balance(state, sender_index) // 32 + if amount is None: + amount = get_balance(state, sender_index) - fee + + transfer = Transfer( + sender=sender_index, + recipient=recipient_index, + amount=amount, + fee=fee, + slot=slot, + pubkey=transfer_pubkey, + ) + if signed: + sign_transfer(state, transfer, transfer_privkey) + + # ensure withdrawal_credentials reproducible + state.validator_registry[transfer.sender].withdrawal_credentials = ( + spec.BLS_WITHDRAWAL_PREFIX_BYTE + spec.hash(transfer.pubkey)[1:] + ) + + return transfer + + +def sign_transfer(state, transfer, privkey): + transfer.signature = bls_sign( + message_hash=signing_root(transfer), + privkey=privkey, + domain=get_domain( + state=state, + domain_type=spec.DOMAIN_TRANSFER, + message_epoch=get_current_epoch(state), + ) + ) + return transfer diff --git a/test_libs/pyspec/eth2spec/test/helpers/voluntary_exits.py b/test_libs/pyspec/eth2spec/test/helpers/voluntary_exits.py new file mode 100644 index 000000000..54376d694 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/helpers/voluntary_exits.py @@ -0,0 +1,28 @@ +# Access constants from spec pkg reference. +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.spec import VoluntaryExit, get_domain +from eth2spec.utils.bls import bls_sign +from eth2spec.utils.minimal_ssz import signing_root + + +def build_voluntary_exit(state, epoch, validator_index, privkey, signed=False): + voluntary_exit = VoluntaryExit( + epoch=epoch, + validator_index=validator_index, + ) + if signed: + sign_voluntary_exit(state, voluntary_exit, privkey) + return voluntary_exit + + +def sign_voluntary_exit(state, voluntary_exit, privkey): + voluntary_exit.signature = bls_sign( + message_hash=signing_root(voluntary_exit), + privkey=privkey, + domain=get_domain( + state=state, + domain_type=spec.DOMAIN_VOLUNTARY_EXIT, + message_epoch=voluntary_exit.epoch, + ) + ) diff --git a/test_libs/pyspec/eth2spec/test/sanity/__init__.py b/test_libs/pyspec/eth2spec/test/sanity/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_libs/pyspec/eth2spec/test/sanity/test_blocks.py b/test_libs/pyspec/eth2spec/test/sanity/test_blocks.py new file mode 100644 index 000000000..654a41d62 --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/sanity/test_blocks.py @@ -0,0 +1,406 @@ +from copy import deepcopy + +import eth2spec.phase0.spec as spec +from eth2spec.utils.bls import bls_sign + +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, + state_transition, +) +from eth2spec.test.helpers.state import get_balance +from eth2spec.test.helpers.transfers import get_valid_transfer +from eth2spec.test.helpers.block import build_empty_block_for_next_slot, sign_block +from eth2spec.test.helpers.keys import privkeys, pubkeys +from eth2spec.test.helpers.attester_slashings import get_valid_attester_slashing +from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing +from eth2spec.test.helpers.attestations import get_valid_attestation +from eth2spec.test.helpers.deposits import prepare_state_and_deposit + +from eth2spec.test.context import spec_state_test, never_bls + + +@never_bls +@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, signed=True) + 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.parent_root + + +@never_bls +@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 + sign_block(state, block) + 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.parent_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 + sign_block(state, block) + 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.parent_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 +# sign_block(state, block, proposer_index=0) +# 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, signed_1=True, signed_2=True) + 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) + sign_block(state, block) + 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, signed_1=True, signed_2=True) + validator_index = (attester_slashing.attestation_1.custody_bit_0_indices + + attester_slashing.attestation_1.custody_bit_1_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) + sign_block(state, block) + 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, signed=True) + + yield 'pre', state + + block = build_empty_block_for_next_slot(state) + block.body.deposits.append(deposit) + sign_block(state, block) + + 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) + sign_block(state, block) + + 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, signed=True) + + # 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) + sign_block(state, attestation_block) + 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 + sign_block(state, epoch_block) + 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) + sign_block(state, initiate_exit_block) + 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 + sign_block(state, exit_block) + 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, signed=True) + 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) + sign_block(state, block) + + 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 + sign_block(state, block) + 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, signed=True) + 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/sanity/test_slots.py b/test_libs/pyspec/eth2spec/test/sanity/test_slots.py new file mode 100644 index 000000000..4c3897a6c --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/sanity/test_slots.py @@ -0,0 +1,57 @@ +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.spec import process_slots +from eth2spec.test.helpers.state import get_state_root +from eth2spec.test.context import spec_state_test + + +@spec_state_test +def test_slots_1(state): + pre_slot = state.slot + pre_root = state.hash_tree_root() + yield 'pre', state + + slots = 1 + yield 'slots', slots + process_slots(state, state.slot + slots) + + yield 'post', state + assert state.slot == pre_slot + 1 + assert get_state_root(state, pre_slot) == pre_root + + +@spec_state_test +def test_slots_2(state): + yield 'pre', state + slots = 2 + yield 'slots', slots + process_slots(state, state.slot + slots) + yield 'post', state + + +@spec_state_test +def test_empty_epoch(state): + yield 'pre', state + slots = spec.SLOTS_PER_EPOCH + yield 'slots', slots + process_slots(state, state.slot + slots) + yield 'post', state + + +@spec_state_test +def test_double_empty_epoch(state): + yield 'pre', state + slots = spec.SLOTS_PER_EPOCH * 2 + yield 'slots', slots + process_slots(state, state.slot + slots) + yield 'post', state + + +@spec_state_test +def test_over_epoch_boundary(state): + process_slots(state, state.slot + (spec.SLOTS_PER_EPOCH // 2)) + yield 'pre', state + slots = spec.SLOTS_PER_EPOCH + yield 'slots', slots + process_slots(state, state.slot + slots) + yield 'post', state diff --git a/test_libs/pyspec/tests/test_finality.py b/test_libs/pyspec/eth2spec/test/test_finality.py similarity index 55% rename from test_libs/pyspec/tests/test_finality.py rename to test_libs/pyspec/eth2spec/test/test_finality.py index 816dfd6bd..cdd09bf23 100644 --- a/test_libs/pyspec/tests/test_finality.py +++ b/test_libs/pyspec/eth2spec/test/test_finality.py @@ -1,20 +1,14 @@ from copy import deepcopy -import pytest - import eth2spec.phase0.spec as spec - -from .helpers import ( - build_empty_block_for_next_slot, - fill_aggregate_attestation, +from eth2spec.phase0.spec import ( get_current_epoch, get_epoch_start_slot, - get_valid_attestation, - next_epoch, ) - -# mark entire file as 'state' -pytestmark = pytest.mark.state +from .context import spec_state_test, never_bls +from .helpers.state import next_epoch +from .helpers.block import build_empty_block_for_next_slot, apply_empty_block +from .helpers.attestations import get_valid_attestation def check_finality(state, @@ -55,13 +49,11 @@ def next_epoch_with_attestations(state, slot_to_attest = post_state.slot - spec.MIN_ATTESTATION_INCLUSION_DELAY + 1 if slot_to_attest >= get_epoch_start_slot(get_current_epoch(post_state)): cur_attestation = get_valid_attestation(post_state, slot_to_attest) - fill_aggregate_attestation(post_state, cur_attestation) block.body.attestations.append(cur_attestation) if fill_prev_epoch: slot_to_attest = post_state.slot - spec.SLOTS_PER_EPOCH + 1 prev_attestation = get_valid_attestation(post_state, slot_to_attest) - fill_aggregate_attestation(post_state, prev_attestation) block.body.attestations.append(prev_attestation) spec.state_transition(post_state, block) @@ -70,126 +62,140 @@ def next_epoch_with_attestations(state, return state, blocks, post_state +@never_bls +@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 +@never_bls +@spec_state_test def test_finality_rule_1(state): # get past first two epochs that finality does not run on next_epoch(state) + apply_empty_block(state) next_epoch(state) + apply_empty_block(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 +@never_bls +@spec_state_test def test_finality_rule_2(state): # get past first two epochs that finality does not run on next_epoch(state) + apply_empty_block(state) next_epoch(state) + apply_empty_block(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 +@never_bls +@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) + apply_empty_block(state) next_epoch(state) + apply_empty_block(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/utils.py b/test_libs/pyspec/eth2spec/test/utils.py new file mode 100644 index 000000000..b61801c3d --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/utils.py @@ -0,0 +1,80 @@ +from typing import Dict, Any, Callable, Iterable +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 + has_contents = False + # put all generated data into a dict. + for data in fn(*args, **kw): + has_contents = True + # 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 + if has_contents: + return out + else: + return None + else: + # just complete the function, ignore all yielded data, we are not using it + for _ in fn(*args, **kw): + continue + return None + return entry + return runner + + +def with_tags(tags: Dict[str, Any]): + """ + Decorator factory, adds tags (key, value) pairs to the output of the function. + Useful to build test-vector annotations with. + This decorator is applied after the ``spectest`` decorator is applied. + :param tags: dict of tags + :return: Decorator. + """ + def runner(fn): + def entry(*args, **kw): + fn_out = fn(*args, **kw) + # do not add tags if the function is not returning a dict at all (i.e. not in generator mode) + if fn_out is None: + return None + return {**tags, **fn_out} + return entry + return runner + + +def with_args(create_args: Callable[[], Iterable[Any]]): + """ + Decorator factory, adds given extra arguments to the decorated function. + :param create_args: function to create arguments with. + :return: Decorator. + """ + def runner(fn): + # this wraps the function, to hide that the function actually yielding data. + def entry(*args, **kw): + return fn(*(list(create_args()) + list(args)), **kw) + return entry + return runner diff --git a/test_libs/pyspec/eth2spec/utils/bls.py b/test_libs/pyspec/eth2spec/utils/bls.py new file mode 100644 index 000000000..52f1fed63 --- /dev/null +++ b/test_libs/pyspec/eth2spec/utils/bls.py @@ -0,0 +1,46 @@ +from py_ecc import bls + +# Flag to make BLS active or not. Used for testing, do not ignore BLS in production unless you know what you are doing. +bls_active = True + +STUB_SIGNATURE = b'\x11' * 96 +STUB_PUBKEY = b'\x22' * 48 + + +def only_with_bls(alt_return=None): + """ + Decorator factory to make a function only run when BLS is active. Otherwise return the default. + """ + def runner(fn): + def entry(*args, **kw): + if bls_active: + return fn(*args, **kw) + else: + return alt_return + return entry + return runner + + +@only_with_bls(alt_return=True) +def bls_verify(pubkey, message_hash, signature, domain): + return bls.verify(message_hash=message_hash, pubkey=pubkey, signature=signature, domain=domain) + + +@only_with_bls(alt_return=True) +def bls_verify_multiple(pubkeys, message_hashes, signature, domain): + return bls.verify_multiple(pubkeys, message_hashes, signature, domain) + + +@only_with_bls(alt_return=STUB_PUBKEY) +def bls_aggregate_pubkeys(pubkeys): + return bls.aggregate_pubkeys(pubkeys) + + +@only_with_bls(alt_return=STUB_SIGNATURE) +def bls_aggregate_signatures(signatures): + return bls.aggregate_signatures(signatures) + + +@only_with_bls(alt_return=STUB_SIGNATURE) +def bls_sign(message_hash, privkey, domain): + return bls.sign(message_hash=message_hash, privkey=privkey, domain=domain) diff --git a/test_libs/pyspec/eth2spec/utils/bls_stub.py b/test_libs/pyspec/eth2spec/utils/bls_stub.py deleted file mode 100644 index ae97de175..000000000 --- a/test_libs/pyspec/eth2spec/utils/bls_stub.py +++ /dev/null @@ -1,12 +0,0 @@ - - -def bls_verify(pubkey, message_hash, signature, domain): - return True - - -def bls_verify_multiple(pubkeys, message_hashes, signature, domain): - return True - - -def bls_aggregate_pubkeys(pubkeys): - return b'\x42' * 48 diff --git a/test_libs/pyspec/eth2spec/utils/hash_function.py b/test_libs/pyspec/eth2spec/utils/hash_function.py index 3fee63d82..acd13edc4 100644 --- a/test_libs/pyspec/eth2spec/utils/hash_function.py +++ b/test_libs/pyspec/eth2spec/utils/hash_function.py @@ -1,6 +1,4 @@ from hashlib import sha256 -# from eth_utils import keccak def hash(x): return sha256(x).digest() -# def hash(x): return keccak(x) 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 763178717..000000000 --- a/test_libs/pyspec/tests/block_processing/test_process_attestation.py +++ /dev/null @@ -1,195 +0,0 @@ -from copy import deepcopy -import pytest - -import eth2spec.phase0.spec as spec - -from eth2spec.phase0.spec import ( - get_current_epoch, - process_attestation, - slot_to_epoch, - state_transition, -) -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_success_since_max_epochs_per_crosslink(state): - for _ in range(spec.MAX_EPOCHS_PER_CROSSLINK + 2): - next_epoch(state) - - attestation = get_valid_attestation(state) - data = attestation.data - assert data.crosslink.end_epoch - data.crosslink.start_epoch == spec.MAX_EPOCHS_PER_CROSSLINK - - for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): - next_slot(state) - - 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) - - attestation.data.crosslink.parent_root = b'\x27' * 32 - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state - - -def test_bad_crosslink_start_epoch(state): - next_epoch(state) - attestation = get_valid_attestation(state) - for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): - next_slot(state) - - attestation.data.crosslink.start_epoch += 1 - - pre_state, post_state = run_attestation_processing(state, attestation, False) - - return pre_state, attestation, post_state - - -def test_bad_crosslink_end_epoch(state): - next_epoch(state) - attestation = get_valid_attestation(state) - for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): - next_slot(state) - - attestation.data.crosslink.end_epoch += 1 - - 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) - - return pre_state, attestation, post_state diff --git a/test_libs/pyspec/tests/block_processing/test_process_attester_slashing.py b/test_libs/pyspec/tests/block_processing/test_process_attester_slashing.py deleted file mode 100644 index 2ea16f13d..000000000 --- a/test_libs/pyspec/tests/block_processing/test_process_attester_slashing.py +++ /dev/null @@ -1,117 +0,0 @@ -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 ( - get_balance, - get_valid_attester_slashing, - next_epoch, -) - -# mark entire file as 'attester_slashing' -pytestmark = pytest.mark.attester_slashings - - -def run_attester_slashing_processing(state, attester_slashing, valid=True): - """ - Run ``process_attester_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_attester_slashing(post_state, attester_slashing) - return state, None - - process_attester_slashing(post_state, attester_slashing) - - slashed_index = attester_slashing.attestation_1.custody_bit_0_indices[0] - slashed_validator = post_state.validator_registry[slashed_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, slashed_index) < - get_balance(state, slashed_index) - ) - proposer_index = get_beacon_proposer_index(state) - # gained whistleblower reward - assert ( - get_balance(post_state, proposer_index) > - get_balance(state, proposer_index) - ) - - return state, post_state - - -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 - - -def test_success_surround(state): - next_epoch(state) - state.current_justified_epoch += 1 - attester_slashing = get_valid_attester_slashing(state) - - # set attestion1 to surround attestation 2 - 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 - - -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 - - -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 - - -def test_participants_already_slashed(state): - attester_slashing = get_valid_attester_slashing(state) - - # set all indices to slashed - attestation_1 = attester_slashing.attestation_1 - validator_indices = attestation_1.custody_bit_0_indices + attestation_1.custody_bit_1_indices - 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 - - -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 diff --git a/test_libs/pyspec/tests/block_processing/test_process_block_header.py b/test_libs/pyspec/tests/block_processing/test_process_block_header.py deleted file mode 100644 index 32b409e5a..000000000 --- a/test_libs/pyspec/tests/block_processing/test_process_block_header.py +++ /dev/null @@ -1,76 +0,0 @@ -from copy import deepcopy -import pytest - - -from eth2spec.phase0.spec import ( - get_beacon_proposer_index, - process_slot, - process_block_header, -) -from tests.helpers import ( - advance_slot, - build_empty_block_for_next_slot, - next_slot, -) - -# mark entire file as 'header' -pytestmark = pytest.mark.header - - -def prepare_state_for_header_processing(state): - process_slot(state) - advance_slot(state) - - -def run_block_header_processing(state, block, valid=True): - """ - Run ``process_block_header`` returning the pre and post state. - If ``valid == False``, run expecting ``AssertionError`` - """ - prepare_state_for_header_processing(state) - post_state = deepcopy(state) - - if not valid: - with pytest.raises(AssertionError): - process_block_header(post_state, block) - return state, None - - process_block_header(post_state, block) - return state, post_state - - -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 - - -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 - - -def test_invalid_parent_block_root(state): - block = build_empty_block_for_next_slot(state) - block.parent_root = b'\12' * 32 # invalid prev root - - pre_state, post_state = run_block_header_processing(state, block, valid=False) - return pre_state, block, None - - -def test_proposer_slashed(state): - # use stub state to get proposer index of next slot - stub_state = deepcopy(state) - next_slot(stub_state) - proposer_index = get_beacon_proposer_index(stub_state) - - # set proposer to slashed - state.validator_registry[proposer_index].slashed = True - - 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 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/block_processing/test_process_transfer.py b/test_libs/pyspec/tests/block_processing/test_process_transfer.py deleted file mode 100644 index 0eeaa7792..000000000 --- a/test_libs/pyspec/tests/block_processing/test_process_transfer.py +++ /dev/null @@ -1,141 +0,0 @@ -from copy import deepcopy -import pytest - -import eth2spec.phase0.spec as spec - -from eth2spec.phase0.spec import ( - get_active_validator_indices, - get_beacon_proposer_index, - get_current_epoch, - process_transfer, -) -from tests.helpers import ( - get_valid_transfer, - next_epoch, -) - - -# mark entire file as 'transfers' -pytestmark = pytest.mark.transfers - - -def run_transfer_processing(state, transfer, valid=True): - """ - Run ``process_transfer`` returning the pre and post state. - 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] - 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 - - -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 - - -def test_success_withdrawable(state): - next_epoch(state) - - transfer = get_valid_transfer(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 - - -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) - - pre_state, post_state = run_transfer_processing(state, transfer) - - return pre_state, transfer, post_state - - -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 - - -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 - - -def test_insufficient_balance(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) - - # 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 - - -def test_no_dust(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) - - # 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 - - -def test_invalid_pubkey(state): - transfer = get_valid_transfer(state) - state.validator_registry[transfer.sender].withdrawal_credentials = spec.ZERO_HASH - - # 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 diff --git a/test_libs/pyspec/tests/block_processing/test_voluntary_exit.py b/test_libs/pyspec/tests/block_processing/test_voluntary_exit.py deleted file mode 100644 index c58c5238a..000000000 --- a/test_libs/pyspec/tests/block_processing/test_voluntary_exit.py +++ /dev/null @@ -1,163 +0,0 @@ -from copy import deepcopy -import pytest - -import eth2spec.phase0.spec as spec - -from eth2spec.phase0.spec import ( - get_active_validator_indices, - get_churn_limit, - get_current_epoch, - process_voluntary_exit, -) -from tests.helpers import ( - build_voluntary_exit, - pubkey_to_privkey, -) - - -# mark entire file as 'voluntary_exits' -pytestmark = pytest.mark.voluntary_exits - - -def run_voluntary_exit_processing(state, voluntary_exit, valid=True): - """ - Run ``process_voluntary_exit`` returning the pre and post state. - If ``valid == False``, run expecting ``AssertionError`` - """ - post_state = deepcopy(state) - - if not valid: - with pytest.raises(AssertionError): - process_voluntary_exit(post_state, voluntary_exit) - return state, None - - process_voluntary_exit(post_state, voluntary_exit) - - 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 - - return state, post_state - - -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 - - 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, - ) - - pre_state, post_state = run_voluntary_exit_processing(state, voluntary_exit) - return pre_state, voluntary_exit, post_state - - -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 - - current_epoch = get_current_epoch(state) - - # exit `MAX_EXITS_PER_EPOCH` - initial_indices = get_active_validator_indices(state, current_epoch)[:get_churn_limit(state)] - post_state = state - for index in initial_indices: - privkey = pubkey_to_privkey[state.validator_registry[index].pubkey] - voluntary_exit = build_voluntary_exit( - state, - current_epoch, - index, - privkey, - ) - - pre_state, post_state = run_voluntary_exit_processing(post_state, voluntary_exit) - - # exit an additional validator - validator_index = get_active_validator_indices(state, current_epoch)[-1] - privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] - voluntary_exit = build_voluntary_exit( - state, - current_epoch, - validator_index, - privkey, - ) - - pre_state, post_state = run_voluntary_exit_processing(post_state, voluntary_exit) - - assert ( - post_state.validator_registry[validator_index].exit_epoch == - post_state.validator_registry[initial_indices[0]].exit_epoch + 1 - ) - - return pre_state, voluntary_exit, post_state - - -def test_validator_not_active(state): - 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] - - state.validator_registry[validator_index].activation_epoch = spec.FAR_FUTURE_EPOCH - - # - # build and test voluntary exit - # - voluntary_exit = build_voluntary_exit( - state, - current_epoch, - validator_index, - privkey, - ) - - pre_state, post_state = run_voluntary_exit_processing(state, voluntary_exit, False) - return pre_state, voluntary_exit, post_state - - -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 - - 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] - - # but validator already has exited - state.validator_registry[validator_index].exit_epoch = current_epoch + 2 - - voluntary_exit = build_voluntary_exit( - state, - current_epoch, - validator_index, - privkey, - ) - - pre_state, post_state = run_voluntary_exit_processing(state, voluntary_exit, False) - return pre_state, voluntary_exit, post_state - - -def test_validator_not_active_long_enough(state): - 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, - ) - - assert ( - current_epoch - state.validator_registry[validator_index].activation_epoch < - spec.PERSISTENT_COMMITTEE_PERIOD - ) - - pre_state, post_state = run_voluntary_exit_processing(state, voluntary_exit, False) - return pre_state, voluntary_exit, 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/helpers.py b/test_libs/pyspec/tests/helpers.py deleted file mode 100644 index ca40bf1d8..000000000 --- a/test_libs/pyspec/tests/helpers.py +++ /dev/null @@ -1,429 +0,0 @@ -from copy import deepcopy - -from py_ecc import bls - -import eth2spec.phase0.spec as spec -from eth2spec.utils.ssz.ssz_impl import signing_root -from eth2spec.phase0.spec import ( - # constants - ZERO_HASH, - MAX_EPOCHS_PER_CROSSLINK, - # SSZ - Attestation, - AttestationData, - AttestationDataAndCustodyBit, - AttesterSlashing, - BeaconBlock, - BeaconBlockHeader, - Crosslink, - Deposit, - DepositData, - Eth1Data, - ProposerSlashing, - Transfer, - VoluntaryExit, - # functions - convert_to_indexed, - bls_domain, - get_active_validator_indices, - get_attesting_indices, - get_block_root, - get_block_root_at_slot, - get_crosslink_committee, - get_current_epoch, - get_domain, - get_epoch_start_slot, - get_genesis_beacon_state, - get_previous_epoch, - get_shard_delta, - hash_tree_root, - slot_to_epoch, - state_transition, - verify_merkle_branch, - hash, -) -from eth2spec.utils.merkle_minimal import ( - calc_merkle_tree_from_leaves, - get_merkle_proof, - get_merkle_root, -) - - -privkeys = [i + 1 for i in range(1024)] -pubkeys = [bls.privtopub(privkey) for privkey in privkeys] -pubkey_to_privkey = {pubkey: privkey for privkey, pubkey in zip(privkeys, pubkeys)} - - -def advance_slot(state) -> None: - state.slot += 1 - - -def get_balance(state, index): - return state.balances[index] - - -def set_bitfield_bit(bitfield, i): - """ - Set the bit in ``bitfield`` at position ``i`` to ``1``. - """ - byte_index = i // 8 - bit_index = i % 8 - return ( - bitfield[:byte_index] + - bytes([bitfield[byte_index] | (1 << bit_index)]) + - bitfield[byte_index+1:] - ) - - -def create_mock_genesis_validator_deposits(num_validators, deposit_data_leaves=None): - if not deposit_data_leaves: - deposit_data_leaves = [] - signature = b'\x33' * 96 - - deposit_data_list = [] - for i in range(num_validators): - pubkey = pubkeys[i] - deposit_data = DepositData( - pubkey=pubkey, - # insecurely use pubkey as withdrawal key as well - withdrawal_credentials=spec.BLS_WITHDRAWAL_PREFIX_BYTE + hash(pubkey)[1:], - amount=spec.MAX_EFFECTIVE_BALANCE, - signature=signature, - ) - item = deposit_data.hash_tree_root() - deposit_data_leaves.append(item) - tree = calc_merkle_tree_from_leaves(tuple(deposit_data_leaves)) - root = get_merkle_root((tuple(deposit_data_leaves))) - proof = list(get_merkle_proof(tree, item_index=i)) - assert verify_merkle_branch(item, proof, spec.DEPOSIT_CONTRACT_TREE_DEPTH, i, root) - deposit_data_list.append(deposit_data) - - genesis_validator_deposits = [] - for i in range(num_validators): - genesis_validator_deposits.append(Deposit( - proof=list(get_merkle_proof(tree, item_index=i)), - index=i, - data=deposit_data_list[i] - )) - return genesis_validator_deposits, root - - -def create_genesis_state(num_validators, deposit_data_leaves=None): - initial_deposits, deposit_root = create_mock_genesis_validator_deposits( - num_validators, - deposit_data_leaves, - ) - return get_genesis_beacon_state( - initial_deposits, - genesis_time=0, - genesis_eth1_data=Eth1Data( - deposit_root=deposit_root, - deposit_count=len(initial_deposits), - block_hash=spec.ZERO_HASH, - ), - ) - - -def build_empty_block_for_next_slot(state): - empty_block = BeaconBlock() - empty_block.slot = state.slot + 1 - empty_block.body.eth1_data.deposit_count = state.deposit_index - previous_block_header = deepcopy(state.latest_block_header) - if previous_block_header.state_root == spec.ZERO_HASH: - previous_block_header.state_root = state.hash_tree_root() - empty_block.parent_root = signing_root(previous_block_header) - return empty_block - - -def build_deposit_data(state, pubkey, privkey, amount): - deposit_data = DepositData( - pubkey=pubkey, - # insecurely use pubkey as withdrawal key as well - withdrawal_credentials=spec.BLS_WITHDRAWAL_PREFIX_BYTE + hash(pubkey)[1:], - amount=amount, - ) - signature = bls.sign( - message_hash=signing_root(deposit_data), - privkey=privkey, - domain=bls_domain(spec.DOMAIN_DEPOSIT), - ) - deposit_data.signature = signature - return deposit_data - - -def build_attestation_data(state, slot, shard): - assert state.slot >= slot - - if slot == state.slot: - block_root = build_empty_block_for_next_slot(state).parent_root - else: - block_root = get_block_root_at_slot(state, slot) - - current_epoch_start_slot = get_epoch_start_slot(get_current_epoch(state)) - if slot < current_epoch_start_slot: - epoch_boundary_root = get_block_root(state, get_previous_epoch(state)) - elif slot == current_epoch_start_slot: - epoch_boundary_root = block_root - else: - epoch_boundary_root = get_block_root(state, get_current_epoch(state)) - - if slot < current_epoch_start_slot: - justified_epoch = state.previous_justified_epoch - justified_block_root = state.previous_justified_root - else: - justified_epoch = state.current_justified_epoch - justified_block_root = state.current_justified_root - - crosslinks = state.current_crosslinks if slot_to_epoch(slot) == get_current_epoch(state) else state.previous_crosslinks - parent_crosslink = crosslinks[shard] - return AttestationData( - beacon_block_root=block_root, - source_epoch=justified_epoch, - source_root=justified_block_root, - target_epoch=slot_to_epoch(slot), - target_root=epoch_boundary_root, - crosslink=Crosslink( - shard=shard, - start_epoch=parent_crosslink.end_epoch, - end_epoch=min(slot_to_epoch(slot), parent_crosslink.end_epoch + MAX_EPOCHS_PER_CROSSLINK), - data_root=spec.ZERO_HASH, - parent_root=hash_tree_root(parent_crosslink), - ), - ) - - -def build_voluntary_exit(state, epoch, validator_index, privkey): - voluntary_exit = VoluntaryExit( - epoch=epoch, - validator_index=validator_index, - ) - voluntary_exit.signature = bls.sign( - message_hash=signing_root(voluntary_exit), - privkey=privkey, - domain=get_domain( - state=state, - domain_type=spec.DOMAIN_VOLUNTARY_EXIT, - message_epoch=epoch, - ) - ) - - return voluntary_exit - - -def build_deposit(state, - deposit_data_leaves, - pubkey, - privkey, - amount): - deposit_data = build_deposit_data(state, pubkey, 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)) - root = get_merkle_root((tuple(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, - ) - - return deposit, root, deposit_data_leaves - - -def get_valid_proposer_slashing(state): - current_epoch = get_current_epoch(state) - validator_index = get_active_validator_indices(state, current_epoch)[-1] - privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] - slot = state.slot - - header_1 = BeaconBlockHeader( - slot=slot, - parent_root=ZERO_HASH, - state_root=ZERO_HASH, - body_root=ZERO_HASH, - ) - header_2 = deepcopy(header_1) - header_2.parent_root = b'\x02' * 32 - header_2.slot = slot + 1 - - domain = get_domain( - state=state, - domain_type=spec.DOMAIN_BEACON_PROPOSER, - ) - header_1.signature = bls.sign( - message_hash=signing_root(header_1), - privkey=privkey, - domain=domain, - ) - header_2.signature = bls.sign( - message_hash=signing_root(header_2), - privkey=privkey, - domain=domain, - ) - - return ProposerSlashing( - proposer_index=validator_index, - header_1=header_1, - header_2=header_2, - ) - - -def get_valid_attester_slashing(state): - attestation_1 = get_valid_attestation(state) - attestation_2 = deepcopy(attestation_1) - attestation_2.data.target_root = b'\x01' * 32 - - return AttesterSlashing( - attestation_1=convert_to_indexed(state, attestation_1), - attestation_2=convert_to_indexed(state, attestation_2), - ) - - -def get_valid_attestation(state, slot=None): - if slot is None: - slot = state.slot - - if slot_to_epoch(slot) == get_current_epoch(state): - shard = (state.latest_start_shard + slot) % spec.SLOTS_PER_EPOCH - else: - previous_shard_delta = get_shard_delta(state, get_previous_epoch(state)) - shard = (state.latest_start_shard - previous_shard_delta + slot) % spec.SHARD_COUNT - - attestation_data = build_attestation_data(state, slot, shard) - - crosslink_committee = get_crosslink_committee(state, attestation_data.target_epoch, attestation_data.crosslink.shard) - - committee_size = len(crosslink_committee) - bitfield_length = (committee_size + 7) // 8 - aggregation_bitfield = b'\xC0' + b'\x00' * (bitfield_length - 1) - custody_bitfield = b'\x00' * bitfield_length - attestation = Attestation( - aggregation_bitfield=aggregation_bitfield, - data=attestation_data, - custody_bitfield=custody_bitfield, - ) - participants = get_attesting_indices( - state, - attestation.data, - attestation.aggregation_bitfield, - ) - assert len(participants) == 2 - - signatures = [] - for validator_index in participants: - privkey = privkeys[validator_index] - signatures.append( - get_attestation_signature( - state, - attestation.data, - privkey - ) - ) - - attestation.aggregation_signature = bls.aggregate_signatures(signatures) - return attestation - - -def get_valid_transfer(state, slot=None, sender_index=None, amount=None, fee=None): - if slot is None: - slot = state.slot - current_epoch = get_current_epoch(state) - if sender_index is None: - sender_index = get_active_validator_indices(state, current_epoch)[-1] - recipient_index = get_active_validator_indices(state, current_epoch)[0] - transfer_pubkey = pubkeys[-1] - transfer_privkey = privkeys[-1] - - if fee is None: - fee = get_balance(state, sender_index) // 32 - if amount is None: - amount = get_balance(state, sender_index) - fee - - transfer = Transfer( - sender=sender_index, - recipient=recipient_index, - amount=amount, - fee=fee, - slot=slot, - pubkey=transfer_pubkey, - signature=ZERO_HASH, - ) - transfer.signature = bls.sign( - message_hash=signing_root(transfer), - privkey=transfer_privkey, - domain=get_domain( - state=state, - domain_type=spec.DOMAIN_TRANSFER, - message_epoch=get_current_epoch(state), - ) - ) - - # ensure withdrawal_credentials reproducable - state.validator_registry[transfer.sender].withdrawal_credentials = ( - spec.BLS_WITHDRAWAL_PREFIX_BYTE + spec.hash(transfer.pubkey)[1:] - ) - - return transfer - - -def get_attestation_signature(state, attestation_data, privkey, custody_bit=0b0): - message_hash = AttestationDataAndCustodyBit( - data=attestation_data, - custody_bit=custody_bit, - ).hash_tree_root() - - return bls.sign( - message_hash=message_hash, - privkey=privkey, - domain=get_domain( - state=state, - domain_type=spec.DOMAIN_ATTESTATION, - message_epoch=attestation_data.target_epoch, - ) - ) - - -def fill_aggregate_attestation(state, attestation): - crosslink_committee = get_crosslink_committee(state, attestation.data.target_epoch, attestation.data.crosslink.shard) - for i in range(len(crosslink_committee)): - attestation.aggregation_bitfield = set_bitfield_bit(attestation.aggregation_bitfield, i) - - -def add_attestation_to_state(state, attestation, slot): - block = build_empty_block_for_next_slot(state) - block.slot = slot - block.body.attestations.append(attestation) - state_transition(state, block) - - -def next_slot(state): - """ - Transition to the next slot via an empty block. - Return the empty block that triggered the transition. - """ - block = build_empty_block_for_next_slot(state) - state_transition(state, block) - return block - - -def next_epoch(state): - """ - Transition to the start slot of the next epoch via an empty block. - Return the empty block that triggered the transition. - """ - block = build_empty_block_for_next_slot(state) - block.slot += spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) - state_transition(state, block) - return block - - -def get_state_root(state, slot) -> bytes: - """ - Return the state root at a recent ``slot``. - """ - assert slot < state.slot <= slot + spec.SLOTS_PER_HISTORICAL_ROOT - return state.latest_state_roots[slot % spec.SLOTS_PER_HISTORICAL_ROOT] diff --git a/test_libs/pyspec/tests/test_sanity.py b/test_libs/pyspec/tests/test_sanity.py deleted file mode 100644 index b4d7a8e8b..000000000 --- a/test_libs/pyspec/tests/test_sanity.py +++ /dev/null @@ -1,436 +0,0 @@ -from copy import deepcopy - -import pytest - -from py_ecc import bls -import eth2spec.phase0.spec as spec - -from eth2spec.utils.ssz.ssz_impl 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, - process_slot, - verify_merkle_branch, - state_transition, - hash, -) -from eth2spec.utils.merkle_minimal import ( - calc_merkle_tree_from_leaves, - get_merkle_proof, - get_merkle_root, -) -from .helpers import ( - advance_slot, - 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) - process_slot(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.parent_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.parent_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.parent_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