diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index ccd337a9b4..a9f5150ea6 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -50,7 +50,7 @@ - [`ForkData`](#forkdata) - [Ethereum 1.0 deposit contract](#ethereum-10-deposit-contract) - [Deposit arguments](#deposit-arguments) - - [`Deposit` logs](#deposit-logs) + - [`Eth1Deposit` logs](#eth1deposit-logs) - [`ChainStart` log](#chainstart-log) - [Vyper code](#vyper-code) - [Beacon chain processing](#beacon-chain-processing) @@ -71,12 +71,14 @@ - [`bytes1`, `bytes2`, ...](#bytes1-bytes2-) - [`get_effective_balance`](#get_effective_balance) - [`get_new_validator_registry_delta_chain_tip`](#get_new_validator_registry_delta_chain_tip) + - [`get_fork_version`](#get_fork_version) - [`get_domain`](#get_domain) - [`verify_casper_votes`](#verify_casper_votes) - [`integer_squareroot`](#integer_squareroot) + - [`BLSVerify`](#blsverify) - [On startup](#on-startup) - - [Routine for activating a validator](#routine-for-activating-a-validator) - - [Routine for exiting a validator](#routine-for-exiting-a-validator) + - [Routine for processing deposits](#routine-for-processing-deposits) + - [Routine for updating validator status](#routine-for-updating-validator-status) - [Per-slot processing](#per-slot-processing) - [Proposer signature](#proposer-signature) - [RANDAO](#randao) @@ -575,9 +577,9 @@ The deposit contract has a single `deposit` function which takes as argument a S We recommend the private key corresponding to `withdrawal_pubkey` be stored in cold storage until a withdrawal is required. -### `Deposit` logs +### `Eth1Deposit` logs -Every deposit, of size between `MIN_DEPOSIT` and `MAX_DEPOSIT`, emits a `Deposit` log for consumption by the beacon chain. The deposit contract does little validation, pushing most of the validator onboarding logic to the beacon chain. In particular, the proof of possession (a BLS12-381 signature) is not verified by the deposit contract. +Every deposit, of size between `MIN_DEPOSIT` and `MAX_DEPOSIT`, emits an `Eth1Deposit` log for consumption by the beacon chain. The deposit contract does little validation, pushing most of the validator onboarding logic to the beacon chain. In particular, the proof of possession (a BLS12-381 signature) is not verified by the deposit contract. ### `ChainStart` log @@ -585,7 +587,7 @@ When sufficiently many full deposits have been made the deposit contract emits t * `genesis_time` equals `time` in the `ChainStart` log * `processed_pow_receipt_root` equals `receipt_root` in the `ChainStart` log -* `initial_validator_entries` is built according to the `Deposit` logs up to the deposit that triggered the `ChainStart` log, processed in the order in which they were emitted (oldest to newest) +* `initial_validator_deposits` is a list of `Deposit` objects built according to the `Eth1Deposit` logs up to the deposit that triggered the `ChainStart` log, processed in the order in which they were emitted (oldest to newest) ### Vyper code @@ -597,7 +599,7 @@ CHAIN_START_FULL_DEPOSIT_THRESHOLD: constant(uint256) = 16384 # 2**14 DEPOSIT_CONTRACT_TREE_DEPTH: constant(uint256) = 32 SECONDS_PER_DAY: constant(uint256) = 86400 -Deposit: event({previous_receipt_root: bytes32, data: bytes[2064], deposit_count: uint256}) +Eth1Deposit: event({previous_receipt_root: bytes32, data: bytes[2064], deposit_count: uint256}) ChainStart: event({receipt_root: bytes32, time: bytes[8]}) receipt_tree: bytes32[uint256] @@ -615,7 +617,7 @@ def deposit(deposit_parameters: bytes[2048]): timestamp_bytes8: bytes[8] = slice(concat("", convert(block.timestamp, bytes32)), start=24, len=8) deposit_data: bytes[2064] = concat(msg_gwei_bytes8, timestamp_bytes8, deposit_parameters) - log.Deposit(self.receipt_tree[1], deposit_data, self.deposit_count) + log.Eth1Deposit(self.receipt_tree[1], deposit_data, self.deposit_count) # add deposit to merkle tree self.receipt_tree[index] = sha3(deposit_data) @@ -959,6 +961,17 @@ def get_new_validator_registry_delta_chain_tip(current_validator_registry_delta_ ) ``` +#### `get_fork_version` + +```python +def get_fork_version(fork_data: ForkData, + slot: int) -> int: + if slot < fork_data.fork_slot: + return fork_data.pre_fork_version + else: + return fork_data.post_fork_version +``` + #### `get_domain` ```python @@ -998,6 +1011,10 @@ def integer_squareroot(n: int) -> int: return x ``` +#### `BLSVerify` + +`BLSVerify` is a function for verifying a BLS12-381 signature, defined in the [BLS Verification spec](https://github.com/ethereum/eth2.0-specs/blob/master/specs/bls_verify.md). + ### On startup A valid block with slot `INITIAL_SLOT_NUMBER` (a "genesis block") has the following values. Other validity rules (e.g. requiring a signature) do not apply. @@ -1025,33 +1042,9 @@ A valid block with slot `INITIAL_SLOT_NUMBER` (a "genesis block") has the follow `STARTUP_STATE_ROOT` is the root of the initial state, computed by running the following code: ```python -def on_startup(initial_validator_entries: List[Any], +def on_startup(initial_validator_deposits: List[Deposit], genesis_time: int, processed_pow_receipt_root: Hash32) -> BeaconState: - # Activate validators - initial_validator_registry = [] - for pubkey, deposit, proof_of_possession, withdrawal_credentials, randao_commitment in initial_validator_entries: - initial_validator_registry, _ = get_new_validators( - current_validators=initial_validator_registry, - fork_data=ForkData( - pre_fork_version=INITIAL_FORK_VERSION, - post_fork_version=INITIAL_FORK_VERSION, - fork_slot=INITIAL_SLOT_NUMBER, - ), - pubkey=pubkey, - deposit=deposit, - proof_of_possession=proof_of_possession, - withdrawal_credentials=withdrawal_credentials, - randao_commitment=randao_commitment, - status=ACTIVE, - current_slot=INITIAL_SLOT_NUMBER, - ) - - # Setup state - initial_shuffling = get_new_shuffling(ZERO_HASH, initial_validator_registry, 0) - active_validator_indices = get_active_validator_indices(initial_validator_registry) - initial_persistent_committees = split(shuffle(active_validator_indices, ZERO_HASH), SHARD_COUNT) - state = BeaconState( # Misc slot=INITIAL_SLOT_NUMBER, @@ -1063,7 +1056,7 @@ def on_startup(initial_validator_entries: List[Any], ), # Validator registry - validator_registry=initial_validator_registry, + validator_registry=[], validator_registry_latest_change_slot=INITIAL_SLOT_NUMBER, validator_registry_exit_count=0, validator_registry_delta_chain_tip=ZERO_HASH, @@ -1071,8 +1064,8 @@ def on_startup(initial_validator_entries: List[Any], # Randomness and committees randao_mix=ZERO_HASH, next_seed=ZERO_HASH, - shard_committees_at_slots=initial_shuffling + initial_shuffling, - persistent_committees=initial_persistent_committees, + shard_committees_at_slots=[], + persistent_committees=[], persistent_committee_reassignments=[], # Finality @@ -1092,14 +1085,33 @@ def on_startup(initial_validator_entries: List[Any], candidate_pow_receipt_roots=[], ) + # handle initial deposits and activations + for deposit in initial_validator_deposits: + validator_index = process_deposit( + state=state, + pubkey=deposit.deposit_data.deposit_parameters.pubkey, + deposit=deposit.deposit_data.value, + proof_of_possession=deposit.deposit_data.deposit_parameters.proof_of_possession, + withdrawal_credentials=deposit.deposit_data.deposit_parameters.withdrawal_credentials, + randao_commitment=deposit.deposit_data.deposit_parameters.randao_commitment + ) + if state.validator_registry[index].balance >= MAX_DEPOSIT: + update_validator_status(state, index, ACTIVE) + + # set initial committee shuffling + initial_shuffling = get_new_shuffling(ZERO_HASH, initial_validator_registry, 0) + state.shard_committees_at_slots = initial_shuffling + initial_shuffling + + # set initial persistent shuffling + active_validator_indices = get_active_validator_indices(state.validator_registry) + state.persistent_committees = split(shuffle(active_validator_indices, ZERO_HASH), SHARD_COUNT) + return state ``` -### Routine for activating a validator - -This routine should be run for every [validator](#dfn-validator) that is activated as part of a log created on Ethereum 1.0 [TODO: explain where to check for these logs]. The status of the [validators](#dfn-validator) added after genesis is `PENDING_ACTIVATION`. These logs should be processed in the order in which they are emitted by Ethereum 1.0. +### Routine for processing deposits -First, some helper functions: +First, a helper function: ```python def min_empty_validator_index(validators: List[ValidatorRecord], current_slot: int) -> int: @@ -1107,35 +1119,33 @@ def min_empty_validator_index(validators: List[ValidatorRecord], current_slot: i if v.balance == 0 and v.latest_status_change_slot + ZERO_BALANCE_VALIDATOR_TTL <= current_slot: return i return None +``` -def get_fork_version(fork_data: ForkData, - slot: int) -> int: - if slot < fork_data.fork_slot: - return fork_data.pre_fork_version - else: - return fork_data.post_fork_version +Now, to add a [validator](#dfn-validator) or top up an existing [validator](#dfn-validator)'s balance by some `deposit` amount: -def get_new_validators(validators: List[ValidatorRecord], - fork_data: ForkData, - pubkey: int, - deposit: int, - proof_of_possession: bytes, - withdrawal_credentials: Hash32, - randao_commitment: Hash32, - status: int, - current_slot: int) -> Tuple[List[ValidatorRecord], int]: +```python +def process_deposit(state: BeaconState, + pubkey: int, + deposit: int, + proof_of_possession: bytes, + withdrawal_credentials: Hash32, + randao_commitment: Hash32, + status: int) -> int: + """ + Process a deposit from Ethereum 1.0. + Note that this function mutates ``state``. + """ assert BLSVerify( pub=pubkey, msg=hash(bytes32(pubkey) + withdrawal_credentials + randao_commitment), sig=proof_of_possession, domain=get_domain( - fork_data, - current_slot, + state.fork_data, + state.slot, DOMAIN_DEPOSIT ) ) - validators_copy = copy.deepcopy(validators) - validator_pubkeys = [v.pubkey for v in validators_copy] + validator_pubkeys = [v.pubkey for v in state.validator_registry] if pubkey not in validator_pubkeys: # Add new validator @@ -1145,85 +1155,101 @@ def get_new_validators(validators: List[ValidatorRecord], randao_commitment=randao_commitment, randao_skips=0, balance=deposit, - status=status, - latest_status_change_slot=current_slot, + status=PENDING_ACTIVATION, + latest_status_change_slot=state.slot, exit_count=0 ) index = min_empty_validator_index(validators_copy) if index is None: - validators_copy.append(validator) + state.validator_registry.append(validator) index = len(validators_copy) - 1 else: - validators_copy[index] = validator + state.validator_registry[index] = validator else: # Increase balance by deposit index = validator_pubkeys.index(pubkey) - validator = validators_copy[index] + validator = state.validator_registry[index] assert validator.withdrawal_credentials == withdrawal_credentials validator.balance += deposit - return validators_copy, index + return index ``` -`BLSVerify` is a function for verifying a BLS12-381 signature, defined in the [BLS12-381 spec](https://github.com/ethereum/eth2.0-specs/blob/master/specs/bls_verify.md). -Now, to add a [validator](#dfn-validator) or top up an existing [validator](#dfn-validator)'s balance: +### Routine for updating validator status ```python -def process_deposit(state: BeaconState, - pubkey: int, - deposit: int, - proof_of_possession: bytes, - withdrawal_credentials: Hash32, - randao_commitment: Hash32, - status: int) -> int: +def update_validator_status(state: BeaconState, + index: int, + new_status: int) -> None: """ - Process a deposit from Ethereum 1.0. + Update the validator status with the given ``index`` to ``new_status``. + Handle other general accounting related to this status update. Note that this function mutates ``state``. """ - state.validator_registry, index = get_new_validators( - current_validators=state.validator_registry, - fork_data=ForkData( - pre_fork_version=state.fork_data.pre_fork_version, - post_fork_version=state.fork_data.post_fork_version, - fork_slot=state.fork_data.fork_slot, - ), - pubkey=pubkey, - deposit=deposit, - proof_of_possession=proof_of_possession, - withdrawal_credentials=withdrawal_credentials, - randao_commitment=randao_commitment, - status=status, - current_slot=state.slot, - ) + if new_status == ACTIVE: + activate_validator(state, index) + if new_status == ACTIVE_PENDING_EXIT: + initiate_validator_exit(state, index) + if new_status in [EXITED_WITH_PENALTY, EXITED_WITHOUT_PENALTY]: + exit_validator(state, index, new_status) +``` - return index +The following are helpers and should only be called via `update_validator_status`: + +```python +def activate_validator(state: BeaconState, + index: int) -> None: + """ + Activate the validator with the given ``index``. + Note that this function mutates ``state``. + """ + if validator.status != PENDING_ACTIVATION: + return + + validator = state.validator_registry[index] + validator.status = ACTIVE + validator.latest_status_change_slot = state.slot + state.validator_registry_delta_chain_tip = get_new_validator_registry_delta_chain_tip( + validator_registry_delta_chain_tip=validator_registry_delta_chain_tip, + index=index, + pubkey=validator.pubkey, + flag=ACTIVATION, + ) ``` -### Routine for exiting a validator +```python +def initiate_validator_exit(state: BeaconState, + index: int) -> None: + """ + Initiate exit for the validator with the given ``index``. + Note that this function mutates ``state``. + """ + if validator.status != ACTIVE: + return + + validator = state.validator_registry[index] + validator.status = ACTIVE_PENDING_EXIT + validator.latest_status_change_slot = state.slot +``` ```python -def exit_validator(index: int, - state: BeaconState, - new_status: bool) -> None: +def exit_validator(state: BeaconState, + index: int, + new_status: int) -> None: """ Exit the validator with the given ``index``. Note that this function mutates ``state``. """ - state.validator_registry_exit_count += 1 - validator = state.validator_registry[index] + prev_status = validator.status + + if prev_status == EXITED_WITH_PENALTY: + return + validator.status = new_status validator.latest_status_change_slot = state.slot - validator.exit_count = state.validator_registry_exit_count - - # Remove validator from persistent committees - for committee in state.persistent_committees: - for i, validator_index in committee: - if validator_index == index: - committee.pop(i) - break if new_status == EXITED_WITH_PENALTY: state.latest_penalized_exit_balances[state.slot // COLLECTIVE_PENALTY_CALCULATION_PERIOD] += get_effective_balance(validator) @@ -1233,12 +1259,25 @@ def exit_validator(index: int, whistleblower.balance += whistleblower_reward validator.balance -= whistleblower_reward + if prev_status == EXITED_WITHOUT_PENALTY + return + + # The following updates only occur if not previous exited + state.validator_registry_exit_count += 1 + validator.exit_count = state.validator_registry_exit_count state.validator_registry_delta_chain_tip = get_new_validator_registry_delta_chain_tip( validator_registry_delta_chain_tip=state.validator_registry_delta_chain_tip, index=index, pubkey=validator.pubkey, - flag=EXIT, + flag=EXIT ) + + # Remove validator from persistent committees + for committee in state.persistent_committees: + for i, validator_index in committee: + if validator_index == index: + committee.pop(i) + break ``` ## Per-slot processing @@ -1296,7 +1335,7 @@ For each `proposer_slashing` in `block.body.proposer_slashings`: * Verify that `proposer_slashing.proposal_data_1.shard == proposer_slashing.proposal_data_2.shard`. * Verify that `proposer_slashing.proposal_data_1.block_hash != proposer_slashing.proposal_data_2.block_hash`. * Verify that `proposer.status != EXITED_WITH_PENALTY`. -* Run `exit_validator(proposer_slashing.proposer_index, state, new_status=EXITED_WITH_PENALTY)`. +* Run `update_validator_status(state, proposer_slashing.proposer_index, new_status=EXITED_WITH_PENALTY)`. #### Casper slashings @@ -1311,7 +1350,7 @@ For each `casper_slashing` in `block.body.casper_slashings`: * Let `intersection = [x for x in indices(casper_slashing.votes_1) if x in indices(casper_slashing.votes_2)]`. * Verify that `len(intersection) >= 1`. * Verify that `casper_slashing.votes_1.data.justified_slot + 1 < casper_slashing.votes_2.data.justified_slot + 1 == casper_slashing.votes_2.data.slot < casper_slashing.votes_1.data.slot` or `casper_slashing.votes_1.data.slot == casper_slashing.votes_2.data.slot`. -* For each [validator](#dfn-validator) index `i` in `intersection`, if `state.validator_registry[i].status` does not equal `EXITED_WITH_PENALTY`, then run `exit_validator(i, state, new_status=EXITED_WITH_PENALTY)` +* For each [validator](#dfn-validator) index `i` in `intersection`, if `state.validator_registry[i].status` does not equal `EXITED_WITH_PENALTY`, then run `update_validator_status(state, i, new_status=EXITED_WITH_PENALTY)` #### Attestations @@ -1335,6 +1374,8 @@ For each `attestation` in `block.body.attestations`: Verify that `len(block.body.deposits) <= MAX_DEPOSITS`. +[TODO: add logic to ensure that deposits from 1.0 chain are processed in order] + For each `deposit` in `block.body.deposits`: * Let `serialized_deposit_data` be the serialized form of `deposit.deposit_data`. It should be the `DepositParameters` followed by 8 bytes for `deposit_data.value` and 8 bytes for `deposit_data.timestamp`. That is, it should match `deposit_data` in the [Ethereum 1.0 deposit contract](#ethereum-10-chain-deposit-contract) of which the hash was placed into the Merkle tree. @@ -1361,8 +1402,7 @@ process_deposit( deposit=deposit.deposit_data.value, proof_of_possession=deposit.deposit_data.deposit_parameters.proof_of_possession, withdrawal_credentials=deposit.deposit_data.deposit_parameters.withdrawal_credentials, - randao_commitment=deposit.deposit_data.deposit_parameters.randao_commitment, - status=PENDING_ACTIVATION + randao_commitment=deposit.deposit_data.deposit_parameters.randao_commitment ) ``` @@ -1377,7 +1417,7 @@ For each `exit` in `block.body.exits`: * Verify that `validator.status == ACTIVE`. * Verify that `state.slot >= exit.slot`. * Verify that `state.slot >= validator.latest_status_change_slot + SHARD_PERSISTENT_COMMITTEE_CHANGE_PERIOD`. -* Run `exit_validator(validator_index, state, new_status=ACTIVE_PENDING_EXIT)`. +* Run `update_validator_status(state, validator_index, new_status=ACTIVE_PENDING_EXIT)`. ### Ejections @@ -1389,9 +1429,9 @@ def process_ejections(state: BeaconState) -> None: Iterate through the validator registry and eject active validators with balance below ``EJECTION_BALANCE``. """ - for i, v in enumerate(state.validator_registry): - if is_active_validator(v) and v.balance < EJECTION_BALANCE: - exit_validator(i, state, new_status=EXITED_WITHOUT_PENALTY) + for index, validator in enumerate(state.validator_registry): + if is_active_validator(validor) and validator.balance < EJECTION_BALANCE: + update_validator_status(state, index, new_status=EXITED_WITHOUT_PENALTY) ``` ## Per-epoch processing @@ -1511,30 +1551,8 @@ def update_validator_registry(state: BeaconState) -> None: Update validator registry. Note that this function mutates ``state``. """ - state.validator_registry, state.latest_penalized_exit_balances, state.validator_registry_delta_chain_tip = get_updated_validator_registry( - state.validator_registry, - state.latest_penalized_exit_balances, - state.validator_registry_delta_chain_tip, - state.slot - ) -``` - -which utilizes the following helper - -```python -def get_updated_validator_registry(validator_registry: List[ValidatorRecord], - latest_penalized_exit_balances: List[int], - validator_registry_delta_chain_tip: int, - current_slot: int) -> Tuple[List[ValidatorRecord], List[int], int]: - """ - Returns the validator registry, as well as ``latest_penalized_exit_balances`` and ``validator_registry_delta_chain_tip``. - """ - # make copies to prevent mutating inputs - validator_registry = copy.deepcopy(state.validator_registry) - latest_penalized_exit_balances = copy.deepcopy(latest_penalized_exit_balances) - # The active validators - active_validator_indices = get_active_validator_indices(validator_registry) + active_validator_indices = get_active_validator_indices(state.validator_registry) # The total effective balance of active validators total_balance = sum([get_effective_balance(v) for i, v in enumerate(validator_registry) if i in active_validator_indices]) @@ -1546,7 +1564,7 @@ def get_updated_validator_registry(validator_registry: List[ValidatorRecord], # Activate validators within the allowable balance churn balance_churn = 0 - for i, validator in enumerate(validator_registry): + for index, validator in enumerate(state.validator_registry): if validator.status == PENDING_ACTIVATION and validator.balance >= MAX_DEPOSIT: # Check the balance churn would be within the allowance balance_churn += get_effective_balance(validator) @@ -1554,18 +1572,11 @@ def get_updated_validator_registry(validator_registry: List[ValidatorRecord], break # Activate validator - validator.status = ACTIVE - validator.latest_status_change_slot = current_slot - validator_registry_delta_chain_tip = get_new_validator_registry_delta_chain_tip( - validator_registry_delta_chain_tip=validator_registry_delta_chain_tip, - index=i, - pubkey=validator.pubkey, - flag=ACTIVATION, - ) + update_validator_status(state, index, new_status=ACTIVE) # Exit validators within the allowable balance churn balance_churn = 0 - for i, validator in enumerate(validator_registry): + for index, validator in enumerate(state.validator_registry): if validator.status == ACTIVE_PENDING_EXIT: # Check the balance churn would be within the allowance balance_churn += get_effective_balance(validator) @@ -1573,14 +1584,8 @@ def get_updated_validator_registry(validator_registry: List[ValidatorRecord], break # Exit validator - validator.status = EXITED_WITHOUT_PENALTY - validator.latest_status_change_slot = current_slot - validator_registry_delta_chain_tip = get_new_validator_registry_delta_chain_tip( - validator_registry_delta_chain_tip=validator_registry_delta_chain_tip, - index=i, - pubkey=validator.pubkey, - flag=EXIT, - ) + update_validator_status(state, index, new_status=EXITED_WITHOUT_PENALTY) + # Calculate the total ETH that has been penalized in the last ~2-3 withdrawal periods period_index = current_slot // COLLECTIVE_PENALTY_CALCULATION_PERIOD