diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index f2452a37d..ea8edc15b 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -61,6 +61,10 @@ - [`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) - [`get_split_offset`](#get_split_offset) - [`get_epoch_committee_count`](#get_epoch_committee_count) @@ -116,7 +120,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) @@ -129,7 +133,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) @@ -183,7 +187,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 | @@ -202,8 +206,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 +257,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 @@ -417,7 +421,6 @@ The types are defined topologically to aid in facilitating an executable version 'signature': 'bytes96', } ``` - #### `Validator` ```python @@ -436,6 +439,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' } ``` @@ -596,7 +601,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 @@ -762,12 +767,45 @@ 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: int) -> int: + return state.balances[index] +``` + +### `set_balance` + +```python +def set_balance(state: BeaconState, index: int, balance: int) -> None: + 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: int, delta: int) -> None: + set_balance(state, index, get_balance(state, index) + delta) +``` + +### `decrease_balance` + +```python +def decrease_balance(state: BeaconState, index: int, delta: int) -> None: + cur_balance = get_balance(state, index) + set_balance(state, index, cur_balance - delta if cur_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 @@ -791,8 +829,12 @@ def get_permuted_index(index: int, list_size: int, seed: Bytes32) -> int: ### `get_split_offset` ```python -def get_split_offset(list_length: int, split_count: int, index: int) -> int: - return (list_length * index) // 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` @@ -1091,7 +1133,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` @@ -1168,7 +1210,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): @@ -1178,9 +1220,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): @@ -1335,14 +1374,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 @@ -1403,8 +1445,8 @@ def slash_validator(state: BeaconState, index: ValidatorIndex) -> None: 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 ``` @@ -1525,7 +1567,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 @@ -1640,9 +1682,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 ) @@ -2033,10 +2078,10 @@ def apply_rewards(state: BeaconState) -> None: deltas1 = get_justification_and_finalization_deltas(state) deltas2 = get_crosslink_deltas(state) for i in range(len(state.validator_registry)): - state.validator_balances[i] = max( + set_balance(state, i, max( 0, - state.validator_balances[i] + deltas1[0][i] + deltas2[0][i] - deltas1[1][i] - deltas2[1][i] - ) + get_balance(state, i) + deltas1[0][i] + deltas2[0][i] - deltas1[1][i] - deltas2[1][i] + )) ``` #### Ejections @@ -2050,7 +2095,7 @@ 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: + if get_balance(state, index) < EJECTION_BALANCE: initiate_validator_exit(state, index) ``` @@ -2093,7 +2138,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: @@ -2178,7 +2223,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 @@ -2484,12 +2529,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 @@ -2511,9 +2556,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/tests/phase0/block_processing/test_process_block_header.py b/tests/phase0/block_processing/test_process_block_header.py index 4ec7e336f..4981b656c 100644 --- a/tests/phase0/block_processing/test_process_block_header.py +++ b/tests/phase0/block_processing/test_process_block_header.py @@ -4,6 +4,8 @@ import pytest from build.phase0.spec import ( get_beacon_proposer_index, + cache_state, + advance_slot, process_block_header, ) from tests.phase0.helpers import ( @@ -14,13 +16,56 @@ from tests.phase0.helpers import ( 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): - pre_state = deepcopy(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(pre_state) - proposer_index = get_beacon_proposer_index(pre_state, block.slot) - pre_state.validator_registry[proposer_index].slashed = True - with pytest.raises(AssertionError): - process_block_header(pre_state, block) + block = build_empty_block_for_next_slot(state) - return state, [block], None + 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 index 297ad37f1..9f1b6add6 100644 --- a/tests/phase0/block_processing/test_process_deposit.py +++ b/tests/phase0/block_processing/test_process_deposit.py @@ -5,6 +5,7 @@ import build.phase0.spec as spec from build.phase0.spec import ( Deposit, + get_balance, process_deposit, ) from tests.phase0.helpers import ( @@ -38,8 +39,9 @@ def test_success(state, deposit_data_leaves, pubkeys, privkeys): process_deposit(post_state, deposit) assert len(post_state.validator_registry) == len(state.validator_registry) + 1 - assert len(post_state.validator_balances) == len(state.validator_balances) + 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 @@ -62,16 +64,16 @@ def test_success_top_up(state, deposit_data_leaves, pubkeys, privkeys): pre_state.latest_eth1_data.deposit_root = root pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves) - pre_balance = pre_state.validator_balances[validator_index] + 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.validator_balances) == len(state.validator_balances) + assert len(post_state.balances) == len(state.balances) assert post_state.deposit_index == post_state.latest_eth1_data.deposit_count - assert post_state.validator_balances[validator_index] == pre_balance + amount + assert get_balance(post_state, validator_index) == pre_balance + amount return pre_state, deposit, post_state diff --git a/tests/phase0/test_sanity.py b/tests/phase0/test_sanity.py index 91bd9fe7a..ec03fb355 100644 --- a/tests/phase0/test_sanity.py +++ b/tests/phase0/test_sanity.py @@ -21,6 +21,7 @@ from build.phase0.spec import ( # functions get_active_validator_indices, get_attestation_participants, + get_balance, get_block_root, get_crosslink_committees_at_slot, get_current_epoch, @@ -28,6 +29,7 @@ from build.phase0.spec import ( get_state_root, advance_slot, cache_state, + set_balance, verify_merkle_branch, hash, ) @@ -168,7 +170,7 @@ def test_proposer_slashing(state, pubkeys, privkeys): assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH # lost whistleblower reward - assert test_state.validator_balances[validator_index] < state.validator_balances[validator_index] + assert get_balance(test_state, validator_index) < get_balance(state, validator_index) return state, [block], test_state @@ -203,7 +205,8 @@ def test_deposit_in_block(state, deposit_data_leaves, pubkeys, privkeys): state_transition(post_state, block) assert len(post_state.validator_registry) == len(state.validator_registry) + 1 - assert len(post_state.validator_balances) == len(state.validator_balances) + 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 @@ -238,12 +241,12 @@ def test_deposit_top_up(state, pubkeys, privkeys, deposit_data_leaves): block = build_empty_block_for_next_slot(pre_state) block.body.deposits.append(deposit) - pre_balance = pre_state.validator_balances[validator_index] + 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.validator_balances) == len(pre_state.validator_balances) - assert post_state.validator_balances[validator_index] == pre_balance + amount + 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 @@ -412,8 +415,8 @@ def test_transfer(state, pubkeys, privkeys): recipient_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] transfer_pubkey = pubkeys[-1] transfer_privkey = privkeys[-1] - amount = pre_state.validator_balances[sender_index] - pre_transfer_recipient_balance = pre_state.validator_balances[recipient_index] + 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, @@ -448,8 +451,8 @@ def test_transfer(state, pubkeys, privkeys): block.body.transfers.append(transfer) state_transition(post_state, block) - sender_balance = post_state.validator_balances[sender_index] - recipient_balance = post_state.validator_balances[recipient_index] + 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 @@ -465,7 +468,7 @@ def test_ejection(state): assert pre_state.validator_registry[validator_index].exit_epoch == spec.FAR_FUTURE_EPOCH # set validator balance to below ejection threshold - pre_state.validator_balances[validator_index] = spec.EJECTION_BALANCE - 1 + set_balance(pre_state, validator_index, spec.EJECTION_BALANCE - 1) post_state = deepcopy(pre_state) #