diff --git a/.circleci/config.yml b/.circleci/config.yml index fd7708f8d..f3c5f6a81 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -52,7 +52,7 @@ jobs: key: v1-specs-repo-{{ .Branch }}-{{ .Revision }} paths: - ~/specs-repo - install_test: + install_env: docker: - image: circleci/python:3.6 working_directory: ~/specs-repo @@ -60,13 +60,13 @@ jobs: - restore_cache: key: v1-specs-repo-{{ .Branch }}-{{ .Revision }} - restore_cached_venv: - venv_name: v1-pyspec + venv_name: v1-pyspec-03 reqs_checksum: '{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}' - run: name: Install pyspec requirements - command: make install_test + command: make install_test && make install_lint - save_cached_venv: - venv_name: v1-pyspec + venv_name: v1-pyspec-03 reqs_checksum: '{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}' venv_path: ./test_libs/pyspec/venv test: @@ -77,7 +77,7 @@ jobs: - restore_cache: key: v1-specs-repo-{{ .Branch }}-{{ .Revision }} - restore_cached_venv: - venv_name: v1-pyspec + venv_name: v1-pyspec-03 reqs_checksum: '{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}' - run: name: Run py-tests @@ -92,20 +92,22 @@ jobs: - restore_cache: key: v1-specs-repo-{{ .Branch }}-{{ .Revision }} - restore_cached_venv: - venv_name: v1-pyspec + venv_name: v1-pyspec-03 reqs_checksum: '{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}' - run: name: Run linter - command: make install_lint && make pyspec && make lint + command: make lint workflows: version: 2.1 test_spec: jobs: - checkout_specs - - lint - - install_test: + - install_env: requires: - checkout_specs - test: requires: - - install_test + - install_env + - lint: + requires: + - test diff --git a/Makefile b/Makefile index 8cc889f21..a6b379b71 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ citest: $(PY_SPEC_ALL_TARGETS) install_lint: cd $(PY_SPEC_DIR); python3 -m venv venv; . venv/bin/activate; pip3 install flake8==3.5.0 -lint: +lint: $(PY_SPEC_ALL_TARGETS) cd $(PY_SPEC_DIR); . venv/bin/activate; \ flake8 --max-line-length=120 ./eth2spec; diff --git a/scripts/phase0/build_spec.py b/scripts/phase0/build_spec.py index 59d9e9f9d..d739ada70 100644 --- a/scripts/phase0/build_spec.py +++ b/scripts/phase0/build_spec.py @@ -5,6 +5,7 @@ import function_puller def build_phase0_spec(sourcefile, outfile): code_lines = [] code_lines.append(""" + from typing import ( Any, Dict, diff --git a/specs/bls_signature.md b/specs/bls_signature.md index d119c4499..3fe1bcc0e 100644 --- a/specs/bls_signature.md +++ b/specs/bls_signature.md @@ -1,6 +1,8 @@ # BLS signature verification -**Warning: This document is pending academic review and should not yet be considered secure.** +**Notice**: This document is a placeholder to facilitate the emergence of cross-client testnets. Substantive changes are postponed until [BLS standardisation](https://github.com/pairingwg/bls_standard) is finalized. + +**Warning**: The constructions in this document should not be considered secure. In particular, the `hash_to_G2` function is known to be unsecure. ## Table of contents diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index cae9dc55d..4e0408938 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -51,6 +51,7 @@ - [`hash`](#hash) - [`hash_tree_root`](#hash_tree_root) - [`signing_root`](#signing_root) + - [`bls_domain`](#bls_domain) - [`slot_to_epoch`](#slot_to_epoch) - [`get_previous_epoch`](#get_previous_epoch) - [`get_current_epoch`](#get_current_epoch) @@ -98,8 +99,7 @@ - [Genesis state](#genesis-state) - [Genesis block](#genesis-block) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - - [State caching](#state-caching) - - [Per-epoch processing](#per-epoch-processing) + - [Epoch processing](#epoch-processing) - [Helper functions](#helper-functions-1) - [Justification and finalization](#justification-and-finalization) - [Crosslinks](#crosslinks) @@ -107,8 +107,7 @@ - [Registry updates](#registry-updates) - [Slashings](#slashings) - [Final updates](#final-updates) - - [Per-slot processing](#per-slot-processing) - - [Per-block processing](#per-block-processing) + - [Block processing](#block-processing) - [Block header](#block-header) - [RANDAO](#randao) - [Eth1 data](#eth1-data) @@ -119,7 +118,6 @@ - [Deposits](#deposits) - [Voluntary exits](#voluntary-exits) - [Transfers](#transfers) - - [State root verification](#state-root-verification) @@ -283,8 +281,9 @@ class Fork(Container): class Crosslink(Container): # Shard number shard: uint64 - # Epoch number - epoch: uint64 + # Crosslinking data from epochs [start....end-1] + start_epoch: uint64 + end_epoch: uint64 # Root of the previous crosslink parent_root: Bytes32 # Root of the crosslinked shard data since the previous crosslink @@ -367,6 +366,7 @@ class BeaconBlockHeader(Container): body_root: Bytes32 signature: Bytes96 ``` + #### `Validator` ```python @@ -560,9 +560,7 @@ class BeaconState(Container): latest_block_roots: Vector[Bytes32, SLOTS_PER_HISTORICAL_ROOT] latest_state_roots: Vector[Bytes32, SLOTS_PER_HISTORICAL_ROOT] latest_active_index_roots: Vector[Bytes32, LATEST_ACTIVE_INDEX_ROOTS_LENGTH] - # Balances slashed at every withdrawal period latest_slashed_balances: Vector[uint64, LATEST_SLASHED_EXIT_LENGTH] - # `latest_block_header.state_root == ZERO_HASH` temporarily latest_block_header: BeaconBlockHeader historical_roots: List[Bytes32] @@ -612,6 +610,16 @@ The `hash` function is SHA256. `def signing_root(object: Container) -> Bytes32` is a function defined in the [SimpleSerialize spec](../simple-serialize.md#self-signed-containers) to compute signing messages. +### `bls_domain` + +```python +def bls_domain(domain_type: int, fork_version: bytes=b'\x00\x00\x00\x00') -> int: + """ + Return the bls domain given by the ``domain_type`` and optional 4 byte ``fork_version`` (defaults to zero). + """ + return bytes_to_int(int_to_bytes(domain_type, length=4) + fork_version) +``` + ### `slot_to_epoch` ```python @@ -631,7 +639,7 @@ def get_previous_epoch(state: BeaconState) -> Epoch: Return the current epoch if it's genesis epoch. """ current_epoch = get_current_epoch(state) - return (current_epoch - 1) if current_epoch > GENESIS_EPOCH else current_epoch + return GENESIS_EPOCH if current_epoch == GENESIS_EPOCH else current_epoch - 1 ``` ### `get_current_epoch` @@ -871,7 +879,7 @@ def get_shuffled_index(index: ValidatorIndex, index_count: int, seed: Bytes32) - # See the 'generalized domain' algorithm on page 3 for round in range(SHUFFLE_ROUND_COUNT): pivot = bytes_to_int(hash(seed + int_to_bytes(round, length=1))[0:8]) % index_count - flip = (pivot - index) % index_count + flip = (pivot + index_count - index) % index_count position = max(index, flip) source = hash(seed + int_to_bytes(round, length=1) + int_to_bytes(position // 256, length=4)) byte = source[(position % 256) // 8] @@ -951,7 +959,7 @@ def get_domain(state: BeaconState, """ epoch = get_current_epoch(state) if message_epoch is None else message_epoch fork_version = state.fork.previous_version if epoch < state.fork.epoch else state.fork.current_version - return bytes_to_int(fork_version + int_to_bytes(domain_type, length=4)) + return bls_domain(domain_type, fork_version) ``` ### `get_bitfield_bit` @@ -1173,7 +1181,11 @@ Let `genesis_state = get_genesis_beacon_state(genesis_deposits, eth2genesis.gene ```python def get_genesis_beacon_state(deposits: List[Deposit], genesis_time: int, genesis_eth1_data: Eth1Data) -> BeaconState: - state = BeaconState(genesis_time=genesis_time, latest_eth1_data=genesis_eth1_data) + state = BeaconState( + genesis_time=genesis_time, + latest_eth1_data=genesis_eth1_data, + latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())), + ) # Process genesis deposits for deposit in deposits: @@ -1199,50 +1211,61 @@ Let `genesis_block = BeaconBlock(state_root=hash_tree_root(genesis_state))`. ## Beacon chain state transition function -We now define the state transition function. At a high level, the state transition is made up of four parts: - -1. State caching, which happens at the start of every slot. -2. The per-epoch transitions, which happens at the start of the first slot of every epoch. -3. The per-slot transitions, which happens at every slot. -4. The per-block transitions, which happens at every block. - -Transition section notes: -* The state caching caches the state root of the previous slot and updates block and state roots records. -* The per-epoch transitions focus on the [validator](#dfn-validator) registry, including adjusting balances and activating and exiting [validators](#dfn-validator), as well as processing crosslinks and managing block justification/finalization. -* The per-slot transitions focus on the slot counter. -* The per-block transitions generally focus on verifying aggregate signatures and saving temporary records relating to the per-block activity in the `BeaconState`. - -Beacon blocks that trigger unhandled Python exceptions (e.g. out-of-range list accesses) and failed `assert`s during the state transition are considered invalid. - -*Note*: If there are skipped slots between a block and its parent block, run the steps in the [state-root](#state-caching), [per-epoch](#per-epoch-processing), and [per-slot](#per-slot-processing) sections once for each skipped slot and then once for the slot containing the new block. - -### State caching - -At every `slot > GENESIS_SLOT` run the following function: +The post-state corresponding to a pre-state `state` and a block `block` is defined as `state_transition(state, block)`. State transitions that trigger an unhandled excpetion (e.g. a failed `assert` or an out-of-range list access) are considered invalid. ```python -def cache_state(state: BeaconState) -> None: - # Cache latest known state root (for previous slot) - latest_state_root = hash_tree_root(state) - state.latest_state_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = latest_state_root - - # Store latest known state root (for previous slot) in latest_block_header if it is empty - if state.latest_block_header.state_root == ZERO_HASH: - state.latest_block_header.state_root = latest_state_root - - # Cache latest known block root (for previous slot) - latest_block_root = signing_root(state.latest_block_header) - state.latest_block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = latest_block_root +def state_transition(state: BeaconState, block: BeaconBlock, validate_state_root: bool=False) -> BeaconState: + # Process slots (including those with no blocks) since block + process_slots(state, block.slot) + # Process block + process_block(state, block) + # Validate state root (`validate_state_root == True` in production) + if validate_state_root: + assert block.state_root == hash_tree_root(state) + # Return post-state + return state ``` -### Per-epoch processing +```python +def process_slots(state: BeaconState, slot: Slot) -> None: + assert state.slot < slot + while state.slot < slot: + process_slot(state) + # Process epoch on the first slot of the next epoch + if (state.slot + 1) % SLOTS_PER_EPOCH == 0: + process_epoch(state) + state.slot += 1 +``` -The steps below happen when `state.slot > GENESIS_SLOT and (state.slot + 1) % SLOTS_PER_EPOCH == 0`. +```python +def process_slot(state: BeaconState) -> None: + # Cache state root + previous_state_root = hash_tree_root(state) + state.latest_state_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_state_root + + # Cache latest block header state root + if state.latest_block_header.state_root == ZERO_HASH: + state.latest_block_header.state_root = previous_state_root + + # Cache block root + previous_block_root = signing_root(state.latest_block_header) + state.latest_block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_block_root +``` + +### Epoch processing + +```python +def process_epoch(state: BeaconState) -> None: + process_justification_and_finalization(state) + process_crosslinks(state) + process_rewards_and_penalties(state) + process_registry_updates(state) + process_slashings(state) + process_final_updates(state) +``` #### Helper functions -We define epoch transition helper functions: - ```python def get_total_active_balance(state: BeaconState) -> Gwei: return get_total_balance(state, get_active_validator_indices(state, get_current_epoch(state))) @@ -1303,8 +1326,6 @@ def get_winning_crosslink_and_attesting_indices(state: BeaconState, #### Justification and finalization -Run the following function: - ```python def process_justification_and_finalization(state: BeaconState) -> None: if get_current_epoch(state) <= GENESIS_EPOCH + 1: @@ -1337,27 +1358,25 @@ def process_justification_and_finalization(state: BeaconState) -> None: # Process finalizations bitfield = state.justification_bitfield # The 2nd/3rd/4th most recent epochs are justified, the 2nd using the 4th as source - if (bitfield >> 1) % 8 == 0b111 and old_previous_justified_epoch == current_epoch - 3: + if (bitfield >> 1) % 8 == 0b111 and old_previous_justified_epoch + 3 == current_epoch: state.finalized_epoch = old_previous_justified_epoch state.finalized_root = get_block_root(state, state.finalized_epoch) # The 2nd/3rd most recent epochs are justified, the 2nd using the 3rd as source - if (bitfield >> 1) % 4 == 0b11 and old_previous_justified_epoch == current_epoch - 2: + if (bitfield >> 1) % 4 == 0b11 and old_previous_justified_epoch + 2 == current_epoch: state.finalized_epoch = old_previous_justified_epoch state.finalized_root = get_block_root(state, state.finalized_epoch) # The 1st/2nd/3rd most recent epochs are justified, the 1st using the 3rd as source - if (bitfield >> 0) % 8 == 0b111 and old_current_justified_epoch == current_epoch - 2: + if (bitfield >> 0) % 8 == 0b111 and old_current_justified_epoch + 2 == current_epoch: state.finalized_epoch = old_current_justified_epoch state.finalized_root = get_block_root(state, state.finalized_epoch) # The 1st/2nd most recent epochs are justified, the 1st using the 2nd as source - if (bitfield >> 0) % 4 == 0b11 and old_current_justified_epoch == current_epoch - 1: + if (bitfield >> 0) % 4 == 0b11 and old_current_justified_epoch + 1 == current_epoch: state.finalized_epoch = old_current_justified_epoch state.finalized_root = get_block_root(state, state.finalized_epoch) ``` #### Crosslinks -Run the following function: - ```python def process_crosslinks(state: BeaconState) -> None: state.previous_crosslinks = [c for c in state.current_crosslinks] @@ -1372,8 +1391,6 @@ def process_crosslinks(state: BeaconState) -> None: #### Rewards and penalties -First, we define additional helpers: - ```python def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: adjusted_quotient = integer_squareroot(get_total_active_balance(state)) // BASE_REWARD_QUOTIENT @@ -1449,8 +1466,6 @@ def get_crosslink_deltas(state: BeaconState) -> Tuple[List[Gwei], List[Gwei]]: return rewards, penalties ``` -Run the following function: - ```python def process_rewards_and_penalties(state: BeaconState) -> None: if get_current_epoch(state) == GENESIS_EPOCH: @@ -1465,8 +1480,6 @@ def process_rewards_and_penalties(state: BeaconState) -> None: #### Registry updates -Run the following function: - ```python def process_registry_updates(state: BeaconState) -> None: # Process activation eligibility and ejections @@ -1495,8 +1508,6 @@ def process_registry_updates(state: BeaconState) -> None: #### Slashings -Run the following function: - ```python def process_slashings(state: BeaconState) -> None: current_epoch = get_current_epoch(state) @@ -1519,8 +1530,6 @@ def process_slashings(state: BeaconState) -> None: #### Final updates -Run the following function: - ```python def process_final_updates(state: BeaconState) -> None: current_epoch = get_current_epoch(state) @@ -1559,19 +1568,16 @@ def process_final_updates(state: BeaconState) -> None: state.current_epoch_attestations = [] ``` -### Per-slot processing - -At every `slot > GENESIS_SLOT` run the following function: +### Block processing ```python -def advance_slot(state: BeaconState) -> None: - state.slot += 1 +def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + process_randao(state, block.body) + process_eth1_data(state, block.body) + process_operations(state, block.body) ``` -### Per-block processing - -For every `block` except the genesis block, run `process_block_header(state, block)`, `process_randao(state, block)` and `process_eth1_data(state, block)`. - #### Block header ```python @@ -1596,44 +1602,57 @@ def process_block_header(state: BeaconState, block: BeaconBlock) -> None: #### RANDAO ```python -def process_randao(state: BeaconState, block: BeaconBlock) -> None: +def process_randao(state: BeaconState, body: BeaconBlockBody) -> None: proposer = state.validator_registry[get_beacon_proposer_index(state)] # Verify that the provided randao value is valid assert bls_verify( proposer.pubkey, hash_tree_root(get_current_epoch(state)), - block.body.randao_reveal, + body.randao_reveal, get_domain(state, DOMAIN_RANDAO), ) # Mix it in state.latest_randao_mixes[get_current_epoch(state) % LATEST_RANDAO_MIXES_LENGTH] = ( xor(get_randao_mix(state, get_current_epoch(state)), - hash(block.body.randao_reveal)) + hash(body.randao_reveal)) ) ``` #### Eth1 data ```python -def process_eth1_data(state: BeaconState, block: BeaconBlock) -> None: - state.eth1_data_votes.append(block.body.eth1_data) - if state.eth1_data_votes.count(block.body.eth1_data) * 2 > SLOTS_PER_ETH1_VOTING_PERIOD: - state.latest_eth1_data = block.body.eth1_data +def process_eth1_data(state: BeaconState, body: BeaconBlockBody) -> None: + state.eth1_data_votes.append(body.eth1_data) + if state.eth1_data_votes.count(body.eth1_data) * 2 > SLOTS_PER_ETH1_VOTING_PERIOD: + state.latest_eth1_data = body.eth1_data ``` #### Operations -*Note*: All functions in this section mutate `state`. +```python +def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # Verify that outstanding deposits are processed up to the maximum number of deposits + assert len(body.deposits) == min(MAX_DEPOSITS, state.latest_eth1_data.deposit_count - state.deposit_index) + # Verify that there are no duplicate transfers + assert len(body.transfers) == len(set(body.transfers)) + + for operations, max_operations, function in ( + (body.proposer_slashings, MAX_PROPOSER_SLASHINGS, process_proposer_slashing), + (body.attester_slashings, MAX_ATTESTER_SLASHINGS, process_attester_slashing), + (body.attestations, MAX_ATTESTATIONS, process_attestation), + (body.deposits, MAX_DEPOSITS, process_deposit), + (body.voluntary_exits, MAX_VOLUNTARY_EXITS, process_voluntary_exit), + (body.transfers, MAX_TRANSFERS, process_transfer), + ): + assert len(operations) <= max_operations + for operation in operations: + function(state, operation) +``` ##### Proposer slashings -Verify that `len(block.body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS`. - -For each `proposer_slashing` in `block.body.proposer_slashings`, run the following function: - ```python -def process_proposer_slashing(state: BeaconState, - proposer_slashing: ProposerSlashing) -> None: +def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: """ Process ``ProposerSlashing`` operation. """ @@ -1654,13 +1673,8 @@ def process_proposer_slashing(state: BeaconState, ##### Attester slashings -Verify that `len(block.body.attester_slashings) <= MAX_ATTESTER_SLASHINGS`. - -For each `attester_slashing` in `block.body.attester_slashings`, run the following function: - ```python -def process_attester_slashing(state: BeaconState, - attester_slashing: AttesterSlashing) -> None: +def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSlashing) -> None: """ Process ``AttesterSlashing`` operation. """ @@ -1673,7 +1687,7 @@ def process_attester_slashing(state: BeaconState, slashed_any = False attesting_indices_1 = attestation_1.custody_bit_0_indices + attestation_1.custody_bit_1_indices attesting_indices_2 = attestation_2.custody_bit_0_indices + attestation_2.custody_bit_1_indices - for index in set(attesting_indices_1).intersection(attesting_indices_2): + for index in sorted(set(attesting_indices_1).intersection(attesting_indices_2)): if is_slashable_validator(state.validator_registry[index], get_current_epoch(state)): slash_validator(state, index) slashed_any = True @@ -1682,10 +1696,6 @@ def process_attester_slashing(state: BeaconState, ##### Attestations -Verify that `len(block.body.attestations) <= MAX_ATTESTATIONS`. - -For each `attestation` in `block.body.attestations`, run the following function: - ```python def process_attestation(state: BeaconState, attestation: Attestation) -> None: """ @@ -1714,7 +1724,8 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: # Check FFG data, crosslink data, and signature assert ffg_data == (data.source_epoch, data.source_root, data.target_epoch) - assert data.crosslink.epoch == min(data.target_epoch, parent_crosslink.epoch + MAX_EPOCHS_PER_CROSSLINK) + assert data.crosslink.start_epoch == parent_crosslink.end_epoch + assert data.crosslink.end_epoch == min(data.target_epoch, parent_crosslink.end_epoch + MAX_EPOCHS_PER_CROSSLINK) assert data.crosslink.parent_root == hash_tree_root(parent_crosslink) assert data.crosslink.data_root == ZERO_HASH # [to be removed in phase 1] validate_indexed_attestation(state, convert_to_indexed(state, attestation)) @@ -1722,10 +1733,6 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: ##### Deposits -Verify that `len(block.body.deposits) == min(MAX_DEPOSITS, state.latest_eth1_data.deposit_count - state.deposit_index)`. - -For each `deposit` in `block.body.deposits`, run the following function: - ```python def process_deposit(state: BeaconState, deposit: Deposit) -> None: """ @@ -1749,8 +1756,9 @@ def process_deposit(state: BeaconState, deposit: Deposit) -> None: validator_pubkeys = [v.pubkey for v in state.validator_registry] if pubkey not in validator_pubkeys: # Verify the deposit signature (proof of possession) + # 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, get_domain(state, DOMAIN_DEPOSIT) + pubkey, signing_root(deposit.data), deposit.data.signature, bls_domain(DOMAIN_DEPOSIT) ): return @@ -1773,10 +1781,6 @@ def process_deposit(state: BeaconState, deposit: Deposit) -> None: ##### Voluntary exits -Verify that `len(block.body.voluntary_exits) <= MAX_VOLUNTARY_EXITS`. - -For each `exit` in `block.body.voluntary_exits`, run the following function: - ```python def process_voluntary_exit(state: BeaconState, exit: VoluntaryExit) -> None: """ @@ -1800,10 +1804,6 @@ def process_voluntary_exit(state: BeaconState, exit: VoluntaryExit) -> None: ##### Transfers -Verify that `len(block.body.transfers) <= MAX_TRANSFERS` and that all transfers are distinct. - -For each `transfer` in `block.body.transfers`, run the following function: - ```python def process_transfer(state: BeaconState, transfer: Transfer) -> None: """ @@ -1834,12 +1834,3 @@ def process_transfer(state: BeaconState, transfer: Transfer) -> None: assert not (0 < state.balances[transfer.sender] < MIN_DEPOSIT_AMOUNT) assert not (0 < state.balances[transfer.recipient] < MIN_DEPOSIT_AMOUNT) ``` - -#### State root verification - -Verify the block's `state_root` by running the following function: - -```python -def verify_block_state_root(state: BeaconState, block: BeaconBlock) -> None: - assert block.state_root == hash_tree_root(state) -``` diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index a030c577b..568879b17 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -35,9 +35,9 @@ - [`empty`](#empty) - [`get_crosslink_chunk_count`](#get_crosslink_chunk_count) - [`get_custody_chunk_bit`](#get_custody_chunk_bit) + - [`get_chunk_bits_root`](#get_chunk_bits_root) - [`get_randao_epoch_for_custody_period`](#get_randao_epoch_for_custody_period) - [`get_validators_custody_reveal_period`](#get_validators_custody_reveal_period) - - [`get_chunk_bits_root`](#get_chunk_bits_root) - [`replace_empty_or_append`](#replace_empty_or_append) - [Per-block processing](#per-block-processing) - [Operations](#operations) @@ -104,6 +104,8 @@ This document details the beacon chain additions and changes in Phase 1 of Ether ### Reward and penalty quotients +| Name | Value | +| - | - | | `EARLY_DERIVED_SECRET_REVEAL_SLOT_REWARD_MULTIPLE` | `2**1` (= 2) | ### Signature domains @@ -146,7 +148,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether 'challenge_index': 'uint64', 'challenger_index': ValidatorIndex, 'responder_index': ValidatorIndex, - 'deadline': Epoch, + 'inclusion_epoch': Epoch, 'data_root': Hash, 'depth': 'uint64', 'chunk_index': 'uint64', @@ -160,7 +162,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether 'challenge_index': 'uint64', 'challenger_index': ValidatorIndex, 'responder_index': ValidatorIndex, - 'deadline': Epoch, + 'inclusion_epoch': Epoch, 'data_root': Hash, 'chunk_count': 'uint64', 'chunk_bits_merkle_root': Hash, @@ -262,12 +264,10 @@ The `empty` function accepts and SSZ type as input and returns an object of that ### `get_crosslink_chunk_count` ```python -def get_custody_chunk_count(attestation: Attestation) -> int: - crosslink_start_epoch = attestation.data.latest_crosslink.epoch - crosslink_end_epoch = slot_to_epoch(attestation.data.slot) - crosslink_crosslink_length = min(MAX_EPOCHS_PER_CROSSLINK, end_epoch - start_epoch) +def get_custody_chunk_count(crosslink: Crosslink) -> int: + crosslink_length = min(MAX_EPOCHS_PER_CROSSLINK, crosslink.end_epoch - crosslink.start_epoch) chunks_per_epoch = 2 * BYTES_PER_SHARD_BLOCK * SLOTS_PER_EPOCH // BYTES_PER_CUSTODY_CHUNK - return crosslink_crosslink_length * chunks_per_epoch + return crosslink_length * chunks_per_epoch ``` ### `get_custody_chunk_bit` @@ -299,7 +299,7 @@ def get_randao_epoch_for_custody_period(period: int, validator_index: ValidatorI ### `get_validators_custody_reveal_period` - ```python +```python def get_validators_custody_reveal_period(state: BeaconState, validator_index: ValidatorIndex, epoch: Epoch=None) -> int: @@ -380,7 +380,7 @@ def process_custody_key_reveal(state: BeaconState, increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) ``` -##### Early derived secret reveals +#### Early derived secret reveals Verify that `len(block.body.early_derived_secret_reveals) <= MAX_EARLY_DERIVED_SECRET_REVEALS`. @@ -478,14 +478,14 @@ def process_chunk_challenge(state: BeaconState, record.chunk_index != challenge.chunk_index ) # Verify depth - depth = math.log2(next_power_of_two(get_custody_chunk_count(challenge.attestation))) + depth = math.log2(next_power_of_two(get_custody_chunk_count(challenge.attestation.data.crosslink))) assert challenge.chunk_index < 2**depth # Add new chunk challenge record new_record = CustodyChunkChallengeRecord( challenge_index=state.custody_challenge_index, challenger_index=get_beacon_proposer_index(state), responder_index=challenge.responder_index - deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE, + inclusion_epoch=get_current_epoch(state), data_root=challenge.attestation.data.crosslink.data_root, depth=depth, chunk_index=challenge.chunk_index, @@ -528,10 +528,9 @@ def process_bit_challenge(state: BeaconState, attesters = get_attesting_indices(state, attestation.data, attestation.aggregation_bitfield) assert challenge.responder_index in attesters - # A validator can be the challenger or responder for at most one challenge at a time + # A validator can be the challenger for at most one challenge at a time for record in state.custody_bit_challenge_records: assert record.challenger_index != challenge.challenger_index - assert record.responder_index != challenge.responder_index # Verify the responder is a valid custody key epoch_to_sign = get_randao_epoch_for_custody_period( @@ -553,7 +552,7 @@ def process_bit_challenge(state: BeaconState, ) # Verify the chunk count - chunk_count = get_custody_chunk_count(challenge.attestation) + chunk_count = get_custody_chunk_count(challenge.attestation.data.crosslink) assert verify_bitfield(challenge.chunk_bits, chunk_count) # Verify the first bit of the hash of the chunk bits does not equal the custody bit custody_bit = get_bitfield_bit(attestation.custody_bitfield, attesters.index(responder_index)) @@ -563,7 +562,7 @@ def process_bit_challenge(state: BeaconState, challenge_index=state.custody_challenge_index, challenger_index=challenge.challenger_index, responder_index=challenge.responder_index, - deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE, + inclusion_epoch=get_current_epoch(state), data_root=challenge.attestation.data.crosslink.data_root, chunk_count=chunk_count, chunk_bits_merkle_root=merkle_root(pad_to_power_of_2((challenge.chunk_bits))), @@ -604,6 +603,8 @@ def process_chunk_challenge_response(state: BeaconState, assert response.chunk_index == challenge.chunk_index # Verify bit challenge data is null assert response.chunk_bits_branch == [] and response.chunk_bits_leaf == ZERO_HASH + # Verify minimum delay + assert get_current_epoch(state) >= challenge.inclusion_epoch + ACTIVATION_EXIT_DELAY # Verify the chunk matches the crosslink data root assert verify_merkle_branch( leaf=hash_tree_root(response.chunk), @@ -626,6 +627,9 @@ def process_bit_challenge_response(state: BeaconState, challenge: CustodyBitChallengeRecord) -> None: # Verify chunk index assert response.chunk_index < challenge.chunk_count + # Verify responder has not been slashed + responder = state.validator_registry[challenge.responder_index] + assert not responder.slashed # Verify the chunk matches the crosslink data root assert verify_merkle_branch( leaf=hash_tree_root(response.chunk), @@ -671,13 +675,13 @@ Run `process_challenge_deadlines(state)` immediately after `process_reveal_deadl ```python def process_challenge_deadlines(state: BeaconState) -> None: for challenge in state.custody_chunk_challenge_records: - if get_current_epoch(state) > challenge.deadline: + if get_current_epoch(state) > challenge.inclusion_epoch + CUSTODY_RESPONSE_DEADLINE: slash_validator(state, challenge.responder_index, challenge.challenger_index) records = state.custody_chunk_challenge_records records[records.index(challenge)] = CustodyChunkChallengeRecord() for challenge in state.custody_bit_challenge_records: - if get_current_epoch(state) > challenge.deadline: + if get_current_epoch(state) > challenge.inclusion_epoch + CUSTODY_RESPONSE_DEADLINE: slash_validator(state, challenge.responder_index, challenge.challenger_index) records = state.custody_bit_challenge_records records[records.index(challenge)] = CustodyBitChallengeRecord() @@ -688,26 +692,13 @@ Append this to `process_final_updates(state)`: ```python # Clean up exposed RANDAO key reveals state.exposed_derived_secrets[current_epoch % EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS] = [] -``` - -In `process_penalties_and_exits`, change the definition of `eligible` to the following (note that it is not a pure function because `state` is declared in the surrounding scope): - -```python -def eligible(state: BeaconState, index: ValidatorIndex) -> bool: - validator = state.validator_registry[index] - # Cannot exit if there are still open chunk challenges - if len([record for record in state.custody_chunk_challenge_records if record.responder_index == index]) > 0: - return False - # Cannot exit if there are still open bit challenges - if len([record for record in state.custody_bit_challenge_records if record.responder_index == index]) > 0: - return False - # Cannot exit if you have not revealed all of your custody keys - elif validator.next_custody_reveal_period <= get_validators_custody_reveal_period(state, index, validator.exit_epoch): - return False - # Cannot exit if you already have - elif validator.withdrawable_epoch < FAR_FUTURE_EPOCH: - return False - # Return minimum time - else: - return current_epoch >= validator.exit_epoch + MIN_VALIDATOR_WITHDRAWAL_EPOCHS + # Reset withdrawable epochs if challenge records are empty + records = state.custody_chunk_challenge_records + state.bit_challenge_records + validator_indices_in_records = set( + [record.challenger_index for record in records] + [record.responder_index for record in records] + ) + for index, validator in enumerate(state.validator_registry): + if index not in validator_indices_in_records: + if validator.exit_epoch != FAR_FUTURE_EPOCH and validator.withdrawable_epoch == FAR_FUTURE_EPOCH: + validator.withdrawable_epoch = validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY ``` diff --git a/specs/core/1_shard-data-chains.md b/specs/core/1_shard-data-chains.md index e1092b404..1d1186247 100644 --- a/specs/core/1_shard-data-chains.md +++ b/specs/core/1_shard-data-chains.md @@ -215,7 +215,7 @@ def get_shard_header(block: ShardBlock) -> ShardBlockHeader: def verify_shard_attestation_signature(state: BeaconState, attestation: ShardAttestation) -> None: data = attestation.data - persistent_committee = get_persistent_committee(state, data.crosslink.shard, data.slot) + persistent_committee = get_persistent_committee(state, data.shard, data.slot) assert verify_bitfield(attestation.aggregation_bitfield, len(persistent_committee)) pubkeys = [] for i, index in enumerate(persistent_committee): @@ -225,7 +225,7 @@ def verify_shard_attestation_signature(state: BeaconState, pubkeys.append(validator.pubkey) assert bls_verify( pubkey=bls_aggregate_pubkeys(pubkeys), - message_hash=data.crosslink.shard_block_root, + message_hash=data.shard_block_root, signature=attestation.aggregate_signature, domain=get_domain(state, slot_to_epoch(data.slot), DOMAIN_SHARD_ATTESTER) ) @@ -280,22 +280,22 @@ def is_valid_shard_block(beacon_blocks: List[BeaconBlock], return True # Check slot number - assert block.slot >= PHASE_1_GENESIS_SLOT + assert candidate.slot >= PHASE_1_GENESIS_SLOT assert unix_time >= beacon_state.genesis_time + (block.slot - GENESIS_SLOT) * SECONDS_PER_SLOT # Check shard number - assert block.shard <= SHARD_COUNT + assert candidate.shard <= SHARD_COUNT # Check beacon block - beacon_block = beacon_blocks[block.slot] - assert block.beacon_block_root == signing_root(beacon_block) - assert beacon_block.slot <= block.slot: + beacon_block = beacon_blocks[candidate.slot] + assert candidate.beacon_block_root == signing_root(beacon_block) + assert beacon_block.slot <= candidate.slot: # Check state root - assert block.state_root == ZERO_HASH # [to be removed in phase 2] + assert candidate.state_root == ZERO_HASH # [to be removed in phase 2] # Check parent block - if block.slot == PHASE_1_GENESIS_SLOT: + if candidate.slot == PHASE_1_GENESIS_SLOT: assert candidate.parent_root == ZERO_HASH else: parent_block = next( @@ -303,26 +303,26 @@ def is_valid_shard_block(beacon_blocks: List[BeaconBlock], signing_root(block) == candidate.parent_root , None) assert parent_block != None - assert parent_block.shard == block.shard - assert parent_block.slot < block.slot + assert parent_block.shard == candidate.shard + assert parent_block.slot < candidate.slot assert signing_root(beacon_blocks[parent_block.slot]) == parent_block.beacon_chain_root # Check attestations - assert len(block.attestations) <= MAX_SHARD_ATTESTIONS - for _, attestation in enumerate(block.attestations): - assert max(GENESIS_SHARD_SLOT, block.slot - SLOTS_PER_EPOCH) <= attestation.data.slot - assert attestation.data.slot <= block.slot - MIN_ATTESTATION_INCLUSION_DELAY - assert attestation.data.crosslink.shard == block.shard + assert len(candidate.attestations) <= MAX_SHARD_ATTESTIONS + for _, attestation in enumerate(candidate.attestations): + assert max(GENESIS_SHARD_SLOT, candidate.slot - SLOTS_PER_EPOCH) <= attestation.data.slot + assert attestation.data.slot <= candidate.slot - MIN_ATTESTATION_INCLUSION_DELAY + assert attestation.data.crosslink.shard == candidate.shard verify_shard_attestation_signature(beacon_state, attestation) # Check signature - proposer_index = get_shard_proposer_index(beacon_state, block.shard, block.slot) + proposer_index = get_shard_proposer_index(beacon_state, candidate.shard, candidate.slot) assert proposer_index is not None assert bls_verify( pubkey=validators[proposer_index].pubkey, message_hash=signing_root(block), - signature=block.signature, - domain=get_domain(beacon_state, slot_to_epoch(block.slot), DOMAIN_SHARD_PROPOSER) + signature=candidate.signature, + domain=get_domain(beacon_state, slot_to_epoch(candidate.slot), DOMAIN_SHARD_PROPOSER) ) return True @@ -339,18 +339,18 @@ Let: ```python def is_valid_shard_attestation(valid_shard_blocks: List[ShardBlock], beacon_state: BeaconState, - candidate: Attestation) -> bool: + candidate: ShardAttestation) -> bool: # Check shard block shard_block = next( block for block in valid_shard_blocks if - signing_root(block) == candidate.attestation.data.crosslink.shard_block_root + signing_root(block) == candidate.data.shard_block_root , None) assert shard_block != None - assert shard_block.slot == attestation.data.slot - assert shard_block.shard == attestation.data.crosslink.shard + assert shard_block.slot == candidate.data.slot + assert shard_block.shard == candidate.data.shard # Check signature - verify_shard_attestation_signature(beacon_state, attestation) + verify_shard_attestation_signature(beacon_state, candidate) return True ``` diff --git a/specs/networking/libp2p-standardization.md b/specs/networking/libp2p-standardization.md new file mode 100644 index 000000000..d1ba07e65 --- /dev/null +++ b/specs/networking/libp2p-standardization.md @@ -0,0 +1,158 @@ +ETH 2.0 Networking Spec - Libp2p standard protocols +=== + +# Abstract + +Ethereum 2.0 clients plan to use the libp2p protocol networking stack for +mainnet release. This document aims to standardize the libp2p client protocols, +configuration and messaging formats. + +# Libp2p Components + +## Transport + +This section details the libp2p transport layer that underlies the +[protocols](#protocols) that are listed in this document. + +Libp2p allows composition of multiple transports. Eth2.0 clients should support +TCP/IP and optionally websockets. Websockets are useful for implementations +running in the browser and therefore native clients would ideally support these implementations +by supporting websockets. + +An ideal libp2p transport would therefore support both TCP/IP and websockets. + +*Note: There is active development in libp2p to facilitate the +[QUIC](https://github.com/libp2p/go-libp2p-quic-transport) transport, which may +be adopted in the future* + +### Encryption + +Libp2p currently offers [Secio](https://github.com/libp2p/specs/pull/106) which +can upgrade a transport which will then encrypt all future communication. Secio +generates a symmetric ephemeral key which peers use to encrypt their +communication. It can support a range of ciphers and currently supports key +derivation for elliptic curve-based public keys. + +Current defaults are: +- Key agreement: `ECDH-P256` (also supports `ECDH-P384`) +- Cipher: `AES-128` (also supports `AES-256`, `TwofishCTR`) +- Digests: `SHA256` (also supports `SHA512`) + +*Note: Secio is being deprecated in favour of [TLS +1.3](https://github.com/libp2p/specs/blob/master/tls/tls.md). It is our +intention to transition to use TLS 1.3 for encryption between nodes, rather +than Secio.* + + +## Protocols + +This section lists the necessary libp2p protocols required by Ethereum 2.0 +running a libp2p network stack. + +## Multistream-select + +#### Protocol id: `/multistream/1.0.0` + +Clients running libp2p should support the +[multistream-select](https://github.com/multiformats/multistream-select/) +protocol which allows clients to negotiate libp2p protocols establish streams +per protocol. + +## Multiplexing + +Libp2p allows clients to compose multiple multiplexing methods. Clients should +support [mplex](https://github.com/libp2p/specs/tree/master/mplex) and +optionally [yamux](https://github.com/hashicorp/yamux/blob/master/spec.md) +(these can be composed). + +**Mplex protocol id: `/mplex/6.7.0`** + +**Yamux protocol id: `/yamux/1.0.0`** + +## Gossipsub + +#### Protocol id: `/eth/serenity/gossipsub/1.0.0` + +*Note: Parameters listed here are subject to a large-scale network feasibility +study* + +The [Gossipsub](https://github.com/libp2p/specs/tree/master/pubsub/gossipsub) +protocol is used for block and attestation propagation across the +network. + +### Configuration Parameters + +Gossipsub has a number of internal configuration parameters which directly +effect the network performance. Clients can implement independently, however +we aim to standardize these across clients to optimize the gossip network for +propagation times and message duplication. Current network-related defaults are: + +``` +( + // The target number of peers in the overlay mesh network (D in the libp2p specs). + mesh_size: 6 + // The minimum number of peers in the mesh network before adding more (D_lo in the libp2p specs). + mesh_lo: 4 + // The maximum number of peers in the mesh network before removing some (D_high in the libp2p sepcs). + mesh_high: 12 + // The number of peers to gossip to during a heartbeat (D_lazy in the libp2p sepcs). + gossip_lazy: 6 // defaults to `mesh_size` + // Time to live for fanout peers (seconds). + fanout_ttl: 60 + // The number of heartbeats to gossip about. + gossip_history: 3 + // Time between each heartbeat (seconds). + heartbeat_interval: 1 +) +``` + +### Topics + +*The Go and Js implementations use string topics - This is likely to be +updated to topic hashes in later versions - https://github.com/libp2p/rust-libp2p/issues/473* + +For Eth2.0 clients, topics are sent as `SHA2-256` hashes of the topic string. + +There are two main topics used to propagate attestations and beacon blocks to +all nodes on the network. + +- The `beacon_block` topic - This topic is used solely for propagating new + beacon blocks to all nodes on the networks. +- The `beacon_attestation` topic - This topic is used to propagate + aggregated attestations to subscribing nodes (typically block proposers) to + be included into future blocks. Attestations are aggregated in their + respective subnets before publishing on this topic. + +Shards are grouped into their own subnets (defined by a shard topic). The +number of shard subnets is defined via `SHARD_SUBNET_COUNT` and the shard +`shard_number % SHARD_SUBNET_COUNT` is assigned to the topic: +`shard{shard_number % SHARD_SUBNET_COUNT}_attestation`. + +### Messages + +*Note: The message format here is Eth2.0-specific* + +Each Gossipsub +[Message](https://github.com/libp2p/go-libp2p-pubsub/blob/master/pb/rpc.proto#L17-L24) +has a maximum size of 512KB (estimated from expected largest uncompressed block +size). + +The `data` field of a Gossipsub `Message` is an SSZ-encoded object. For the `beacon_block` topic, +this is a `beacon_block`. For the `beacon_attestation` topic, this is +an `attestation`. + +## Eth-2 RPC + +#### Protocol Id: `/eth/serenity/beacon/rpc/1` + +The [RPC Interface](./rpc-interface.md) is specified in this repository. + +## Discovery + +Discovery Version 5 +([discv5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md)) +will be used for discovery. This protocol uses a UDP transport and specifies +its own encryption, ip-discovery and topic advertisement. Therefore, it has no +need to establish streams through `multistream-select`, rather, act +as a standalone implementation that feeds discovered peers/topics (ENR-records) as +`multiaddrs` into the libp2p service. diff --git a/specs/networking/rpc-interface.md b/specs/networking/rpc-interface.md index ab7a08392..b81f78408 100644 --- a/specs/networking/rpc-interface.md +++ b/specs/networking/rpc-interface.md @@ -41,7 +41,7 @@ Remote method calls are wrapped in a "request" structure: ( id: uint64 method_id: uint16 - body: Request + body: (message_body...) ) ``` @@ -55,15 +55,7 @@ and their corresponding responses are wrapped in a "response" structure: ) ``` -If an error occurs, a variant of the response structure is returned: - -``` -( - id: uint64 - response_code: uint16 - result: bytes -) -``` +A union type is used to determine the contents of the `body` field in the request structure. Each "body" entry in the RPC calls below corresponds to one subtype in the `body` type union. The details of the RPC-Over-`libp2p` protocol are similar to [JSON-RPC 2.0](https://www.jsonrpc.org/specification). Specifically: diff --git a/specs/validator/0_beacon-chain-validator.md b/specs/validator/0_beacon-chain-validator.md index 49290b432..f8272d446 100644 --- a/specs/validator/0_beacon-chain-validator.md +++ b/specs/validator/0_beacon-chain-validator.md @@ -20,6 +20,8 @@ - [Process deposit](#process-deposit) - [Validator index](#validator-index) - [Activation](#activation) + - [Validator assignments](#validator-assignments) + - [Lookahead](#lookahead) - [Beacon chain responsibilities](#beacon-chain-responsibilities) - [Block proposal](#block-proposal) - [Block header](#block-header) @@ -45,8 +47,6 @@ - [Aggregation bitfield](#aggregation-bitfield) - [Custody bitfield](#custody-bitfield) - [Aggregate signature](#aggregate-signature) - - [Validator assignments](#validator-assignments) - - [Lookahead](#lookahead) - [How to avoid slashing](#how-to-avoid-slashing) - [Proposer slashing](#proposer-slashing) - [Attester slashing](#attester-slashing) @@ -98,7 +98,7 @@ To submit a deposit: * Pack the validator's [initialization parameters](#initialization) into `deposit_data`, a [`DepositData`](../core/0_beacon-chain.md#depositdata) SSZ object. * Let `amount` be the amount in Gwei to be deposited by the validator where `MIN_DEPOSIT_AMOUNT <= amount <= MAX_EFFECTIVE_BALANCE`. * Set `deposit_data.amount = amount`. -* Let `signature` be the result of `bls_sign` of the `signing_root(deposit_data)` with `domain=DOMAIN_DEPOSIT`. +* Let `signature` be the result of `bls_sign` of the `signing_root(deposit_data)` with `domain=bls_domain(DOMAIN_DEPOSIT)`. (Deposits are valid regardless of fork version, `bls_domain` will default to zeroes there). * Send a transaction on the Ethereum 1.0 chain to `DEPOSIT_CONTRACT_ADDRESS` executing `def deposit(pubkey: bytes[48], withdrawal_credentials: bytes[32], signature: bytes[96])` along with a deposit of `amount` Gwei. *Note*: Deposits made for the same `pubkey` are treated as for the same validator. A singular `Validator` will be added to `state.validator_registry` with each additional deposit amount added to the validator's balance. A validator can only be activated when total deposits for the validator pubkey meet or exceed `MAX_EFFECTIVE_BALANCE`. @@ -127,13 +127,62 @@ Once a validator is activated, the validator is assigned [responsibilities](#bea *Note*: There is a maximum validator churn per finalized epoch so the delay until activation is variable depending upon finality, total active validator balance, and the number of validators in the queue to be activated. +## Validator assignments + +A validator can get committee assignments for a given epoch using the following helper via `get_committee_assignment(state, epoch, validator_index)` where `epoch <= next_epoch`. + +```python +def get_committee_assignment( + state: BeaconState, + epoch: Epoch, + validator_index: ValidatorIndex) -> Tuple[List[ValidatorIndex], Shard, Slot]: + """ + Return the committee assignment in the ``epoch`` for ``validator_index``. + ``assignment`` returned is a tuple of the following form: + * ``assignment[0]`` is the list of validators in the committee + * ``assignment[1]`` is the shard to which the committee is assigned + * ``assignment[2]`` is the slot at which the committee is assigned + """ + next_epoch = get_current_epoch(state) + 1 + assert epoch <= next_epoch + + committees_per_slot = get_epoch_committee_count(state, epoch) // SLOTS_PER_EPOCH + epoch_start_slot = get_epoch_start_slot(epoch) + for slot in range(epoch_start_slot, epoch_start_slot + SLOTS_PER_EPOCH) + offset = committees_per_slot * (slot % SLOTS_PER_EPOCH) + slot_start_shard = (get_epoch_start_shard(state, epoch) + offset) % SHARD_COUNT + for i in range(committees_per_slot): + shard = (slot_start_shard + i) % SHARD_COUNT + committee = get_crosslink_committee(state, epoch, shard) + if validator_index in committee: + return committee, shard, slot +``` + +A validator can use the following function to see if they are supposed to propose during their assigned committee slot. This function can only be run with a `state` of the slot in question. Proposer selection is only stable within the context of the current epoch. + +```python +def is_proposer(state: BeaconState, + validator_index: ValidatorIndex) -> bool: + return get_beacon_proposer_index(state) == validator_index +``` + +*Note*: To see if a validator is assigned to propose during the slot, the beacon state must be in the epoch in question. At the epoch boundaries, the validator must run an epoch transition into the epoch to successfully check the proposal assignment of the first slot. + +### Lookahead + +The beacon chain shufflings are designed to provide a minimum of 1 epoch lookahead on the validator's upcoming committee assignments for attesting dictated by the shuffling and slot. Note that this lookahead does not apply to proposing, which must be checked during the epoch in question. + +`get_committee_assignment` should be called at the start of each epoch to get the assignment for the next epoch (`current_epoch + 1`). A validator should plan for future assignments by noting at which future slot they will have to attest and also which shard they should begin syncing (in Phase 1+). + +Specifically, a validator should call `get_committee_assignment(state, next_epoch, validator_index)` when checking for next epoch assignments. + ## Beacon chain responsibilities A validator has two primary responsibilities to the beacon chain: [proposing blocks](#block-proposal) and [creating attestations](#attestations-1). Proposals happen infrequently, whereas attestations should be created once per epoch. ### Block proposal -A validator is expected to propose a [`BeaconBlock`](../core/0_beacon-chain.md#beaconblock) at the beginning of any slot during which `get_beacon_proposer_index(state)` returns the validator's `validator_index`. To propose, the validator selects the `BeaconBlock`, `parent`, that in their view of the fork choice is the head of the chain during `slot - 1`. The validator is to create, sign, and broadcast a `block` that is a child of `parent` and that executes a valid [beacon chain state transition](../core/0_beacon-chain.md#beacon-chain-state-transition-function). +A validator is expected to propose a [`BeaconBlock`](../core/0_beacon-chain.md#beaconblock) at the beginning of any slot during which `is_proposer(state, validator_index)` returns `True`. To propose, the validator selects the `BeaconBlock`, `parent`, that in their view of the fork choice is the head of the chain during `slot - 1`. The validator creates, signs, and broadcasts a `block` that is a child of `parent` that satisfies a valid [beacon chain state transition](../core/0_beacon-chain.md#beacon-chain-state-transition-function). There is one proposer per slot, so if there are N active validators any individual validator will on average be assigned to propose once per N slots (e.g. at 312500 validators = 10 million ETH, that's once per ~3 weeks). @@ -229,7 +278,7 @@ Up to `MAX_VOLUNTARY_EXITS` [`VoluntaryExit`](../core/0_beacon-chain.md#voluntar ### Attestations -A validator is expected to create, sign, and broadcast an attestation during each epoch. The slot during which the validator performs this role is any slot at which `get_crosslink_committees_at_slot(state, slot)` contains a committee that contains `validator_index`. +A validator is expected to create, sign, and broadcast an attestation during each epoch. The committee, assigned shard, and assigned slot for which the validator performs this role during an epoch is defined by `get_committee_assignment(state, epoch, validator_index)`. A validator should create and broadcast the attestation halfway through the `slot` during which the validator is assigned ― that is, `SECONDS_PER_SLOT * 0.5` seconds after the start of `slot`. @@ -238,7 +287,7 @@ A validator should create and broadcast the attestation halfway through the `slo First the validator should construct `attestation_data`, an [`AttestationData`](../core/0_beacon-chain.md#attestationdata) object based upon the state at the assigned slot. * Let `head_block` be the result of running the fork choice during the assigned slot. -* Let `head_state` be the state of `head_block` processed through any empty slots up to the assigned slot. +* Let `head_state` be the state of `head_block` processed through any empty slots up to the assigned slot using `process_slots(state, slot)`. ##### LMD GHOST vote @@ -257,10 +306,12 @@ Set `attestation_data.beacon_block_root = signing_root(head_block)`. ##### Crosslink vote -Construct `attestation_data.crosslink` via the following +Construct `attestation_data.crosslink` via the following. -* Set `attestation_data.crosslink.shard = shard` where `shard` is the shard associated with the validator's committee defined by `get_crosslink_committees_at_slot`. -* Set `attestation_data.crosslink.epoch = min(attestation_data.target_epoch, head_state.current_crosslinks[shard].epoch + MAX_EPOCHS_PER_CROSSLINK)`. +* Set `attestation_data.crosslink.shard = shard` where `shard` is the shard associated with the validator's committee. +* Let `parent_crosslink = head_state.current_crosslinks[shard]`. +* Set `attestation_data.crosslink.start_epoch = parent_crosslink.end_epoch`. +* Set `attestation_data.crosslink.end_epoch = min(attestation_data.target_epoch, parent_crosslink.end_epoch + MAX_EPOCHS_PER_CROSSLINK)`. * Set `attestation_data.crosslink.parent_root = hash_tree_root(head_state.current_crosslinks[shard])`. * Set `attestation_data.crosslink.data_root = ZERO_HASH`. *Note*: This is a stub for Phase 0. @@ -310,67 +361,6 @@ signed_attestation_data = bls_sign( ) ``` -## Validator assignments - -A validator can get the current, previous, and next epoch committee assignments using the following helper via `get_committee_assignment(state, epoch, validator_index)` where `previous_epoch <= epoch <= next_epoch`. - -```python -def get_committee_assignment( - state: BeaconState, - epoch: Epoch, - validator_index: ValidatorIndex) -> Tuple[List[ValidatorIndex], Shard, Slot]: - """ - Return the committee assignment in the ``epoch`` for ``validator_index``. - ``assignment`` returned is a tuple of the following form: - * ``assignment[0]`` is the list of validators in the committee - * ``assignment[1]`` is the shard to which the committee is assigned - * ``assignment[2]`` is the slot at which the committee is assigned - """ - previous_epoch = get_previous_epoch(state) - next_epoch = get_current_epoch(state) + 1 - assert previous_epoch <= epoch <= next_epoch - - epoch_start_slot = get_epoch_start_slot(epoch) - for slot in range(epoch_start_slot, epoch_start_slot + SLOTS_PER_EPOCH): - crosslink_committees = get_crosslink_committees_at_slot( - state, - slot, - ) - selected_committees = [ - committee # Tuple[List[ValidatorIndex], Shard] - for committee in crosslink_committees - if validator_index in committee[0] - ] - if len(selected_committees) > 0: - validators = selected_committees[0][0] - shard = selected_committees[0][1] - - assignment = (validators, shard, slot) - return assignment -``` - -A validator can use the following function to see if they are supposed to propose during their assigned committee slot. This function can only be run during the slot in question. Proposer selection is only stable within the context of the current epoch. - -```python -def is_proposer_at_slot(state: BeaconState, - slot: Slot, - validator_index: ValidatorIndex) -> bool: - assert state.slot == slot - - return get_beacon_proposer_index(state) == validator_index -``` - -*Note*: To see if a validator is assigned to proposer during the slot, the validator must run an empty slot transition from the previous state to the current slot. - - -### Lookahead - -The beacon chain shufflings are designed to provide a minimum of 1 epoch lookahead on the validator's upcoming committee assignments for attesting dictated by the shuffling and slot. Note that this lookahead does not apply to proposing, which must checked during the slot in question. - -`get_committee_assignment` should be called at the start of each epoch to get the assignment for the next epoch (`current_epoch + 1`). A validator should plan for future assignments which involves noting at which future slot one will have to attest and also which shard one should begin syncing (in Phase 1+). - -Specifically, a validator should call `get_committee_assignment(state, next_epoch, validator_index)` when checking for next epoch assignments. - ## How to avoid slashing "Slashing" is the burning of some amount of validator funds and immediate ejection from the active validator set. In Phase 0, there are two ways in which funds can be slashed -- [proposer slashing](#proposer-slashing) and [attester slashing](#attester-slashing). Although being slashed has serious repercussions, it is simple enough to avoid being slashed all together by remaining _consistent_ with respect to the messages a validator has previously signed. diff --git a/test_generators/bls/requirements.txt b/test_generators/bls/requirements.txt index 8a933d41c..5eebde29f 100644 --- a/test_generators/bls/requirements.txt +++ b/test_generators/bls/requirements.txt @@ -1,3 +1,3 @@ -py-ecc==1.6.0 +py-ecc==1.7.0 eth-utils==1.4.1 ../../test_libs/gen_helpers diff --git a/test_libs/pyspec/eth2spec/phase0/state_transition.py b/test_libs/pyspec/eth2spec/phase0/state_transition.py deleted file mode 100644 index 1bef358d4..000000000 --- a/test_libs/pyspec/eth2spec/phase0/state_transition.py +++ /dev/null @@ -1,112 +0,0 @@ -from . import spec - - -from typing import ( - Any, - Callable, - List -) - -from .spec import ( - BeaconState, - BeaconBlock, - Slot, -) - - -def expected_deposit_count(state: BeaconState) -> int: - return min( - spec.MAX_DEPOSITS, - state.latest_eth1_data.deposit_count - state.deposit_index - ) - - -def process_operation_type(state: BeaconState, - operations: List[Any], - max_operations: int, - tx_fn: Callable[[BeaconState, Any], None]) -> None: - assert len(operations) <= max_operations - for operation in operations: - tx_fn(state, operation) - - -def process_operations(state: BeaconState, block: BeaconBlock) -> None: - process_operation_type( - state, - block.body.proposer_slashings, - spec.MAX_PROPOSER_SLASHINGS, - spec.process_proposer_slashing, - ) - - process_operation_type( - state, - block.body.attester_slashings, - spec.MAX_ATTESTER_SLASHINGS, - spec.process_attester_slashing, - ) - - process_operation_type( - state, - block.body.attestations, - spec.MAX_ATTESTATIONS, - spec.process_attestation, - ) - - assert len(block.body.deposits) == expected_deposit_count(state) - process_operation_type( - state, - block.body.deposits, - spec.MAX_DEPOSITS, - spec.process_deposit, - ) - - process_operation_type( - state, - block.body.voluntary_exits, - spec.MAX_VOLUNTARY_EXITS, - spec.process_voluntary_exit, - ) - - assert len(block.body.transfers) == len(set(block.body.transfers)) - process_operation_type( - state, - block.body.transfers, - spec.MAX_TRANSFERS, - spec.process_transfer, - ) - - -def process_block(state: BeaconState, - block: BeaconBlock, - verify_state_root: bool=False) -> None: - spec.process_block_header(state, block) - spec.process_randao(state, block) - spec.process_eth1_data(state, block) - - process_operations(state, block) - if verify_state_root: - spec.verify_block_state_root(state, block) - - -def process_epoch_transition(state: BeaconState) -> None: - spec.process_justification_and_finalization(state) - spec.process_crosslinks(state) - spec.process_rewards_and_penalties(state) - spec.process_registry_updates(state) - spec.process_slashings(state) - spec.process_final_updates(state) - - -def state_transition_to(state: BeaconState, up_to: Slot) -> BeaconState: - while state.slot < up_to: - spec.cache_state(state) - if (state.slot + 1) % spec.SLOTS_PER_EPOCH == 0: - process_epoch_transition(state) - spec.advance_slot(state) - - -def state_transition(state: BeaconState, - block: BeaconBlock, - verify_state_root: bool=False) -> BeaconState: - state_transition_to(state, block.slot) - process_block(state, block, verify_state_root) diff --git a/test_libs/pyspec/eth2spec/utils/bls_stub.py b/test_libs/pyspec/eth2spec/utils/bls_stub.py index 108c4ef71..ae97de175 100644 --- a/test_libs/pyspec/eth2spec/utils/bls_stub.py +++ b/test_libs/pyspec/eth2spec/utils/bls_stub.py @@ -9,4 +9,4 @@ def bls_verify_multiple(pubkeys, message_hashes, signature, domain): def bls_aggregate_pubkeys(pubkeys): - return b'\x42' * 96 + return b'\x42' * 48 diff --git a/test_libs/pyspec/tests/block_processing/test_process_attestation.py b/test_libs/pyspec/tests/block_processing/test_process_attestation.py index 9cf08bae2..763178717 100644 --- a/test_libs/pyspec/tests/block_processing/test_process_attestation.py +++ b/test_libs/pyspec/tests/block_processing/test_process_attestation.py @@ -3,13 +3,11 @@ import pytest import eth2spec.phase0.spec as spec -from eth2spec.phase0.state_transition import ( - state_transition, -) from eth2spec.phase0.spec import ( get_current_epoch, process_attestation, slot_to_epoch, + state_transition, ) from tests.helpers import ( build_empty_block_for_next_slot, @@ -66,6 +64,22 @@ def test_success_prevous_epoch(state): 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 @@ -126,7 +140,33 @@ def test_bad_previous_crosslink(state): for _ in range(spec.MIN_ATTESTATION_INCLUSION_DELAY): next_slot(state) - state.current_crosslinks[attestation.data.crosslink.shard].epoch += 10 + 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) 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 index 36c729821..32b409e5a 100644 --- a/test_libs/pyspec/tests/block_processing/test_process_block_header.py +++ b/test_libs/pyspec/tests/block_processing/test_process_block_header.py @@ -4,11 +4,11 @@ import pytest from eth2spec.phase0.spec import ( get_beacon_proposer_index, - cache_state, - advance_slot, + process_slot, process_block_header, ) from tests.helpers import ( + advance_slot, build_empty_block_for_next_slot, next_slot, ) @@ -18,7 +18,7 @@ pytestmark = pytest.mark.header def prepare_state_for_header_processing(state): - cache_state(state) + process_slot(state) advance_slot(state) diff --git a/test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py b/test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py index 29e7347b1..347a507ef 100644 --- a/test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py +++ b/test_libs/pyspec/tests/epoch_processing/test_process_crosslinks.py @@ -3,13 +3,11 @@ import pytest import eth2spec.phase0.spec as spec -from eth2spec.phase0.state_transition import ( - state_transition, -) from eth2spec.phase0.spec import ( - cache_state, + process_slot, get_crosslink_deltas, process_crosslinks, + state_transition, ) from tests.helpers import ( add_attestation_to_state, @@ -35,7 +33,7 @@ def run_process_crosslinks(state, valid=True): state_transition(state, block) # cache state before epoch transition - cache_state(state) + process_slot(state) post_state = deepcopy(state) process_crosslinks(post_state) diff --git a/test_libs/pyspec/tests/helpers.py b/test_libs/pyspec/tests/helpers.py index 202488576..a4849bfbb 100644 --- a/test_libs/pyspec/tests/helpers.py +++ b/test_libs/pyspec/tests/helpers.py @@ -2,9 +2,6 @@ from copy import deepcopy from py_ecc import bls -from eth2spec.phase0.state_transition import ( - state_transition, -) import eth2spec.phase0.spec as spec from eth2spec.utils.minimal_ssz import signing_root from eth2spec.phase0.spec import ( @@ -27,6 +24,7 @@ from eth2spec.phase0.spec import ( VoluntaryExit, # functions convert_to_indexed, + bls_domain, get_active_validator_indices, get_attesting_indices, get_block_root, @@ -40,6 +38,7 @@ from eth2spec.phase0.spec import ( get_shard_delta, hash_tree_root, slot_to_epoch, + state_transition, verify_merkle_branch, hash, ) @@ -55,6 +54,10 @@ 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] @@ -142,10 +145,7 @@ def build_deposit_data(state, pubkey, privkey, amount): signature = bls.sign( message_hash=signing_root(deposit_data), privkey=privkey, - domain=get_domain( - state, - spec.DOMAIN_DEPOSIT, - ) + domain=bls_domain(spec.DOMAIN_DEPOSIT), ) deposit_data.signature = signature return deposit_data @@ -175,6 +175,7 @@ def build_attestation_data(state, slot, shard): 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, @@ -183,9 +184,10 @@ def build_attestation_data(state, slot, shard): target_root=epoch_boundary_root, crosslink=Crosslink( shard=shard, - epoch=min(slot_to_epoch(slot), crosslinks[shard].epoch + MAX_EPOCHS_PER_CROSSLINK), + 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(crosslinks[shard]), + parent_root=hash_tree_root(parent_crosslink), ), ) diff --git a/test_libs/pyspec/tests/test_finality.py b/test_libs/pyspec/tests/test_finality.py index ca048c2b2..816dfd6bd 100644 --- a/test_libs/pyspec/tests/test_finality.py +++ b/test_libs/pyspec/tests/test_finality.py @@ -4,9 +4,6 @@ import pytest import eth2spec.phase0.spec as spec -from eth2spec.phase0.state_transition import ( - state_transition, -) from .helpers import ( build_empty_block_for_next_slot, fill_aggregate_attestation, @@ -67,7 +64,7 @@ def next_epoch_with_attestations(state, fill_aggregate_attestation(post_state, prev_attestation) block.body.attestations.append(prev_attestation) - state_transition(post_state, block) + spec.state_transition(post_state, block) blocks.append(block) return state, blocks, post_state diff --git a/test_libs/pyspec/tests/test_sanity.py b/test_libs/pyspec/tests/test_sanity.py index 0cb1b9be3..1c05e6b53 100644 --- a/test_libs/pyspec/tests/test_sanity.py +++ b/test_libs/pyspec/tests/test_sanity.py @@ -20,13 +20,10 @@ from eth2spec.phase0.spec import ( get_block_root_at_slot, get_current_epoch, get_domain, - advance_slot, - cache_state, + process_slot, verify_merkle_branch, - hash, -) -from eth2spec.phase0.state_transition import ( state_transition, + hash, ) from eth2spec.utils.merkle_minimal import ( calc_merkle_tree_from_leaves, @@ -34,6 +31,7 @@ from eth2spec.utils.merkle_minimal import ( get_merkle_root, ) from .helpers import ( + advance_slot, get_balance, build_deposit_data, build_empty_block_for_next_slot, @@ -54,7 +52,7 @@ pytestmark = pytest.mark.sanity def test_slot_transition(state): test_state = deepcopy(state) - cache_state(test_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()