diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..02871530e --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,41 @@ +# Python CircleCI 2.0 configuration file +version: 2 +jobs: + build: + docker: + - image: circleci/python:3.6 + working_directory: ~/repo + + steps: + - checkout + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "requirements.txt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: + name: install dependencies + command: | + python3 -m venv venv + . venv/bin/activate + pip install -r requirements.txt + - run: + name: build phase0 spec + command: make build/phase0 + + - save_cache: + paths: + - ./venv + key: v1-dependencies-{{ checksum "requirements.txt" }} + + - run: + name: run tests + command: | + . venv/bin/activate + pytest tests + + - store_artifacts: + path: test-reports + destination: test-reports diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f33dd5256 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +/__pycache__ +/venv +/.pytest_cache + +build/ +output/ diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..88f17dcf9 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +SPEC_DIR = ./specs +SCRIPT_DIR = ./scripts +BUILD_DIR = ./build +UTILS_DIR = ./utils + + +.PHONY: clean all test + + +all: $(BUILD_DIR)/phase0 + + +clean: + rm -rf $(BUILD_DIR) + + +# runs a limited set of tests against a minimal config +# run pytest with `-m` option to full suite +test: + pytest -m minimal_config tests/ + + +$(BUILD_DIR)/phase0: + mkdir -p $@ + python3 $(SCRIPT_DIR)/phase0/build_spec.py $(SPEC_DIR)/core/0_beacon-chain.md $@/spec.py + mkdir -p $@/utils + cp $(UTILS_DIR)/phase0/* $@/utils + cp $(UTILS_DIR)/phase0/state_transition.py $@ + touch $@/__init__.py $@/utils/__init__.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..9145e951e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +eth-utils>=1.3.0,<2 +eth-typing>=2.1.0,<3.0.0 +oyaml==0.7 +pycryptodome==3.7.3 +py_ecc>=1.6.0 +pytest>=3.6,<3.7 diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/phase0/__init__.py b/scripts/phase0/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/phase0/build_spec.py b/scripts/phase0/build_spec.py new file mode 100644 index 000000000..6116f1ffe --- /dev/null +++ b/scripts/phase0/build_spec.py @@ -0,0 +1,79 @@ +import sys +import function_puller + + +def build_spec(sourcefile, outfile): + code_lines = [] + + code_lines.append("from build.phase0.utils.minimal_ssz import *") + code_lines.append("from build.phase0.utils.bls_stub import *") + for i in (1, 2, 3, 4, 8, 32, 48, 96): + code_lines.append("def int_to_bytes%d(x): return x.to_bytes(%d, 'little')" % (i, i)) + code_lines.append("SLOTS_PER_EPOCH = 64") # stub, will get overwritten by real var + code_lines.append("def slot_to_epoch(x): return x // SLOTS_PER_EPOCH") + + code_lines.append(""" +from typing import ( + Any, + Callable, + List, + NewType, + Tuple, +) + + +Slot = NewType('Slot', int) # uint64 +Epoch = NewType('Epoch', int) # uint64 +Shard = NewType('Shard', int) # uint64 +ValidatorIndex = NewType('ValidatorIndex', int) # uint64 +Gwei = NewType('Gwei', int) # uint64 +Bytes32 = NewType('Bytes32', bytes) # bytes32 +BLSPubkey = NewType('BLSPubkey', bytes) # bytes48 +BLSSignature = NewType('BLSSignature', bytes) # bytes96 +Any = None +Store = None + """) + + code_lines += function_puller.get_lines(sourcefile) + + code_lines.append(""" +# Monkey patch validator get committee code +_compute_committee = compute_committee +committee_cache = {} +def compute_committee(validator_indices: List[ValidatorIndex], + seed: Bytes32, + index: int, + total_committees: int) -> List[ValidatorIndex]: + + param_hash = (hash_tree_root(validator_indices), seed, index, total_committees) + + if param_hash in committee_cache: + # print("Cache hit, epoch={0}".format(epoch)) + return committee_cache[param_hash] + else: + # print("Cache miss, epoch={0}".format(epoch)) + ret = _compute_committee(validator_indices, seed, index, total_committees) + committee_cache[param_hash] = ret + return ret + + +# Monkey patch hash cache +_hash = hash +hash_cache = {} +def hash(x): + if x in hash_cache: + return hash_cache[x] + else: + ret = _hash(x) + hash_cache[x] = ret + return ret + """) + + with open(outfile, 'w') as out: + out.write("\n".join(code_lines)) + + +if __name__ == '__main__': + if len(sys.argv) < 3: + print("Error: spec source and outfile must defined") + build_spec(sys.argv[1], sys.argv[2]) diff --git a/scripts/phase0/function_puller.py b/scripts/phase0/function_puller.py new file mode 100644 index 000000000..7d5796fc7 --- /dev/null +++ b/scripts/phase0/function_puller.py @@ -0,0 +1,46 @@ +import sys + + +def get_lines(file_name): + code_lines = [] + pulling_from = None + current_name = None + processing_typedef = False + for linenum, line in enumerate(open(sys.argv[1]).readlines()): + line = line.rstrip() + if pulling_from is None and len(line) > 0 and line[0] == '#' and line[-1] == '`': + current_name = line[line[:-1].rfind('`') + 1: -1] + if line[:9] == '```python': + assert pulling_from is None + pulling_from = linenum + 1 + elif line[:3] == '```': + if pulling_from is None: + pulling_from = linenum + else: + if processing_typedef: + assert code_lines[-1] == '}' + code_lines[-1] = '})' + pulling_from = None + processing_typedef = False + else: + if pulling_from == linenum and line == '{': + code_lines.append('%s = SSZType({' % current_name) + processing_typedef = True + elif pulling_from is not None: + code_lines.append(line) + elif pulling_from is None and len(line) > 0 and line[0] == '|': + row = line[1:].split('|') + if len(row) >= 2: + for i in range(2): + row[i] = row[i].strip().strip('`') + if '`' in row[i]: + row[i] = row[i][:row[i].find('`')] + eligible = True + if row[0][0] not in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_': + eligible = False + for c in row[0]: + if c not in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789': + eligible = False + if eligible: + code_lines.append(row[0] + ' = ' + (row[1].replace('**TBD**', '0x1234567890123567890123456789012357890'))) + return code_lines diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 07179aa0a..0a105e89b 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -59,11 +59,17 @@ - [`get_current_epoch`](#get_current_epoch) - [`get_epoch_start_slot`](#get_epoch_start_slot) - [`is_active_validator`](#is_active_validator) + - [`is_slashable_validator`](#is_slashable_validator) - [`get_active_validator_indices`](#get_active_validator_indices) + - [`get_balance`](#get_balance) + - [`set_balance`](#set_balance) + - [`increase_balance`](#increase_balance) + - [`decrease_balance`](#decrease_balance) - [`get_permuted_index`](#get_permuted_index) - - [`split`](#split) + - [`get_split_offset`](#get_split_offset) - [`get_epoch_committee_count`](#get_epoch_committee_count) - - [`get_shuffling`](#get_shuffling) + - [`compute_committee`](#compute_committee) + - [`get_previous_epoch_committee_count`](#get_previous_epoch_committee_count) - [`get_current_epoch_committee_count`](#get_current_epoch_committee_count) - [`get_crosslink_committees_at_slot`](#get_crosslink_committees_at_slot) - [`get_block_root`](#get_block_root) @@ -112,7 +118,7 @@ - [Helper functions](#helper-functions-1) - [Justification](#justification) - [Crosslinks](#crosslinks) - - [Eth1 data](#eth1-data-1) + - [Eth1 data](#eth1-data) - [Rewards and penalties](#rewards-and-penalties) - [Justification and finalization](#justification-and-finalization) - [Crosslinks](#crosslinks-1) @@ -125,7 +131,7 @@ - [Per-block processing](#per-block-processing) - [Block header](#block-header) - [RANDAO](#randao) - - [Eth1 data](#eth1-data) + - [Eth1 data](#eth1-data-1) - [Transactions](#transactions) - [Proposer slashings](#proposer-slashings) - [Attester slashings](#attester-slashings) @@ -179,7 +185,7 @@ Code snippets appearing in `this style` are to be interpreted as Python code. | `SHARD_COUNT` | `2**10` (= 1,024) | | `TARGET_COMMITTEE_SIZE` | `2**7` (= 128) | | `MAX_BALANCE_CHURN_QUOTIENT` | `2**5` (= 32) | -| `MAX_INDICES_PER_SLASHABLE_VOTE` | `2**12` (= 4,096) | +| `MAX_SLASHABLE_ATTESTATION_PARTICIPANTS` | `2**12` (= 4,096) | | `MAX_EXIT_DEQUEUES_PER_EPOCH` | `2**2` (= 4) | | `SHUFFLE_ROUND_COUNT` | 90 | @@ -198,8 +204,8 @@ Code snippets appearing in `this style` are to be interpreted as Python code. | - | - | :-: | | `MIN_DEPOSIT_AMOUNT` | `2**0 * 10**9` (= 1,000,000,000) | Gwei | | `MAX_DEPOSIT_AMOUNT` | `2**5 * 10**9` (= 32,000,000,000) | Gwei | -| `FORK_CHOICE_BALANCE_INCREMENT` | `2**0 * 10**9` (= 1,000,000,000) | Gwei | | `EJECTION_BALANCE` | `2**4 * 10**9` (= 16,000,000,000) | Gwei | +| `HIGH_BALANCE_INCREMENT` | `2**0 * 10**9` (= 1,000,000,000) | Gwei | ### Initial values @@ -253,7 +259,7 @@ Code snippets appearing in `this style` are to be interpreted as Python code. | `MIN_PENALTY_QUOTIENT` | `2**5` (= 32) | * The `BASE_REWARD_QUOTIENT` parameter dictates the per-epoch reward. It corresponds to ~2.54% annual interest assuming 10 million participating ETH in every epoch. -* 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)`. +* 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 transactions per block @@ -316,6 +322,8 @@ The types are defined topologically to aid in facilitating an executable version { # Root of the deposit tree 'deposit_root': 'bytes32', + # Total number of deposits + 'deposit_count': 'uint64', # Block hash 'block_hash': 'bytes32', } @@ -415,7 +423,6 @@ The types are defined topologically to aid in facilitating an executable version 'signature': 'bytes96', } ``` - #### `Validator` ```python @@ -434,6 +441,8 @@ The types are defined topologically to aid in facilitating an executable version 'initiated_exit': 'bool', # Was the validator slashed 'slashed': 'bool', + # Rounded balance + 'high_balance': 'uint64' } ``` @@ -594,7 +603,7 @@ The types are defined topologically to aid in facilitating an executable version # Validator registry 'validator_registry': [Validator], - 'validator_balances': ['uint64'], + 'balances': ['uint64'], 'validator_registry_update_epoch': 'uint64', # Randomness and committees @@ -680,7 +689,8 @@ def get_temporary_block_header(block: BeaconBlock) -> BeaconBlockHeader: previous_block_root=block.previous_block_root, state_root=ZERO_HASH, block_body_root=hash_tree_root(block.body), - signature=block.signature, + # signed_root(block) is used for block id purposes so signature is a stub + signature=EMPTY_SIGNATURE, ) ``` @@ -733,6 +743,18 @@ def is_active_validator(validator: Validator, epoch: Epoch) -> bool: return validator.activation_epoch <= epoch < validator.exit_epoch ``` +### `is_slashable_validator` +```python +def is_slashable_validator(validator: Validator, epoch: Epoch) -> bool: + """ + Check if ``validator`` is slashable. + """ + return ( + validator.activation_epoch <= epoch < validator.withdrawable_epoch and + validator.slashed is False + ) +``` + ### `get_active_validator_indices` ```python @@ -743,12 +765,59 @@ def get_active_validator_indices(validators: List[Validator], epoch: Epoch) -> L return [i for i, v in enumerate(validators) if is_active_validator(v, epoch)] ``` +### `get_balance` + +```python +def get_balance(state: BeaconState, index: ValidatorIndex) -> Gwei: + """ + Return the balance for a validator with the given ``index``. + """ + return state.balances[index] +``` + +### `set_balance` + +```python +def set_balance(state: BeaconState, index: ValidatorIndex, balance: Gwei) -> None: + """ + Set the balance for a validator with the given ``index`` in both ``BeaconState`` + and validator's rounded balance ``high_balance``. + """ + validator = state.validator_registry[index] + HALF_INCREMENT = HIGH_BALANCE_INCREMENT // 2 + if validator.high_balance > balance or validator.high_balance + 3 * HALF_INCREMENT < balance: + validator.high_balance = balance - balance % HIGH_BALANCE_INCREMENT + state.balances[index] = balance +``` + +### `increase_balance` + +```python +def increase_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: + """ + Increase the balance for a validator with the given ``index`` by ``delta``. + """ + set_balance(state, index, get_balance(state, index) + delta) +``` + +### `decrease_balance` + +```python +def decrease_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: + """ + Decrease the balance for a validator with the given ``index`` by ``delta``. + Set to ``0`` when underflow. + """ + current_balance = get_balance(state, index) + set_balance(state, index, current_balance - delta if current_balance >= delta else 0) +``` + ### `get_permuted_index` ```python def get_permuted_index(index: int, list_size: int, seed: Bytes32) -> int: """ - Return `p(index)` in a pseudorandom permutation `p` of `0...list_size-1` with ``seed`` as entropy. + Return `p(index)` in a pseudorandom permutation `p` of `0...list_size - 1` with ``seed`` as entropy. Utilizes 'swap or not' shuffling found in https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf @@ -769,18 +838,15 @@ def get_permuted_index(index: int, list_size: int, seed: Bytes32) -> int: return index ``` -### `split` +### `get_split_offset` ```python -def split(values: List[Any], split_count: int) -> List[List[Any]]: - """ - Splits ``values`` into ``split_count`` pieces. - """ - list_length = len(values) - return [ - values[(list_length * i // split_count): (list_length * (i + 1) // split_count)] - for i in range(split_count) - ] +def get_split_offset(list_size: int, chunks: int, index: int) -> int: + """ + Returns a value such that for a list L, chunk count k and index i, + split(L, k)[i] == L[get_split_offset(len(L), k, i): get_split_offset(len(L), k, i+1)] + """ + return (list_size * index) // chunks ``` ### `get_epoch_committee_count` @@ -799,28 +865,26 @@ def get_epoch_committee_count(active_validator_count: int) -> int: ) * SLOTS_PER_EPOCH ``` -### `get_shuffling` +### `compute_committee` ```python -def get_shuffling(seed: Bytes32, - validators: List[Validator], - epoch: Epoch) -> List[List[ValidatorIndex]]: +def compute_committee(validator_indices: List[ValidatorIndex], + seed: Bytes32, + index: int, + total_committees: int) -> List[ValidatorIndex]: """ - Shuffle active validators and split into crosslink committees. - Return a list of committees (each a list of validator indices). + Return the ``index``'th shuffled committee out of a total ``total_committees`` + using ``validator_indices`` and ``seed``. """ - # Shuffle active validator indices - active_validator_indices = get_active_validator_indices(validators, epoch) - length = len(active_validator_indices) - shuffled_indices = [active_validator_indices[get_permuted_index(i, length, seed)] for i in range(length)] - - # Split the shuffled active validator indices - return split(shuffled_indices, get_epoch_committee_count(length)) + start_offset = get_split_offset(len(validator_indices), total_committees, index) + end_offset = get_split_offset(len(validator_indices), total_committees, index + 1) + return [ + validator_indices[get_permuted_index(i, len(validator_indices), seed)] + for i in range(start_offset, end_offset) + ] ``` -**Invariant**: if `get_shuffling(seed, validators, epoch)` returns some value `x` for some `epoch <= get_current_epoch(state) + ACTIVATION_EXIT_DELAY`, it should return the same value `x` for the same `seed` and `epoch` and possible future modifications of `validators` forever in phase 0, and until the ~1 year deletion delay in phase 2 and in the future. - -**Note**: this definition and the next few definitions make heavy use of repetitive computing. Production implementations are expected to appropriately use caching/memoization to avoid redoing work. +**Note**: this definition and the next few definitions are highly inefficient as algorithms as they re-calculate many sub-expressions. Production implementations are expected to appropriately use caching/memoization to avoid redoing work. ### `get_current_epoch_committee_count` @@ -867,18 +931,14 @@ def get_crosslink_committees_at_slot(state: BeaconState, current_epoch_committees = get_current_epoch_committee_count(state) start_shard = (state.latest_start_shard + EPOCH_LENGTH * current_epoch_committees) % SHARD_COUNT - shuffling = get_shuffling( - generate_seed(state, epoch), - state.validator_registry, - epoch, - ) - offset = slot % SLOTS_PER_EPOCH + indices = get_active_validator_indices(state.validator_registry, shuffling_epoch) committees_per_slot = committees_per_epoch // SLOTS_PER_EPOCH - slot_start_shard = (start_shard + committees_per_slot * offset) % SHARD_COUNT + offset = slot % SLOTS_PER_EPOCH + slot_start_shard = (shuffling_start_shard + committees_per_slot * offset) % SHARD_COUNT return [ ( - shuffling[committees_per_slot * offset + i], + compute_committee(indices, seed, committees_per_slot * offset + i, committees_per_epoch), (slot_start_shard + i) % SHARD_COUNT, ) for i in range(committees_per_slot) @@ -953,20 +1013,26 @@ def generate_seed(state: BeaconState, ```python def get_beacon_proposer_index(state: BeaconState, - slot: Slot, - registry_change: bool=False) -> ValidatorIndex: + slot: Slot) -> ValidatorIndex: """ Return the beacon proposer index for the ``slot``. + Due to proposer selection being based upon the validator balances during + the epoch in question, this can only be run for the current epoch. """ - epoch = slot_to_epoch(slot) current_epoch = get_current_epoch(state) - previous_epoch = get_previous_epoch(state) - next_epoch = current_epoch + 1 + assert slot_to_epoch(slot) == current_epoch - assert previous_epoch <= epoch <= next_epoch - - first_committee, _ = get_crosslink_committees_at_slot(state, slot, registry_change)[0] - return first_committee[epoch % len(first_committee)] + first_committee, _ = get_crosslink_committees_at_slot(state, slot)[0] + i = 0 + while True: + rand_byte = hash( + generate_seed(state, current_epoch) + + int_to_bytes8(i // 32) + )[i % 32] + candidate = first_committee[(current_epoch + i) % len(first_committee)] + if get_effective_balance(state, candidate) * 256 > MAX_DEPOSIT_AMOUNT * rand_byte: + return candidate + i += 1 ``` ### `verify_merkle_branch` @@ -1030,7 +1096,7 @@ def get_effective_balance(state: BeaconState, index: ValidatorIndex) -> Gwei: """ Return the effective balance (also known as "balance at stake") for a validator with the given ``index``. """ - return min(state.validator_balances[index], MAX_DEPOSIT_AMOUNT) + return min(get_balance(state, index), MAX_DEPOSIT_AMOUNT) ``` ### `get_total_balance` @@ -1107,7 +1173,7 @@ def verify_slashable_attestation(state: BeaconState, slashable_attestation: Slas if slashable_attestation.custody_bitfield != b'\x00' * len(slashable_attestation.custody_bitfield): # [TO BE REMOVED IN PHASE 1] return False - if len(slashable_attestation.validator_indices) == 0: + if not (1 <= len(slashable_attestation.validator_indices) <= MAX_SLASHABLE_ATTESTATION_PARTICIPANTS): return False for i in range(len(slashable_attestation.validator_indices) - 1): @@ -1117,9 +1183,6 @@ def verify_slashable_attestation(state: BeaconState, slashable_attestation: Slas if not verify_bitfield(slashable_attestation.custody_bitfield, len(slashable_attestation.validator_indices)): return False - if len(slashable_attestation.validator_indices) > MAX_INDICES_PER_SLASHABLE_VOTE: - return False - custody_bit_0_indices = [] custody_bit_1_indices = [] for i, validator_index in enumerate(slashable_attestation.validator_indices): @@ -1274,14 +1337,17 @@ def process_deposit(state: BeaconState, deposit: Deposit) -> None: withdrawable_epoch=FAR_FUTURE_EPOCH, initiated_exit=False, slashed=False, + high_balance=0 ) # Note: In phase 2 registry indices that have been withdrawn for a long time will be recycled. state.validator_registry.append(validator) - state.validator_balances.append(amount) + state.balances.append(0) + set_balance(state, len(state.validator_registry) - 1, amount) else: # Increase balance by deposit amount - state.validator_balances[validator_pubkeys.index(pubkey)] += amount + index = validator_pubkeys.index(pubkey) + increase_balance(state, index, amount) ``` ### Routines for updating validator status @@ -1318,17 +1384,14 @@ def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: ```python def exit_validator(state: BeaconState, index: ValidatorIndex) -> None: """ - Exit the validator of the given ``index``. + Exit the validator with the given ``index``. Note that this function mutates ``state``. """ validator = state.validator_registry[index] - delayed_activation_exit_epoch = get_delayed_activation_exit_epoch(get_current_epoch(state)) - # The following updates only occur if not previous exited - if validator.exit_epoch <= delayed_activation_exit_epoch: - return - else: - validator.exit_epoch = delayed_activation_exit_epoch + # Update validator exit epoch if not previously exited + if validator.exit_epoch == FAR_FUTURE_EPOCH: + validator.exit_epoch = get_delayed_activation_exit_epoch(get_current_epoch(state)) ``` #### `slash_validator` @@ -1340,14 +1403,13 @@ def slash_validator(state: BeaconState, index: ValidatorIndex) -> None: Note that this function mutates ``state``. """ validator = state.validator_registry[index] - assert state.slot < get_epoch_start_slot(validator.withdrawable_epoch) # [TO BE REMOVED IN PHASE 2] exit_validator(state, index) state.latest_slashed_balances[get_current_epoch(state) % LATEST_SLASHED_EXIT_LENGTH] += get_effective_balance(state, index) whistleblower_index = get_beacon_proposer_index(state, state.slot) whistleblower_reward = get_effective_balance(state, index) // WHISTLEBLOWER_REWARD_QUOTIENT - state.validator_balances[whistleblower_index] += whistleblower_reward - state.validator_balances[index] -= whistleblower_reward + increase_balance(state, whistleblower_index, whistleblower_reward) + decrease_balance(state, index, whistleblower_reward) validator.slashed = True validator.withdrawable_epoch = get_current_epoch(state) + LATEST_SLASHED_EXIT_LENGTH ``` @@ -1392,6 +1454,7 @@ When sufficiently many full deposits have been made the deposit contract emits t * `genesis_time` equals `time` in the `Eth2Genesis` log * `latest_eth1_data.deposit_root` equals `deposit_root` in the `Eth2Genesis` log +* `latest_eth1_data.deposit_count` equals `deposit_count` in the `Eth2Genesis` log * `latest_eth1_data.block_hash` equals the hash of the block that included the log * `genesis_validator_deposits` is a list of `Deposit` objects built according to the `Deposit` logs up to the deposit that triggered the `Eth2Genesis` log, processed in the order in which they were emitted (oldest to newest) @@ -1415,6 +1478,7 @@ When enough full deposits have been made to the deposit contract, an `Eth2Genesi * Let `genesis_time` be the timestamp specified in the `Eth2Genesis` log. * Let `genesis_eth1_data` be the `Eth1Data` object where: * `genesis_eth1_data.deposit_root` is the `deposit_root` contained in the `Eth2Genesis` log. + * `genesis_eth1_data.deposit_count` is the `deposit_count` contained in the `Eth2Genesis` log. * `genesis_eth1_data.block_hash` is the hash of the Ethereum 1.0 block that emitted the `Eth2Genesis` log. * Let `genesis_state = get_genesis_beacon_state(genesis_validator_deposits, genesis_time, genesis_eth1_data)`. * Let `genesis_block = get_empty_block()`. @@ -1433,6 +1497,7 @@ def get_empty_block() -> BeaconBlock: randao_reveal=EMPTY_SIGNATURE, eth1_data=Eth1Data( deposit_root=ZERO_HASH, + deposit_count=0, block_hash=ZERO_HASH, ), proposer_slashings=[], @@ -1465,7 +1530,7 @@ def get_genesis_beacon_state(genesis_validator_deposits: List[Deposit], # Validator registry validator_registry=[], - validator_balances=[], + balances=[], validator_registry_update_epoch=GENESIS_EPOCH, # Randomness and committees @@ -1475,7 +1540,7 @@ def get_genesis_beacon_state(genesis_validator_deposits: List[Deposit], # Finality previous_epoch_attestations=[], current_epoch_attestations=[], - previous_justified_epoch=GENESIS_EPOCH, + previous_justified_epoch=GENESIS_EPOCH - 1, current_justified_epoch=GENESIS_EPOCH, previous_justified_root=ZERO_HASH, current_justified_root=ZERO_HASH, @@ -1574,9 +1639,12 @@ def lmd_ghost(store: Store, start_state: BeaconState, start_block: BeaconBlock) for validator_index in active_validator_indices ] + # Use the rounded-balance-with-hysteresis supplied by the protocol for fork + # choice voting. This reduces the number of recomputations that need to be + # made for optimized implementations that precompute and save data def get_vote_count(block: BeaconBlock) -> int: return sum( - get_effective_balance(start_state.validator_balances[validator_index]) // FORK_CHOICE_BALANCE_INCREMENT + start_state.validator_registry[validator_index].high_balance for validator_index, target in attestation_targets if get_ancestor(store, target, block.slot) == block ) @@ -1624,7 +1692,7 @@ def cache_state(state: BeaconState) -> None: state.latest_block_header.state_root = previous_slot_state_root # store latest known block for previous slot - state.latest_block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = hash_tree_root(state.latest_block_header) + state.latest_block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = signed_root(state.latest_block_header) ``` ### Per-epoch processing @@ -1937,12 +2005,8 @@ def compute_inactivity_leak_deltas(state: BeaconState) -> Tuple[List[Gwei], List ```python def get_crosslink_deltas(state: BeaconState) -> Tuple[List[Gwei], List[Gwei]]: - # deltas[0] for rewards - # deltas[1] for penalties - deltas = [ - [0 for index in range(len(state.validator_registry))], - [0 for index in range(len(state.validator_registry))] - ] + rewards = [0 for index in range(len(state.validator_registry))] + penalties = [0 for index in range(len(state.validator_registry))] previous_epoch_start_slot = get_epoch_start_slot(get_previous_epoch(state)) current_epoch_start_slot = get_epoch_start_slot(get_current_epoch(state)) for slot in range(previous_epoch_start_slot, current_epoch_start_slot): @@ -1952,10 +2016,10 @@ def get_crosslink_deltas(state: BeaconState) -> Tuple[List[Gwei], List[Gwei]]: total_balance = get_total_balance(state, crosslink_committee) for index in crosslink_committee: if index in participants: - deltas[0][index] += get_base_reward(state, index) * participating_balance // total_balance + rewards[index] += get_base_reward(state, index) * participating_balance // total_balance else: - deltas[1][index] += get_base_reward(state, index) - return deltas + penalties[index] += get_base_reward(state, index) + return [rewards, penalties] ``` #### Apply rewards @@ -1964,12 +2028,16 @@ Run the following: ```python def apply_rewards(state: BeaconState) -> None: - deltas1 = get_justification_and_finalization_deltas(state) - deltas2 = get_crosslink_deltas(state) + rewards1, penalties1 = get_justification_and_finalization_deltas(state) + rewards2, penalties2 = get_crosslink_deltas(state) for i in range(len(state.validator_registry)): - state.validator_balances[i] = max( - 0, - state.validator_balances[i] + deltas1[0][i] + deltas2[0][i] - deltas1[1][i] - deltas2[1][i] + set_balance( + state, + i, + max( + 0, + get_balance(state, i) + rewards1[i] + rewards2[i] - penalties1[i] - penalties2[i], + ), ) ``` @@ -1984,8 +2052,8 @@ def process_ejections(state: BeaconState) -> None: and eject active validators with balance below ``EJECTION_BALANCE``. """ for index in get_active_validator_indices(state.validator_registry, get_current_epoch(state)): - if state.validator_balances[index] < EJECTION_BALANCE: - exit_validator(state, index) + if get_balance(state, index) < EJECTION_BALANCE: + initiate_validator_exit(state, index) ``` #### Validator registry and shuffling seed data @@ -2011,7 +2079,7 @@ def update_validator_registry(state: BeaconState) -> None: # Activate validators within the allowable balance churn balance_churn = 0 for index, validator in enumerate(state.validator_registry): - if validator.activation_epoch == FAR_FUTURE_EPOCH and state.validator_balances[index] >= MAX_DEPOSIT_AMOUNT: + if validator.activation_epoch == FAR_FUTURE_EPOCH and get_balance(state, index) >= MAX_DEPOSIT_AMOUNT: # Check the balance churn would be within the allowance balance_churn += get_effective_balance(state, index) if balance_churn > max_balance_churn: @@ -2021,16 +2089,21 @@ def update_validator_registry(state: BeaconState) -> None: activate_validator(state, index, is_genesis=False) # Exit validators within the allowable balance churn - balance_churn = 0 - for index, validator in enumerate(state.validator_registry): - if validator.exit_epoch == FAR_FUTURE_EPOCH and validator.initiated_exit: - # Check the balance churn would be within the allowance - balance_churn += get_effective_balance(state, index) - if balance_churn > max_balance_churn: - break + if current_epoch < state.validator_registry_update_epoch + LATEST_SLASHED_EXIT_LENGTH: + balance_churn = ( + state.latest_slashed_balances[state.validator_registry_update_epoch % LATEST_SLASHED_EXIT_LENGTH] - + state.latest_slashed_balances[current_epoch % LATEST_SLASHED_EXIT_LENGTH] + ) - # Exit validator - exit_validator(state, index) + for index, validator in enumerate(state.validator_registry): + if validator.exit_epoch == FAR_FUTURE_EPOCH and validator.initiated_exit: + # Check the balance churn would be within the allowance + balance_churn += get_effective_balance(state, index) + if balance_churn > max_balance_churn: + break + + # Exit validator + exit_validator(state, index) state.validator_registry_update_epoch = current_epoch ``` @@ -2075,7 +2148,7 @@ def process_slashings(state: BeaconState) -> None: get_effective_balance(state, index) * min(total_penalties * 3, total_balance) // total_balance, get_effective_balance(state, index) // MIN_PENALTY_QUOTIENT ) - state.validator_balances[index] -= penalty + decrease_balance(state, index, penalty) ``` ```python @@ -2153,11 +2226,13 @@ def process_block_header(state: BeaconState, block: BeaconBlock) -> None: # Verify that the slots match assert block.slot == state.slot # Verify that the parent matches - assert block.previous_block_root == hash_tree_root(state.latest_block_header) + assert block.previous_block_root == signed_root(state.latest_block_header) # Save current block as the new latest block state.latest_block_header = get_temporary_block_header(block) - # Verify proposer signature + # Verify proposer is not slashed proposer = state.validator_registry[get_beacon_proposer_index(state, state.slot)] + assert not proposer.slashed + # Verify proposer signature assert bls_verify( pubkey=proposer.pubkey, message_hash=signed_root(block), @@ -2218,8 +2293,8 @@ def process_proposer_slashing(state: BeaconState, assert slot_to_epoch(proposer_slashing.header_1.slot) == slot_to_epoch(proposer_slashing.header_2.slot) # But the headers are different assert proposer_slashing.header_1 != proposer_slashing.header_2 - # Proposer is not yet slashed - assert proposer.slashed is False + # Check proposer is slashable + assert is_slashable_validator(proposer, get_current_epoch(state)) # Signatures are valid for header in (proposer_slashing.header_1, proposer_slashing.header_2): assert bls_verify( @@ -2258,7 +2333,7 @@ def process_attester_slashing(state: BeaconState, index for index in attestation1.validator_indices if ( index in attestation2.validator_indices and - state.validator_registry[index].slashed is False + is_slashable_validator(state.validator_registry[index], get_current_epoch(state)) ) ] assert len(slashable_indices) >= 1 @@ -2278,81 +2353,56 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: Process ``Attestation`` transaction. Note that this function mutates ``state``. """ - # Can't submit attestations that are too far in history (or in prehistory) - assert attestation.data.slot >= GENESIS_SLOT - assert state.slot <= attestation.data.slot + SLOTS_PER_EPOCH - # Can't submit attestations too quickly - assert attestation.data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot - # Verify that the justified epoch and root is correct - if slot_to_epoch(attestation.data.slot) >= get_current_epoch(state): - # Case 1: current epoch attestations - assert attestation.data.source_epoch == state.current_justified_epoch - assert attestation.data.source_root == state.current_justified_root - else: - # Case 2: previous epoch attestations - assert attestation.data.source_epoch == state.previous_justified_epoch - assert attestation.data.source_root == state.previous_justified_root - # Check that the crosslink data is valid - acceptable_crosslink_data = { - # Case 1: Latest crosslink matches the one in the state - attestation.data.previous_crosslink, - # Case 2: State has already been updated, state's latest crosslink matches the crosslink - # the attestation is trying to create - Crosslink( + assert max(GENESIS_SLOT, state.slot - SLOTS_PER_EPOCH) <= attestation.data.slot + assert attestation.data.slot <= state.slot - MIN_ATTESTATION_INCLUSION_DELAY + + # Check target epoch, source epoch, and source root + target_epoch = slot_to_epoch(attestation.data.slot) + assert (target_epoch, attestation.data.source_epoch, attestation.data.source_root) in { + (get_current_epoch(state), state.current_justified_epoch, state.current_justified_root), + (get_previous_epoch(state), state.previous_justified_epoch, state.previous_justified_root), + } + + # Check crosslink data + assert attestation.data.crosslink_data_root == ZERO_HASH # [to be removed in phase 1] + assert state.latest_crosslinks[attestation.data.shard] in { + attestation.data.previous_crosslink, # Case 1: latest crosslink matches previous crosslink + Crosslink( # Case 2: latest crosslink matches current crosslink crosslink_data_root=attestation.data.crosslink_data_root, epoch=min(slot_to_epoch(attestation.data.slot), attestation.data.previous_crosslink.epoch + MAX_CROSSLINK_EPOCHS) ) } - assert state.latest_crosslinks[attestation.data.shard] in acceptable_crosslink_data - # Attestation must be nonempty! - assert attestation.aggregation_bitfield != b'\x00' * len(attestation.aggregation_bitfield) - # Custody must be empty (to be removed in phase 1) - assert attestation.custody_bitfield == b'\x00' * len(attestation.custody_bitfield) - # Get the committee for the specific shard that this attestation is for - crosslink_committee = [ - committee for committee, shard in get_crosslink_committees_at_slot(state, attestation.data.slot) - if shard == attestation.data.shard - ][0] - # Custody bitfield must be a subset of the attestation bitfield - for i in range(len(crosslink_committee)): - if get_bitfield_bit(attestation.aggregation_bitfield, i) == 0b0: - assert get_bitfield_bit(attestation.custody_bitfield, i) == 0b0 - # Verify aggregate signature - participants = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) - custody_bit_1_participants = get_attestation_participants(state, attestation.data, attestation.custody_bitfield) - custody_bit_0_participants = [i for i in participants if i not in custody_bit_1_participants] - assert bls_verify_multiple( - pubkeys=[ - bls_aggregate_pubkeys([state.validator_registry[i].pubkey for i in custody_bit_0_participants]), - bls_aggregate_pubkeys([state.validator_registry[i].pubkey for i in custody_bit_1_participants]), - ], - message_hashes=[ - hash_tree_root(AttestationDataAndCustodyBit(data=attestation.data, custody_bit=0b0)), - hash_tree_root(AttestationDataAndCustodyBit(data=attestation.data, custody_bit=0b1)), - ], + # Check custody bits [to be generalised in phase 1] + assert attestation.custody_bitfield == b'\x00' * len(attestation.custody_bitfield) + + # Check aggregate signature [to be generalised in phase 1] + participants = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) + assert len(participants) != 0 + assert bls_verify( + pubkey=bls_aggregate_pubkeys([state.validator_registry[i].pubkey for i in participants]), + message_hash=hash_tree_root(AttestationDataAndCustodyBit(data=attestation.data, custody_bit=0b0)), signature=attestation.aggregate_signature, - domain=get_domain(state.fork, slot_to_epoch(attestation.data.slot), DOMAIN_ATTESTATION), + domain=get_domain(state.fork, target_epoch, DOMAIN_ATTESTATION), ) - # Crosslink data root is zero (to be removed in phase 1) - assert attestation.data.crosslink_data_root == ZERO_HASH - # Apply the attestation + + # Cache pending attestation pending_attestation = PendingAttestation( data=attestation.data, aggregation_bitfield=attestation.aggregation_bitfield, custody_bitfield=attestation.custody_bitfield, inclusion_slot=state.slot ) - if slot_to_epoch(attestation.data.slot) == get_current_epoch(state): + if target_epoch == get_current_epoch(state): state.current_epoch_attestations.append(pending_attestation) - elif slot_to_epoch(attestation.data.slot) == get_previous_epoch(state): + else: state.previous_epoch_attestations.append(pending_attestation) ``` ##### Deposits -Verify that `len(block.body.deposits) <= MAX_DEPOSITS`. +Verify that `len(block.body.deposits) == min(MAX_DEPOSITS, latest_eth1_data.deposit_count - state.deposit_index)`. For each `deposit` in `block.body.deposits`, run `process_deposit(state, deposit)`. @@ -2369,13 +2419,15 @@ def process_voluntary_exit(state: BeaconState, exit: VoluntaryExit) -> None: Note that this function mutates ``state``. """ validator = state.validator_registry[exit.validator_index] + # Verify the validator is active + assert is_active_validator(validator, get_current_epoch(state)) # Verify the validator has not yet exited assert validator.exit_epoch == FAR_FUTURE_EPOCH # Verify the validator has not initiated an exit assert validator.initiated_exit is False # Exits must specify an epoch when they become valid; they are not valid before then assert get_current_epoch(state) >= exit.epoch - # Must have been in the validator set long enough + # Verify the validator has been active long enough assert get_current_epoch(state) - validator.activation_epoch >= PERSISTENT_COMMITTEE_PERIOD # Verify signature assert bls_verify( @@ -2384,7 +2436,7 @@ def process_voluntary_exit(state: BeaconState, exit: VoluntaryExit) -> None: signature=exit.signature, domain=get_domain(state.fork, exit.epoch, DOMAIN_VOLUNTARY_EXIT) ) - # Run the exit + # Initiate exit initiate_validator_exit(state, exit.validator_index) ``` @@ -2403,12 +2455,12 @@ def process_transfer(state: BeaconState, transfer: Transfer) -> None: Note that this function mutates ``state``. """ # Verify the amount and fee aren't individually too big (for anti-overflow purposes) - assert state.validator_balances[transfer.sender] >= max(transfer.amount, transfer.fee) + assert get_balance(state, transfer.sender) >= max(transfer.amount, transfer.fee) # Verify that we have enough ETH to send, and that after the transfer the balance will be either # exactly zero or at least MIN_DEPOSIT_AMOUNT assert ( - state.validator_balances[transfer.sender] == transfer.amount + transfer.fee or - state.validator_balances[transfer.sender] >= transfer.amount + transfer.fee + MIN_DEPOSIT_AMOUNT + get_balance(state, transfer.sender) == transfer.amount + transfer.fee or + get_balance(state, transfer.sender) >= transfer.amount + transfer.fee + MIN_DEPOSIT_AMOUNT ) # A transfer is valid in only one slot assert state.slot == transfer.slot @@ -2430,9 +2482,9 @@ def process_transfer(state: BeaconState, transfer: Transfer) -> None: domain=get_domain(state.fork, slot_to_epoch(transfer.slot), DOMAIN_TRANSFER) ) # Process the transfer - state.validator_balances[transfer.sender] -= transfer.amount + transfer.fee - state.validator_balances[transfer.recipient] += transfer.amount - state.validator_balances[get_beacon_proposer_index(state, state.slot)] += transfer.fee + decrease_balance(state, transfer.sender, transfer.amount + transfer.fee) + increase_balance(state, transfer.recipient, transfer.amount) + increase_balance(state, get_beacon_proposer_index(state, state.slot), transfer.fee) ``` #### State root verification diff --git a/specs/core/1_shard-data-chains.md b/specs/core/1_shard-data-chains.md index c76f9ba08..92cee4d19 100644 --- a/specs/core/1_shard-data-chains.md +++ b/specs/core/1_shard-data-chains.md @@ -19,7 +19,6 @@ At the current stage, Phase 1, while fundamentally feature-complete, is still su - [Signature domains](#signature-domains) - [Shard chains and crosslink data](#shard-chains-and-crosslink-data) - [Helper functions](#helper-functions) - - [`get_split_offset`](#get_split_offset) - [`get_shuffled_committee`](#get_shuffled_committee) - [`get_persistent_committee`](#get_persistent_committee) - [`get_shard_proposer_index`](#get_shard_proposer_index) @@ -122,17 +121,6 @@ Phase 1 depends upon all of the constants defined in [Phase 0](0_beacon-chain.md ## Helper functions -#### `get_split_offset` - -````python -def get_split_offset(list_size: int, chunks: int, index: int) -> int: - """ - Returns a value such that for a list L, chunk count k and index i, - split(L, k)[i] == L[get_split_offset(len(L), k, i): get_split_offset(len(L), k, i+1)] - """ - return (list_size * index) // chunks -```` - #### `get_shuffled_committee` ```python diff --git a/specs/light_client/merkle_proofs.md b/specs/light_client/merkle_proofs.md new file mode 100644 index 000000000..cf4dad2e3 --- /dev/null +++ b/specs/light_client/merkle_proofs.md @@ -0,0 +1,134 @@ +### Generalized Merkle tree index + +In a binary Merkle tree, we define a "generalized index" of a node as `2**depth + index`. Visually, this looks as follows: + +``` + 1 + 2 3 +4 5 6 7 + ... +``` + +Note that the generalized index has the convenient property that the two children of node `k` are `2k` and `2k+1`, and also that it equals the position of a node in the linear representation of the Merkle tree that's computed by this function: + +```python +def merkle_tree(leaves): + o = [0] * len(leaves) + leaves + for i in range(len(leaves)-1, 0, -1): + o[i] = hash(o[i*2] + o[i*2+1]) + return o +``` + +We will define Merkle proofs in terms of generalized indices. + +### SSZ object to index + +We can describe the hash tree of any SSZ object, rooted in `hash_tree_root(object)`, as a binary Merkle tree whose depth may vary. For example, an object `{x: bytes32, y: List[uint64]}` would look as follows: + +``` + root + / \ + x y_root + / \ +y_data_root len(y) + / \ + /\ /\ + ....... +``` + +We can now define a concept of a "path", a way of describing a function that takes as input an SSZ object and outputs some specific (possibly deeply nested) member. For example, `foo -> foo.x` is a path, as are `foo -> len(foo.y)` and `foo -> foo[5]`. We'll describe paths as lists: in these three cases they are `["x"]`, `["y", "len"]` and `["y", 5]` respectively. We can now define a function `get_generalized_indices(object: Any, path: List[str OR int], root=1: int) -> int` that converts an object and a path to a set of generalized indices (note that for constant-sized objects, there is only one generalized index and it only depends on the path, but for dynamically sized objects the indices may depend on the object itself too). For dynamically-sized objects, the set of indices will have more than one member because of the need to access an array's length to determine the correct generalized index for some array access. + +```python +def get_generalized_indices(obj: Any, path: List[str or int], root=1) -> List[int]: + if len(path) == 0: + return [root] + elif isinstance(obj, StaticList): + items_per_chunk = (32 // len(serialize(x))) if isinstance(x, int) else 1 + new_root = root * next_power_of_2(len(obj) // items_per_chunk) + path[0] // items_per_chunk + return get_generalized_indices(obj[path[0]], path[1:], new_root) + elif isinstance(obj, DynamicList) and path[0] == "len": + return [root * 2 + 1] + elif isinstance(obj, DynamicList) and isinstance(path[0], int): + assert path[0] < len(obj) + items_per_chunk = (32 // len(serialize(x))) if isinstance(x, int) else 1 + new_root = root * 2 * next_power_of_2(len(obj) // items_per_chunk) + path[0] // items_per_chunk + return [root *2 + 1] + get_generalized_indices(obj[path[0]], path[1:], new_root) + elif hasattr(obj, "fields"): + index = list(fields.keys()).index(path[0]) + new_root = root * next_power_of_2(len(fields)) + index + return get_generalized_indices(getattr(obj, path[0]), path[1:], new_root) + else: + raise Exception("Unknown type / path") +``` + +### Merkle multiproofs + +We define a Merkle multiproof as a minimal subset of nodes in a Merkle tree needed to fully authenticate that a set of nodes actually are part of a Merkle tree with some specified root, at a particular set of generalized indices. For example, here is the Merkle multiproof for positions 0, 1, 6 in an 8-node Merkle tree (ie. generalized indices 8, 9, 14): + +``` + . + . . + . * * . +x x . . . . x * +``` + +. are unused nodes, * are used nodes, x are the values we are trying to prove. Notice how despite being a multiproof for 3 values, it requires only 3 auxiliary nodes, only one node more than would be required to prove a single value. Normally the efficiency gains are not quite that extreme, but the savings relative to individual Merkle proofs are still significant. As a rule of thumb, a multiproof for k nodes at the same level of an n-node tree has size `k * (n/k + log(n/k))`. + +Here is code for creating and verifying a multiproof. First a helper: + +```python +def log2(x): + return 0 if x == 1 else 1 + log2(x//2) +``` + +First, a method for computing the generalized indices of the auxiliary tree nodes that a proof of a given set of generalized indices will require: + +```python +def get_proof_indices(tree_indices: List[int]) -> List[int]: + # Get all indices touched by the proof + maximal_indices = set({}) + for i in tree_indices: + x = i + while x > 1: + maximal_indices.add(x ^ 1) + x //= 2 + maximal_indices = tree_indices + sorted(list(maximal_indices))[::-1] + # Get indices that cannot be recalculated from earlier indices + redundant_indices = set({}) + proof = [] + for index in maximal_indices: + if index not in redundant_indices: + proof.append(index) + while index > 1: + redundant_indices.add(index) + if (index ^ 1) not in redundant_indices: + break + index //= 2 + return [i for i in proof if i not in tree_indices] +```` + +Generating a proof is simply a matter of taking the node of the SSZ hash tree with the union of the given generalized indices for each index given by `get_proof_indices`, and outputting the list of nodes in the same order. + +```python +def verify_multi_proof(root, indices, leaves, proof): + tree = {} + for index, leaf in zip(indices, leaves): + tree[index] = leaf + for index, proofitem in zip(get_proof_indices(indices), proof): + tree[index] = proofitem + indexqueue = sorted(tree.keys())[:-1] + i = 0 + while i < len(indexqueue): + index = indexqueue[i] + if index >= 2 and index^1 in tree: + tree[index//2] = hash(tree[index - index%2] + tree[index - index%2 + 1]) + indexqueue.append(index//2) + i += 1 + return (indices == []) or (1 in tree and tree[1] == root) +``` + +#### Proofs for execution + +We define `MerklePartial(f, arg1, arg2...)` as being a list of Merkle multiproofs of the sets of nodes in the hash trees of the SSZ objects that are needed to authenticate the values needed to compute some function `f(arg1, arg2...)`. An individual Merkle multiproof is given as a dynamic sized list of `bytes32` values, a `MerklePartial` is a fixed-size list of objects `{proof: ["bytes32"], value: "bytes32"}`, one for each `arg` to `f` (if some `arg` is a base type, then the multiproof is empty). + +Ideally, any function which accepts an SSZ object should also be able to accept a `MerklePartial` object as a substitute. diff --git a/specs/light_client/sync_protocol.md b/specs/light_client/sync_protocol.md new file mode 100644 index 000000000..6f020af95 --- /dev/null +++ b/specs/light_client/sync_protocol.md @@ -0,0 +1,184 @@ +# Beacon Chain Light Client Syncing + +__NOTICE__: This document is a work-in-progress for researchers and implementers. One of the design goals of the eth2 beacon chain is light-client friendlines, both to allow low-resource clients (mobile phones, IoT, etc) to maintain access to the blockchain in a reasonably safe way, but also to facilitate the development of "bridges" between the eth2 beacon chain and other chains. + +## Table of Contents + + +- [Beacon Chain Light Client Syncing](#beacon-chain-light-client-syncing) + - [Table of Contents](#table-of-contents) + - [Light client state](#light-client-state) + - [Updating the shuffled committee](#updating-the-shuffled-committee) + - [Computing the current committee](#computing-the-current-committee) + - [Verifying blocks](#verifying-blocks) + + + +### Preliminaries + +We define an "expansion" of an object as an object where a field in an object that is meant to represent the `hash_tree_root` of another object is replaced by the object. Note that defining expansions is not a consensus-layer-change; it is merely a "re-interpretation" of the object. Particularly, the `hash_tree_root` of an expansion of an object is identical to that of the original object, and we can define expansions where, given a complete history, it is always possible to compute the expansion of any object in the history. The opposite of an expansion is a "summary" (eg. `BeaconBlockHeader` is a summary of `BeaconBlock`). + +We define two expansions: + +* `ExtendedBeaconBlock`, which is identical to a `BeaconBlock` except `state_root` is replaced with the corresponding `state: ExtendedBeaconState` +* `ExtendedBeaconState`, which is identical to a `BeaconState` except `latest_active_index_roots: List[Bytes32]` is replaced by `latest_active_indices: List[List[ValidatorIndex]]`, where `BeaconState.latest_active_index_roots[i] = hash_tree_root(ExtendedBeaconState.latest_active_indices[i])` + +Note that there is now a new way to compute `get_active_validator_indices`: + +```python +def get_active_validator_indices(state: BeaconState, epoch: Epoch) -> List[ValidatorIndex]: + return state.latest_active_indices[epoch % LATEST_ACTIVE_INDEX_ROOTS_LENGTH] +``` + +Note that it takes `state` instead of `state.validator_registry` as an argument. This does not affect its use in `get_shuffled_committee`, because `get_shuffled_committee` has access to the full `state` as one of its arguments. + +A `MerklePartial(f, *args)` is an object that contains a minimal Merkle proof needed to compute `f(*args)`. A `MerklePartial` can be used in place of a regular SSZ object, though a computation would return an error if it attempts to access part of the object that is not contained in the proof. + +We add a data type `PeriodData` and four helpers: + +```python +{ + 'validator_count': 'uint64', + 'seed': 'bytes32', + 'committee': [Validator] +} +``` + +```python +def get_earlier_start_epoch(slot: Slot) -> int: + return slot - slot % PERSISTENT_COMMITTEE_PERIOD - PERSISTENT_COMMITTEE_PERIOD * 2 + +def get_later_start_epoch(slot: Slot) -> int: + return slot - slot % PERSISTENT_COMMITTEE_PERIOD - PERSISTENT_COMMITTEE_PERIOD + +def get_earlier_period_data(block: ExtendedBeaconBlock, shard_id: Shard) -> PeriodData: + period_start = get_earlier_start_epoch(block.slot) + validator_count = len(get_active_validator_indices(block.state, period_start)) + committee_count = validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE) + 1 + indices = get_shuffled_committee(block.state, shard_id, period_start, 0, committee_count) + return PeriodData( + validator_count, + generate_seed(block.state, period_start), + [block.state.validator_registry[i] for i in indices] + ) + +def get_later_period_data(block: ExtendedBeaconBlock, shard_id: Shard) -> PeriodData: + period_start = get_later_start_epoch(block.slot) + validator_count = len(get_active_validator_indices(block.state, period_start)) + committee_count = validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE) + 1 + indices = get_shuffled_committee(block.state, shard_id, period_start, 0, committee_count) + return PeriodData( + validator_count, + generate_seed(block.state, period_start), + [block.state.validator_registry[i] for i in indices] + ) +``` + +### Light client state + +A light client will keep track of: + +* A random `shard_id` in `[0...SHARD_COUNT-1]` (selected once and retained forever) +* A block header that they consider to be finalized (`finalized_header`) and do not expect to revert. +* `later_period_data = get_maximal_later_committee(finalized_header, shard_id)` +* `earlier_period_data = get_maximal_earlier_committee(finalized_header, shard_id)` + +We use the struct `validator_memory` to keep track of these variables. + +### Updating the shuffled committee + +If a client's `validator_memory.finalized_header` changes so that `header.slot // PERSISTENT_COMMITTEE_PERIOD` increases, then the client can ask the network for a `new_committee_proof = MerklePartial(get_maximal_later_committee, validator_memory.finalized_header, shard_id)`. It can then compute: + +```python +earlier_period_data = later_period_data +later_period_data = get_later_period_data(new_committee_proof, finalized_header, shard_id) +``` + +The maximum size of a proof is `128 * ((22-7) * 32 + 110) = 75520` bytes for validator records and `(22-7) * 32 + 128 * 8 = 1504` for the active index proof (much smaller because the relevant active indices are all beside each other in the Merkle tree). This needs to be done once per `PERSISTENT_COMMITTEE_PERIOD` epochs (2048 epochs / 9 days), or ~38 bytes per epoch. + +### Computing the current committee + +Here is a helper to compute the committee at a slot given the maximal earlier and later committees: + +```python +def compute_committee(header: BeaconBlockHeader, + validator_memory: ValidatorMemory): + + earlier_validator_count = validator_memory.earlier_period_data.validator_count + later_validator_count = validator_memory.later_period_data.validator_count + earlier_committee = validator_memory.earlier_period_data.committee + later_committee = validator_memory.later_period_data.committee + earlier_start_epoch = get_earlier_start_epoch(header.slot) + later_start_epoch = get_later_start_epoch(header.slot) + epoch = slot_to_epoch(header.slot) + + actual_committee_count = max( + earlier_validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE), + later_validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE), + ) + 1 + + def get_offset(count, end:bool): + return get_split_offset(count, + SHARD_COUNT * committee_count, + validator_memory.shard_id * committee_count + (1 if end else 0)) + + actual_earlier_committee = maximal_earlier_committee[ + 0:get_offset(earlier_validator_count, True) - get_offset(earlier_validator_count, False) + ] + actual_later_committee = maximal_later_committee[ + 0:get_offset(later_validator_count, True) - get_offset(later_validator_count, False) + ] + def get_switchover_epoch(index): + return ( + bytes_to_int(hash(validator_memory.earlier_period_data.seed + bytes3(index))[0:8]) % + PERSISTENT_COMMITTEE_PERIOD + ) + + # Take not-yet-cycled-out validators from earlier committee and already-cycled-in validators from + # later committee; return a sorted list of the union of the two, deduplicated + return sorted(list(set( + [i for i in earlier_committee if epoch % PERSISTENT_COMMITTEE_PERIOD < get_switchover_epoch(i)] + + [i for i in later_committee if epoch % PERSISTENT_COMMITTEE_PERIOD >= get_switchover_epoch(i)] + ))) + +``` + +Note that this method makes use of the fact that the committee for any given shard always starts and ends at the same validator index independently of the committee count (this is because the validator set is split into `SHARD_COUNT * committee_count` slices but the first slice of a shard is a multiple `committee_count * i`, so the start of the slice is `n * committee_count * i // (SHARD_COUNT * committee_count) = n * i // SHARD_COUNT`, using the slightly nontrivial algebraic identity `(x * a) // ab == x // b`). + +### Verifying blocks + +If a client wants to update its `finalized_header` it asks the network for a `BlockValidityProof`, which is simply: + +```python +{ + 'header': BlockHeader, + 'shard_aggregate_signature': 'bytes96', + 'shard_bitfield': 'bytes', + 'shard_parent_block': ShardBlock +} +``` + +The verification procedure is as follows: + +```python +def verify_block_validity_proof(proof: BlockValidityProof, validator_memory: ValidatorMemory) -> bool: + assert proof.shard_parent_block.beacon_chain_ref == hash_tree_root(proof.header) + committee = compute_committee(proof.header, validator_memory) + # Verify that we have >=50% support + support_balance = sum([c.high_balance for i, c in enumerate(committee) if get_bitfield_bit(proof.shard_bitfield, i) is True]) + total_balance = sum([c.high_balance for i, c in enumerate(committee)] + assert support_balance * 2 > total_balance + # Verify shard attestations + group_public_key = bls_aggregate_pubkeys([ + v.pubkey for v, index in enumerate(committee) if + get_bitfield_bit(proof.shard_bitfield, i) is True + ]) + assert bls_verify( + pubkey=group_public_key, + message_hash=hash_tree_root(shard_parent_block), + signature=shard_aggregate_signature, + domain=get_domain(state, slot_to_epoch(shard_block.slot), DOMAIN_SHARD_ATTESTER) + ) +``` + +The size of this proof is only 200 (header) + 96 (signature) + 16 (bitfield) + 352 (shard block) = 664 bytes. It can be reduced further by replacing `ShardBlock` with `MerklePartial(lambda x: x.beacon_chain_ref, ShardBlock)`, which would cut off ~220 bytes. diff --git a/specs/validator/0_beacon-chain-validator.md b/specs/validator/0_beacon-chain-validator.md index be3008227..f1a10a048 100644 --- a/specs/validator/0_beacon-chain-validator.md +++ b/specs/validator/0_beacon-chain-validator.md @@ -182,15 +182,15 @@ epoch_signature = bls_sign( * Let `D` be the set of `Eth1DataVote` objects `vote` in `state.eth1_data_votes` where: * `vote.eth1_data.block_hash` is the hash of an eth1.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_data`. + * `vote.eth1_data.deposit_count` is the deposit count of the eth1.0 deposit contract at the block defined by `vote.eth1_data.block_hash`. * `vote.eth1_data.deposit_root` is the deposit root of the eth1.0 deposit contract at the block defined by `vote.eth1_data.block_hash`. * If `D` is empty: * Let `block_hash` be the block hash of the `ETH1_FOLLOW_DISTANCE`'th ancestor of the head of the canonical eth1.0 chain. - * Let `deposit_root` be the deposit root of the eth1.0 deposit contract in the post-state of the block referenced by `block_hash` + * Let `deposit_root` and `deposit_count` be the deposit root and deposit count of the eth1.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` be the member of `D` that has the highest `vote.vote_count`, breaking ties by favoring block hashes with higher associated block height. - * Let `block_hash = best_vote.eth1_data.block_hash`. - * Let `deposit_root = best_vote.eth1_data.deposit_root`. -* Set `block.eth1_data = Eth1Data(deposit_root=deposit_root, block_hash=block_hash)`. + * 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. +* Set `block.eth1_data = best_vote_data`. ##### Signature @@ -212,25 +212,25 @@ block_signature = bls_sign( ##### Proposer slashings -Up to `MAX_PROPOSER_SLASHINGS` [`ProposerSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposerslashing) objects can be included in the `block`. The proposer slashings must satisfy the verification conditions found in [proposer slashings processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposer-slashings-1). The validator receives a small "whistleblower" reward for each proposer slashing found and included. +Up to `MAX_PROPOSER_SLASHINGS` [`ProposerSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposerslashing) objects can be included in the `block`. The proposer slashings must satisfy the verification conditions found in [proposer slashings processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposer-slashings). The validator receives a small "whistleblower" reward for each proposer slashing found and included. ##### Attester slashings -Up to `MAX_ATTESTER_SLASHINGS` [`AttesterSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attesterslashing) objects can be included in the `block`. The attester slashings must satisfy the verification conditions found in [Attester slashings processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attester-slashings-1). The validator receives a small "whistleblower" reward for each attester slashing found and included. +Up to `MAX_ATTESTER_SLASHINGS` [`AttesterSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attesterslashing) objects can be included in the `block`. The attester slashings must satisfy the verification conditions found in [Attester slashings processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attester-slashings). The validator receives a small "whistleblower" reward for each attester slashing found and included. ##### Attestations -Up to `MAX_ATTESTATIONS` aggregate attestations can be included in the `block`. The attestations added must satisfy the verification conditions found in [attestation processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestations-1). To maximize profit, the validator should attempt to gather aggregate attestations that include singular attestations from the largest number of validators whose signatures from the same epoch have not previously been added on chain. +Up to `MAX_ATTESTATIONS` aggregate attestations can be included in the `block`. The attestations added must satisfy the verification conditions found in [attestation processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestations). To maximize profit, the validator should attempt to gather aggregate attestations that include singular attestations from the largest number of validators whose signatures from the same epoch have not previously been added on chain. ##### Deposits -Up to `MAX_DEPOSITS` [`Deposit`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#deposit) objects can be included in the `block`. These deposits are constructed from the `Deposit` logs from the [Eth1.0 deposit contract](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#ethereum-10-deposit-contract) and must be processed in sequential order. The deposits included in the `block` must satisfy the verification conditions found in [deposits processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#deposits-1). +If there are any unprocessed deposits for the existing `state.latest_eth1_data` (i.e. `state.latest_eth1_data.deposit_count > state.deposit_index`), then pending deposits _must_ be added to the block. The expected number of deposits is exactly `min(MAX_DEPOSITS, latest_eth1_data.deposit_count - state.deposit_index)`. These [`deposits`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#deposit) are constructed from the `Deposit` logs from the [Eth1.0 deposit contract](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#ethereum-10-deposit-contract) and must be processed in sequential order. The deposits included in the `block` must satisfy the verification conditions found in [deposits processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#deposits). The `proof` for each deposit must be constructed against the deposit root contained in `state.latest_eth1_data` rather than the deposit root at the time the deposit was initially logged from the 1.0 chain. This entails storing a full deposit merkle tree locally and computing updated proofs against the `latest_eth1_data.deposit_root` as needed. See [`minimal_merkle.py`](https://github.com/ethereum/research/blob/master/spec_pythonizer/utils/merkle_minimal.py) for a sample implementation. ##### Voluntary exits -Up to `MAX_VOLUNTARY_EXITS` [`VoluntaryExit`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#voluntaryexit) objects can be included in the `block`. The exits must satisfy the verification conditions found in [exits processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#exits-1). +Up to `MAX_VOLUNTARY_EXITS` [`VoluntaryExit`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#voluntaryexit) objects can be included in the `block`. The exits must satisfy the verification conditions found in [exits processing](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#voluntary-exits). ### Attestations @@ -338,15 +338,13 @@ def get_committee_assignment( state: BeaconState, epoch: Epoch, validator_index: ValidatorIndex, - registry_change: bool=False) -> Tuple[List[ValidatorIndex], Shard, Slot, bool]: + registry_change: bool=False) -> Tuple[List[ValidatorIndex], Shard, Slot]: """ Return the committee assignment in the ``epoch`` for ``validator_index`` and ``registry_change``. ``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 - * ``assignment[3]`` is a bool signalling if the validator is expected to propose - a beacon block at the assigned slot. """ previous_epoch = get_previous_epoch(state) next_epoch = get_current_epoch(state) + 1 @@ -367,15 +365,29 @@ def get_committee_assignment( if len(selected_committees) > 0: validators = selected_committees[0][0] shard = selected_committees[0][1] - is_proposer = validator_index == get_beacon_proposer_index(state, slot, registry_change=registry_change) - assignment = (validators, shard, slot, is_proposer) + 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 epoch of the slot in question and can not reliably be used to predict an epoch in advance. + +```python +def is_proposer_at_slot(state: BeaconState, + slot: Slot, + validator_index: ValidatorIndex) -> bool: + current_epoch = get_current_epoch(state) + assert slot_to_epoch(slot) == current_epoch + + return get_beacon_proposer_index(state, slot) == validator_index +``` + +_Note_: If a validator is assigned to the 0th slot of an epoch, the validator must run an empty slot transition from the previous epoch into the 0th slot of the epoch to be able to check if they are a proposer at that slot. + + ### Lookahead -The beacon chain shufflings are designed to provide a minimum of 1 epoch lookahead on the validator's upcoming assignments of proposing and attesting dictated by the shuffling and slot. +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 epoch in question. There are three possibilities for the shuffling at the next epoch: 1. The shuffling changes due to a "validator registry change". @@ -386,7 +398,7 @@ Either (2) or (3) occurs if (1) fails. The choice between (2) and (3) is determi When querying for assignments in the next epoch there are two options -- with and without a `registry_change` -- which is the optional fourth parameter of the `get_committee_assignment`. -`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 always plan for assignments from both values of `registry_change` unless the validator can concretely eliminate one of the options. Planning for future assignments involves noting at which future slot one might have to attest and propose and also which shard one should begin syncing (in phase 1+). +`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 always plan for assignments from both values of `registry_change` unless the validator can concretely eliminate one of the options. Planning for future assignments involves noting at which future slot one might have to attest and also which shard one should begin syncing (in phase 1+). Specifically, a validator should call both `get_committee_assignment(state, next_epoch, validator_index, registry_change=True)` and `get_committee_assignment(state, next_epoch, validator_index, registry_change=False)` when checking for next epoch assignments. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/phase0/__init__.py b/tests/phase0/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/phase0/block_processing/test_process_block_header.py b/tests/phase0/block_processing/test_process_block_header.py new file mode 100644 index 000000000..4981b656c --- /dev/null +++ b/tests/phase0/block_processing/test_process_block_header.py @@ -0,0 +1,71 @@ +from copy import deepcopy +import pytest + + +from build.phase0.spec import ( + get_beacon_proposer_index, + cache_state, + advance_slot, + process_block_header, +) +from tests.phase0.helpers import ( + build_empty_block_for_next_slot, +) + +# mark entire file as 'header' +pytestmark = pytest.mark.header + + +def prepare_state_for_header_processing(state): + cache_state(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_previous_block_root(state): + block = build_empty_block_for_next_slot(state) + block.previous_block_root = b'\12'*32 # invalid prev root + + pre_state, post_state = run_block_header_processing(state, block, valid=False) + return pre_state, block, None + + +def test_proposer_slashed(state): + # set proposer to slashed + proposer_index = get_beacon_proposer_index(state, state.slot + 1) + 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/tests/phase0/block_processing/test_process_deposit.py b/tests/phase0/block_processing/test_process_deposit.py new file mode 100644 index 000000000..0726dddef --- /dev/null +++ b/tests/phase0/block_processing/test_process_deposit.py @@ -0,0 +1,141 @@ +from copy import deepcopy +import pytest + +import build.phase0.spec as spec + +from build.phase0.spec import ( + get_balance, + ZERO_HASH, + process_deposit, +) +from tests.phase0.helpers import ( + build_deposit, + privkeys, + pubkeys, +) + + +# mark entire file as 'voluntary_exits' +pytestmark = pytest.mark.voluntary_exits + + +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_DEPOSIT_AMOUNT, + ) + + 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_DEPOSIT_AMOUNT + 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_DEPOSIT_AMOUNT // 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_DEPOSIT_AMOUNT, + ) + + # 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_DEPOSIT_AMOUNT, + ) + + # 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/tests/phase0/block_processing/test_process_proposer_slashing.py b/tests/phase0/block_processing/test_process_proposer_slashing.py new file mode 100644 index 000000000..467d2164b --- /dev/null +++ b/tests/phase0/block_processing/test_process_proposer_slashing.py @@ -0,0 +1,97 @@ +from copy import deepcopy +import pytest + +import build.phase0.spec as spec +from build.phase0.spec import ( + get_balance, + get_current_epoch, + process_proposer_slashing, +) +from tests.phase0.helpers import ( + get_valid_proposer_slashing, +) + +# mark entire file as 'header' +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 not slashed_validator.initiated_exit + 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/tests/phase0/block_processing/test_voluntary_exit.py b/tests/phase0/block_processing/test_voluntary_exit.py new file mode 100644 index 000000000..6adc81464 --- /dev/null +++ b/tests/phase0/block_processing/test_voluntary_exit.py @@ -0,0 +1,175 @@ +from copy import deepcopy +import pytest + +import build.phase0.spec as spec + +from build.phase0.spec import ( + get_active_validator_indices, + get_current_epoch, + process_voluntary_exit, +) +from tests.phase0.helpers import ( + build_voluntary_exit, + pubkey_to_privkey, +) + + +# mark entire file as 'voluntary_exits' +pytestmark = pytest.mark.voluntary_exits + + +def test_success(state): + pre_state = deepcopy(state) + # + # setup pre_state + # + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit + pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + # + # build voluntary exit + # + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey] + + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + post_state = deepcopy(pre_state) + + # + # test valid exit + # + process_voluntary_exit(post_state, voluntary_exit) + + assert not pre_state.validator_registry[validator_index].initiated_exit + assert post_state.validator_registry[validator_index].initiated_exit + + return pre_state, voluntary_exit, post_state + + +def test_validator_not_active(state): + pre_state = deepcopy(state) + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey] + + # + # setup pre_state + # + pre_state.validator_registry[validator_index].activation_epoch = spec.FAR_FUTURE_EPOCH + + # + # build and test voluntary exit + # + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + with pytest.raises(AssertionError): + process_voluntary_exit(pre_state, voluntary_exit) + + return pre_state, voluntary_exit, None + + +def test_validator_already_exited(state): + pre_state = deepcopy(state) + # + # setup pre_state + # + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow validator able to exit + pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey] + + # but validator already has exited + pre_state.validator_registry[validator_index].exit_epoch = current_epoch + 2 + + # + # build voluntary exit + # + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + with pytest.raises(AssertionError): + process_voluntary_exit(pre_state, voluntary_exit) + + return pre_state, voluntary_exit, None + + +def test_validator_already_initiated_exit(state): + pre_state = deepcopy(state) + # + # setup pre_state + # + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow validator able to exit + pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey] + + # but validator already has initiated exit + pre_state.validator_registry[validator_index].initiated_exit = True + + # + # build voluntary exit + # + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + with pytest.raises(AssertionError): + process_voluntary_exit(pre_state, voluntary_exit) + + return pre_state, voluntary_exit, None + + +def test_validator_not_active_long_enough(state): + pre_state = deepcopy(state) + # + # setup pre_state + # + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pubkey_to_privkey[pre_state.validator_registry[validator_index].pubkey] + + # but validator already has initiated exit + pre_state.validator_registry[validator_index].initiated_exit = True + + # + # build voluntary exit + # + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + assert ( + current_epoch - pre_state.validator_registry[validator_index].activation_epoch < + spec.PERSISTENT_COMMITTEE_PERIOD + ) + + with pytest.raises(AssertionError): + process_voluntary_exit(pre_state, voluntary_exit) + + return pre_state, voluntary_exit, None diff --git a/tests/phase0/conftest.py b/tests/phase0/conftest.py new file mode 100644 index 000000000..36a087941 --- /dev/null +++ b/tests/phase0/conftest.py @@ -0,0 +1,70 @@ +import pytest + +from build.phase0 import spec + +from tests.phase0.helpers import ( + create_genesis_state, +) + + +DEFAULT_CONFIG = {} # no change + +MINIMAL_CONFIG = { + "SHARD_COUNT": 8, + "MIN_ATTESTATION_INCLUSION_DELAY": 2, + "TARGET_COMMITTEE_SIZE": 4, + "SLOTS_PER_EPOCH": 8, + "GENESIS_EPOCH": spec.GENESIS_SLOT // 8, + "SLOTS_PER_HISTORICAL_ROOT": 64, + "LATEST_RANDAO_MIXES_LENGTH": 64, + "LATEST_ACTIVE_INDEX_ROOTS_LENGTH": 64, + "LATEST_SLASHED_EXIT_LENGTH": 64, +} + + +def overwrite_spec_config(config): + for field in config: + setattr(spec, field, config[field]) + if field == "LATEST_RANDAO_MIXES_LENGTH": + spec.BeaconState.fields['latest_randao_mixes'][1] = config[field] + elif field == "SHARD_COUNT": + spec.BeaconState.fields['latest_crosslinks'][1] = config[field] + elif field == "SLOTS_PER_HISTORICAL_ROOT": + spec.BeaconState.fields['latest_block_roots'][1] = config[field] + spec.BeaconState.fields['latest_state_roots'][1] = config[field] + spec.HistoricalBatch.fields['block_roots'][1] = config[field] + spec.HistoricalBatch.fields['state_roots'][1] = config[field] + elif field == "LATEST_ACTIVE_INDEX_ROOTS_LENGTH": + spec.BeaconState.fields['latest_active_index_roots'][1] = config[field] + elif field == "LATEST_SLASHED_EXIT_LENGTH": + spec.BeaconState.fields['latest_slashed_balances'][1] = config[field] + + +@pytest.fixture( + params=[ + pytest.param(MINIMAL_CONFIG, marks=pytest.mark.minimal_config), + DEFAULT_CONFIG, + ] +) +def config(request): + return request.param + + +@pytest.fixture(autouse=True) +def overwrite_config(config): + overwrite_spec_config(config) + + +@pytest.fixture +def num_validators(): + return 100 + + +@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/tests/phase0/helpers.py b/tests/phase0/helpers.py new file mode 100644 index 000000000..a0ede04e5 --- /dev/null +++ b/tests/phase0/helpers.py @@ -0,0 +1,247 @@ +from copy import deepcopy + +from py_ecc import bls + +import build.phase0.spec as spec +from build.phase0.utils.minimal_ssz import signed_root +from build.phase0.spec import ( + # constants + EMPTY_SIGNATURE, + ZERO_HASH, + # SSZ + AttestationData, + BeaconBlockHeader, + Deposit, + DepositInput, + DepositData, + Eth1Data, + ProposerSlashing, + VoluntaryExit, + # functions + get_active_validator_indices, + get_block_root, + get_current_epoch, + get_domain, + get_empty_block, + get_epoch_start_slot, + get_genesis_beacon_state, + verify_merkle_branch, + hash, +) +from build.phase0.utils.merkle_minimal import ( + calc_merkle_tree_from_leaves, + get_merkle_proof, + get_merkle_root, +) + + +privkeys = [i + 1 for i in range(1000)] +pubkeys = [bls.privtopub(privkey) for privkey in privkeys] +pubkey_to_privkey = {pubkey: privkey for privkey, pubkey in zip(privkeys, pubkeys)} + + +def create_mock_genesis_validator_deposits(num_validators, deposit_data_leaves=None): + if not deposit_data_leaves: + deposit_data_leaves = [] + deposit_timestamp = 0 + proof_of_possession = b'\x33' * 96 + + deposit_data_list = [] + for i in range(num_validators): + pubkey = pubkeys[i] + deposit_data = DepositData( + amount=spec.MAX_DEPOSIT_AMOUNT, + timestamp=deposit_timestamp, + deposit_input=DepositInput( + pubkey=pubkey, + # insecurely use pubkey as withdrawal key as well + withdrawal_credentials=spec.BLS_WITHDRAWAL_PREFIX_BYTE + hash(pubkey)[1:], + proof_of_possession=proof_of_possession, + ), + ) + item = hash(deposit_data.serialize()) + 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, + deposit_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 force_registry_change_at_next_epoch(state): + # artificially trigger registry update at next epoch transition + state.finalized_epoch = get_current_epoch(state) - 1 + for crosslink in state.latest_crosslinks: + crosslink.epoch = state.finalized_epoch + state.validator_registry_update_epoch = state.finalized_epoch - 1 + + +def build_empty_block_for_next_slot(state): + empty_block = get_empty_block() + empty_block.slot = state.slot + 1 + 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.previous_block_root = signed_root(previous_block_header) + return empty_block + + +def build_deposit_data(state, pubkey, privkey, amount): + deposit_input = DepositInput( + pubkey=pubkey, + # insecurely use pubkey as withdrawal key as well + withdrawal_credentials=spec.BLS_WITHDRAWAL_PREFIX_BYTE + hash(pubkey)[1:], + proof_of_possession=EMPTY_SIGNATURE, + ) + proof_of_possession = bls.sign( + message_hash=signed_root(deposit_input), + privkey=privkey, + domain=get_domain( + state.fork, + get_current_epoch(state), + spec.DOMAIN_DEPOSIT, + ) + ) + deposit_input.proof_of_possession = proof_of_possession + deposit_data = DepositData( + amount=amount, + timestamp=0, + deposit_input=deposit_input, + ) + return deposit_data + + +def build_attestation_data(state, slot, shard): + assert state.slot >= slot + + block_root = build_empty_block_for_next_slot(state).previous_block_root + + epoch_start_slot = get_epoch_start_slot(get_current_epoch(state)) + if epoch_start_slot == slot: + epoch_boundary_root = block_root + else: + get_block_root(state, epoch_start_slot) + + if slot < epoch_start_slot: + justified_block_root = state.previous_justified_root + else: + justified_block_root = state.current_justified_root + + return AttestationData( + slot=slot, + shard=shard, + beacon_block_root=block_root, + source_epoch=state.current_justified_epoch, + source_root=justified_block_root, + target_root=epoch_boundary_root, + crosslink_data_root=spec.ZERO_HASH, + previous_crosslink=deepcopy(state.latest_crosslinks[shard]), + ) + + +def build_voluntary_exit(state, epoch, validator_index, privkey): + voluntary_exit = VoluntaryExit( + epoch=epoch, + validator_index=validator_index, + signature=EMPTY_SIGNATURE, + ) + voluntary_exit.signature = bls.sign( + message_hash=signed_root(voluntary_exit), + privkey=privkey, + domain=get_domain( + fork=state.fork, + epoch=epoch, + domain_type=spec.DOMAIN_VOLUNTARY_EXIT, + ) + ) + + return voluntary_exit + + +def build_deposit(state, + deposit_data_leaves, + pubkey, + privkey, + amount): + deposit_data = build_deposit_data(state, pubkey, privkey, amount) + + item = hash(deposit_data.serialize()) + 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, + deposit_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.validator_registry, current_epoch)[-1] + privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] + slot = state.slot + + header_1 = BeaconBlockHeader( + slot=slot, + previous_block_root=ZERO_HASH, + state_root=ZERO_HASH, + block_body_root=ZERO_HASH, + signature=EMPTY_SIGNATURE, + ) + header_2 = deepcopy(header_1) + header_2.previous_block_root = b'\x02' * 32 + header_2.slot = slot + 1 + + domain = get_domain( + fork=state.fork, + epoch=get_current_epoch(state), + domain_type=spec.DOMAIN_BEACON_BLOCK, + ) + header_1.signature = bls.sign( + message_hash=signed_root(header_1), + privkey=privkey, + domain=domain, + ) + header_2.signature = bls.sign( + message_hash=signed_root(header_2), + privkey=privkey, + domain=domain, + ) + + return ProposerSlashing( + proposer_index=validator_index, + header_1=header_1, + header_2=header_2, + ) diff --git a/tests/phase0/test_sanity.py b/tests/phase0/test_sanity.py new file mode 100644 index 000000000..0b195fe96 --- /dev/null +++ b/tests/phase0/test_sanity.py @@ -0,0 +1,469 @@ +from copy import deepcopy + +import pytest + +from py_ecc import bls +import build.phase0.spec as spec + +from build.phase0.utils.minimal_ssz import signed_root +from build.phase0.spec import ( + # constants + EMPTY_SIGNATURE, + ZERO_HASH, + # SSZ + Attestation, + AttestationDataAndCustodyBit, + BeaconBlockHeader, + Deposit, + Transfer, + ProposerSlashing, + VoluntaryExit, + # functions + get_active_validator_indices, + get_attestation_participants, + get_balance, + get_block_root, + get_crosslink_committees_at_slot, + get_current_epoch, + get_domain, + get_state_root, + advance_slot, + cache_state, + set_balance, + verify_merkle_branch, + hash, +) +from build.phase0.state_transition import ( + state_transition, +) +from build.phase0.utils.merkle_minimal import ( + calc_merkle_tree_from_leaves, + get_merkle_proof, + get_merkle_root, +) +from tests.phase0.helpers import ( + build_attestation_data, + build_deposit_data, + build_empty_block_for_next_slot, + force_registry_change_at_next_epoch, + get_valid_proposer_slashing, + privkeys, + pubkeys, +) + + +# mark entire file as 'sanity' +pytestmark = pytest.mark.sanity + + +def test_slot_transition(state): + test_state = deepcopy(state) + cache_state(test_state) + advance_slot(test_state) + assert test_state.slot == state.slot + 1 + assert get_state_root(test_state, state.slot) == state.hash_tree_root() + return test_state + + +def test_empty_block_transition(state): + test_state = deepcopy(state) + + block = build_empty_block_for_next_slot(test_state) + state_transition(test_state, block) + + assert len(test_state.eth1_data_votes) == len(state.eth1_data_votes) + 1 + assert get_block_root(test_state, state.slot) == block.previous_block_root + + return state, [block], test_state + + +def test_skipped_slots(state): + test_state = deepcopy(state) + block = build_empty_block_for_next_slot(test_state) + block.slot += 3 + + state_transition(test_state, block) + + assert test_state.slot == block.slot + for slot in range(state.slot, test_state.slot): + assert get_block_root(test_state, slot) == block.previous_block_root + + return state, [block], test_state + + +def test_empty_epoch_transition(state): + test_state = deepcopy(state) + block = build_empty_block_for_next_slot(test_state) + block.slot += spec.SLOTS_PER_EPOCH + + state_transition(test_state, block) + + assert test_state.slot == block.slot + for slot in range(state.slot, test_state.slot): + assert get_block_root(test_state, slot) == block.previous_block_root + + return state, [block], test_state + + +def test_empty_epoch_transition_not_finalizing(state): + test_state = deepcopy(state) + block = build_empty_block_for_next_slot(test_state) + block.slot += spec.SLOTS_PER_EPOCH * 5 + + state_transition(test_state, block) + + assert test_state.slot == block.slot + assert test_state.finalized_epoch < get_current_epoch(test_state) - 4 + + 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].initiated_exit + assert not state.validator_registry[validator_index].slashed + + slashed_validator = test_state.validator_registry[validator_index] + assert not slashed_validator.initiated_exit + 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_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_DEPOSIT_AMOUNT) + + item = hash(deposit_data.serialize()) + 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, + deposit_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_DEPOSIT_AMOUNT + 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_DEPOSIT_AMOUNT // 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 = hash(deposit_data.serialize()) + 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, + deposit_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): + test_state = deepcopy(state) + slot = state.slot + shard = state.current_shuffling_start_shard + attestation_data = build_attestation_data(state, slot, shard) + + crosslink_committees = get_crosslink_committees_at_slot(state, slot) + crosslink_committee = [committee for committee, _shard in crosslink_committees if _shard == attestation_data.shard][0] + + committee_size = len(crosslink_committee) + bitfield_length = (committee_size + 7) // 8 + aggregation_bitfield = b'\x01' + b'\x00' * (bitfield_length - 1) + custody_bitfield = b'\x00' * bitfield_length + attestation = Attestation( + aggregation_bitfield=aggregation_bitfield, + data=attestation_data, + custody_bitfield=custody_bitfield, + aggregate_signature=EMPTY_SIGNATURE, + ) + participants = get_attestation_participants( + test_state, + attestation.data, + attestation.aggregation_bitfield, + ) + assert len(participants) == 1 + + validator_index = participants[0] + privkey = privkeys[validator_index] + + message_hash = AttestationDataAndCustodyBit( + data=attestation.data, + custody_bit=0b0, + ).hash_tree_root() + + attestation.aggregation_signature = bls.sign( + message_hash=message_hash, + privkey=privkey, + domain=get_domain( + fork=test_state.fork, + epoch=get_current_epoch(test_state), + domain_type=spec.DOMAIN_ATTESTATION, + ) + ) + + # + # 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.validator_registry, + 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 + # artificially trigger registry update at next epoch transition + force_registry_change_at_next_epoch(pre_state) + + post_state = deepcopy(pre_state) + + voluntary_exit = VoluntaryExit( + epoch=get_current_epoch(pre_state), + validator_index=validator_index, + signature=EMPTY_SIGNATURE, + ) + voluntary_exit.signature = bls.sign( + message_hash=signed_root(voluntary_exit), + privkey=privkeys[validator_index], + domain=get_domain( + fork=pre_state.fork, + epoch=get_current_epoch(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 not pre_state.validator_registry[validator_index].initiated_exit + assert post_state.validator_registry[validator_index].initiated_exit + 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_no_exit_churn_too_long_since_change(state): + pre_state = deepcopy(state) + validator_index = get_active_validator_indices( + pre_state.validator_registry, + get_current_epoch(pre_state) + )[-1] + + # + # setup pre_state + # + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit + pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + # artificially trigger registry update at next epoch transition + force_registry_change_at_next_epoch(pre_state) + # make epochs since registry update greater than LATEST_SLASHED_EXIT_LENGTH + pre_state.validator_registry_update_epoch = ( + get_current_epoch(pre_state) - spec.LATEST_SLASHED_EXIT_LENGTH + ) + # set validator to have previously initiated exit + pre_state.validator_registry[validator_index].initiated_exit = True + + post_state = deepcopy(pre_state) + + # + # Process registry change but ensure no exit + # + 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_update_epoch == get_current_epoch(post_state) - 1 + assert post_state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH + + return pre_state, [block], post_state + + +def test_transfer(state): + pre_state = deepcopy(state) + current_epoch = get_current_epoch(pre_state) + sender_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[-1] + recipient_index = get_active_validator_indices(pre_state.validator_registry, 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, + signature=EMPTY_SIGNATURE, + ) + transfer.signature = bls.sign( + message_hash=signed_root(transfer), + privkey=transfer_privkey, + domain=get_domain( + fork=pre_state.fork, + epoch=get_current_epoch(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_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_ejection(state): + pre_state = deepcopy(state) + + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[-1] + + assert pre_state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH + + # set validator balance to below ejection threshold + set_balance(pre_state, validator_index, spec.EJECTION_BALANCE - 1) + + post_state = deepcopy(pre_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].initiated_exit == True + + return pre_state, [block], post_state + + +def test_historical_batch(state): + pre_state = deepcopy(state) + pre_state.slot += spec.SLOTS_PER_HISTORICAL_ROOT - (pre_state.slot % spec.SLOTS_PER_HISTORICAL_ROOT) - 1 + + post_state = deepcopy(pre_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(pre_state.historical_roots) + 1 + + return pre_state, [block], post_state diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/utils/phase0/__init__.py b/utils/phase0/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/utils/phase0/bls_stub.py b/utils/phase0/bls_stub.py new file mode 100644 index 000000000..108c4ef71 --- /dev/null +++ b/utils/phase0/bls_stub.py @@ -0,0 +1,12 @@ + + +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' * 96 diff --git a/utils/phase0/hash_function.py b/utils/phase0/hash_function.py new file mode 100644 index 000000000..21e6555bf --- /dev/null +++ b/utils/phase0/hash_function.py @@ -0,0 +1,7 @@ +# 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/utils/phase0/jsonize.py b/utils/phase0/jsonize.py new file mode 100644 index 000000000..816192ec6 --- /dev/null +++ b/utils/phase0/jsonize.py @@ -0,0 +1,52 @@ +from .minimal_ssz import hash_tree_root + + +def jsonize(value, typ, include_hash_tree_roots=False): + if isinstance(typ, str) and typ[:4] == 'uint': + return value + elif typ == 'bool': + assert value in (True, False) + return value + elif isinstance(typ, list): + return [jsonize(element, typ[0], include_hash_tree_roots) for element in value] + elif isinstance(typ, str) and typ[:4] == 'byte': + return '0x' + value.hex() + elif hasattr(typ, 'fields'): + ret = {} + for field, subtype in typ.fields.items(): + ret[field] = jsonize(getattr(value, field), subtype, include_hash_tree_roots) + if include_hash_tree_roots: + ret[field + "_hash_tree_root"] = '0x' + hash_tree_root(getattr(value, field), subtype).hex() + if include_hash_tree_roots: + ret["hash_tree_root"] = '0x' + hash_tree_root(value, typ).hex() + return ret + else: + print(value, typ) + raise Exception("Type not recognized") + + +def dejsonize(json, typ): + if isinstance(typ, str) and typ[:4] == 'uint': + return json + elif typ == 'bool': + assert json in (True, False) + return json + elif isinstance(typ, list): + return [dejsonize(element, typ[0]) for element in json] + elif isinstance(typ, str) and typ[:4] == 'byte': + return bytes.fromhex(json[2:]) + elif hasattr(typ, 'fields'): + temp = {} + for field, subtype in typ.fields.items(): + temp[field] = dejsonize(json[field], subtype) + if field + "_hash_tree_root" in json: + assert(json[field + "_hash_tree_root"][2:] == + hash_tree_root(temp[field], subtype).hex()) + ret = typ(**temp) + if "hash_tree_root" in json: + assert(json["hash_tree_root"][2:] == + hash_tree_root(ret, typ).hex()) + return ret + else: + print(json, typ) + raise Exception("Type not recognized") diff --git a/utils/phase0/merkle_minimal.py b/utils/phase0/merkle_minimal.py new file mode 100644 index 000000000..7c5483de3 --- /dev/null +++ b/utils/phase0/merkle_minimal.py @@ -0,0 +1,30 @@ +from .hash_function import hash + + +zerohashes = [b'\x00' * 32] +for layer in range(1, 32): + zerohashes.append(hash(zerohashes[layer - 1] + zerohashes[layer - 1])) + + +# Compute a Merkle root of a right-zerobyte-padded 2**32 sized tree +def calc_merkle_tree_from_leaves(values): + values = list(values) + tree = [values[::]] + for h in range(32): + if len(values) % 2 == 1: + values.append(zerohashes[h]) + values = [hash(values[i] + values[i + 1]) for i in range(0, len(values), 2)] + tree.append(values[::]) + return tree + + +def get_merkle_root(values): + return calc_merkle_tree_from_leaves(values)[-1][0] + + +def get_merkle_proof(tree, item_index): + proof = [] + for i in range(32): + subindex = (item_index // 2**i) ^ 1 + proof.append(tree[i][subindex] if subindex < len(tree[i]) else zerohashes[i]) + return proof diff --git a/utils/phase0/minimal_ssz.py b/utils/phase0/minimal_ssz.py new file mode 100644 index 000000000..c4828d08f --- /dev/null +++ b/utils/phase0/minimal_ssz.py @@ -0,0 +1,213 @@ +from .hash_function import hash + + +BYTES_PER_CHUNK = 32 +BYTES_PER_LENGTH_PREFIX = 4 +ZERO_CHUNK = b'\x00' * BYTES_PER_CHUNK + + +def SSZType(fields): + class SSZObject(): + def __init__(self, **kwargs): + for f in fields: + if f not in kwargs: + raise Exception("Missing constructor argument: %s" % f) + setattr(self, f, kwargs[f]) + + def __eq__(self, other): + return ( + self.fields == other.fields and + self.serialize() == other.serialize() + ) + + def __hash__(self): + return int.from_bytes(self.hash_tree_root(), byteorder="little") + + def __str__(self): + output = [] + for field in self.fields: + output.append(f'{field}: {getattr(self, field)}') + return "\n".join(output) + + def serialize(self): + return serialize_value(self, self.__class__) + + def hash_tree_root(self): + return hash_tree_root(self, self.__class__) + + SSZObject.fields = fields + return SSZObject + + +class Vector(): + def __init__(self, items): + self.items = items + self.length = len(items) + + def __getitem__(self, key): + return self.items[key] + + def __setitem__(self, key, value): + self.items[key] = value + + def __iter__(self): + return iter(self.items) + + def __len__(self): + return self.length + + +def is_basic(typ): + return isinstance(typ, str) and (typ[:4] in ('uint', 'bool') or typ == 'byte') + + +def is_constant_sized(typ): + if is_basic(typ): + return True + elif isinstance(typ, list) and len(typ) == 1: + return is_constant_sized(typ[0]) + elif isinstance(typ, list) and len(typ) == 2: + return False + elif isinstance(typ, str) and typ[:5] == 'bytes': + return len(typ) > 5 + elif hasattr(typ, 'fields'): + for subtype in typ.fields.values(): + if not is_constant_sized(subtype): + return False + return True + else: + raise Exception("Type not recognized") + + +def coerce_to_bytes(x): + if isinstance(x, str): + o = x.encode('utf-8') + assert len(o) == len(x) + return o + elif isinstance(x, bytes): + return x + else: + raise Exception("Expecting bytes") + + +def serialize_value(value, typ=None): + if typ is None: + typ = infer_type(value) + if isinstance(typ, str) and typ[:4] == 'uint': + length = int(typ[4:]) + assert length in (8, 16, 32, 64, 128, 256) + return value.to_bytes(length // 8, 'little') + elif typ == 'bool': + assert value in (True, False) + return b'\x01' if value is True else b'\x00' + elif (isinstance(typ, list) and len(typ) == 1) or typ == 'bytes': + serialized_bytes = coerce_to_bytes(value) if typ == 'bytes' else b''.join([serialize_value(element, typ[0]) for element in value]) + assert len(serialized_bytes) < 2**(8 * BYTES_PER_LENGTH_PREFIX) + serialized_length = len(serialized_bytes).to_bytes(BYTES_PER_LENGTH_PREFIX, 'little') + return serialized_length + serialized_bytes + elif isinstance(typ, list) and len(typ) == 2: + assert len(value) == typ[1] + return b''.join([serialize_value(element, typ[0]) for element in value]) + elif isinstance(typ, str) and len(typ) > 5 and typ[:5] == 'bytes': + assert len(value) == int(typ[5:]), (value, int(typ[5:])) + return coerce_to_bytes(value) + elif hasattr(typ, 'fields'): + serialized_bytes = b''.join([serialize_value(getattr(value, field), subtype) for field, subtype in typ.fields.items()]) + if is_constant_sized(typ): + return serialized_bytes + else: + assert len(serialized_bytes) < 2**(8 * BYTES_PER_LENGTH_PREFIX) + serialized_length = len(serialized_bytes).to_bytes(BYTES_PER_LENGTH_PREFIX, 'little') + return serialized_length + serialized_bytes + else: + print(value, typ) + raise Exception("Type not recognized") + + +def chunkify(bytez): + bytez += b'\x00' * (-len(bytez) % BYTES_PER_CHUNK) + return [bytez[i:i + 32] for i in range(0, len(bytez), 32)] + + +def pack(values, subtype): + return chunkify(b''.join([serialize_value(value, subtype) for value in values])) + + +def is_power_of_two(x): + return x > 0 and x & (x - 1) == 0 + + +def merkleize(chunks): + tree = chunks[::] + while not is_power_of_two(len(tree)): + tree.append(ZERO_CHUNK) + tree = [ZERO_CHUNK] * len(tree) + tree + for i in range(len(tree) // 2 - 1, 0, -1): + tree[i] = hash(tree[i * 2] + tree[i * 2 + 1]) + return tree[1] + + +def mix_in_length(root, length): + return hash(root + length.to_bytes(32, 'little')) + + +def infer_type(value): + if hasattr(value.__class__, 'fields'): + return value.__class__ + elif isinstance(value, Vector): + return [infer_type(value[0]) if len(value) > 0 else 'uint64', len(value)] + elif isinstance(value, list): + return [infer_type(value[0])] if len(value) > 0 else ['uint64'] + elif isinstance(value, (bytes, str)): + return 'bytes' + elif isinstance(value, int): + return 'uint64' + else: + raise Exception("Failed to infer type") + + +def hash_tree_root(value, typ=None): + if typ is None: + typ = infer_type(value) + if is_basic(typ): + return merkleize(pack([value], typ)) + elif isinstance(typ, list) and len(typ) == 1 and is_basic(typ[0]): + return mix_in_length(merkleize(pack(value, typ[0])), len(value)) + elif isinstance(typ, list) and len(typ) == 1 and not is_basic(typ[0]): + return mix_in_length(merkleize([hash_tree_root(element, typ[0]) for element in value]), len(value)) + elif isinstance(typ, list) and len(typ) == 2 and is_basic(typ[0]): + assert len(value) == typ[1] + return merkleize(pack(value, typ[0])) + elif typ == 'bytes': + return mix_in_length(merkleize(chunkify(coerce_to_bytes(value))), len(value)) + elif isinstance(typ, str) and typ[:5] == 'bytes' and len(typ) > 5: + assert len(value) == int(typ[5:]) + return merkleize(chunkify(coerce_to_bytes(value))) + elif isinstance(typ, list) and len(typ) == 2 and not is_basic(typ[0]): + return merkleize([hash_tree_root(element, typ[0]) for element in value]) + elif hasattr(typ, 'fields'): + return merkleize([hash_tree_root(getattr(value, field), subtype) for field, subtype in typ.fields.items()]) + else: + raise Exception("Type not recognized") + + +def truncate(container): + field_keys = list(container.fields.keys()) + truncated_fields = { + key: container.fields[key] + for key in field_keys[:-1] + } + truncated_class = SSZType(truncated_fields) + kwargs = { + field: getattr(container, field) + for field in field_keys[:-1] + } + return truncated_class(**kwargs) + + +def signed_root(container): + return hash_tree_root(truncate(container)) + + +def serialize(ssz_object): + return getattr(ssz_object, 'serialize')() diff --git a/utils/phase0/state_transition.py b/utils/phase0/state_transition.py new file mode 100644 index 000000000..88c4f934a --- /dev/null +++ b/utils/phase0/state_transition.py @@ -0,0 +1,113 @@ +from . import spec + + +from typing import ( # noqa: F401 + Any, + Callable, + List, + NewType, + Tuple, +) + +from .spec import ( + BeaconState, + BeaconBlock, +) + + +def expected_deposit_count(state: BeaconState) -> int: + return min( + spec.MAX_DEPOSITS, + state.latest_eth1_data.deposit_count - state.deposit_index + ) + + +def process_transaction_type(state: BeaconState, + transactions: List[Any], + max_transactions: int, + tx_fn: Callable[[BeaconState, Any], None]) -> None: + assert len(transactions) <= max_transactions + for transaction in transactions: + tx_fn(state, transaction) + + +def process_transactions(state: BeaconState, block: BeaconBlock) -> None: + process_transaction_type( + state, + block.body.proposer_slashings, + spec.MAX_PROPOSER_SLASHINGS, + spec.process_proposer_slashing, + ) + + process_transaction_type( + state, + block.body.attester_slashings, + spec.MAX_ATTESTER_SLASHINGS, + spec.process_attester_slashing, + ) + + process_transaction_type( + state, + block.body.attestations, + spec.MAX_ATTESTATIONS, + spec.process_attestation, + ) + + assert len(block.body.deposits) == expected_deposit_count(state) + process_transaction_type( + state, + block.body.deposits, + spec.MAX_DEPOSITS, + spec.process_deposit, + ) + + process_transaction_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_transaction_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_transactions(state, block) + if verify_state_root: + spec.verify_block_state_root(state, block) + + +def process_epoch_transition(state: BeaconState) -> None: + spec.update_justification_and_finalization(state) + spec.process_crosslinks(state) + spec.maybe_reset_eth1_period(state) + spec.apply_rewards(state) + spec.process_ejections(state) + spec.update_registry_and_shuffling_data(state) + spec.process_slashings(state) + spec.process_exit_queue(state) + spec.finish_epoch_update(state) + + +def state_transition(state: BeaconState, + block: BeaconBlock, + verify_state_root: bool=False) -> BeaconState: + while state.slot < block.slot: + spec.cache_state(state) + if (state.slot + 1) % spec.SLOTS_PER_EPOCH == 0: + process_epoch_transition(state) + spec.advance_slot(state) + if block.slot == state.slot: + process_block(state, block, verify_state_root)