From 22f21db7329885de985399a3b057357e4865a5dc Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 20 Mar 2019 17:07:20 +0000 Subject: [PATCH 01/18] Create 1_shard-data-chains-validator.md Text moved away from `1_shard-data-chains.md` (see #812 rewrite). --- .../1_shard-data-chains-validator.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 specs/validator/1_shard-data-chains-validator.md diff --git a/specs/validator/1_shard-data-chains-validator.md b/specs/validator/1_shard-data-chains-validator.md new file mode 100644 index 0000000000..2e3fc7911a --- /dev/null +++ b/specs/validator/1_shard-data-chains-validator.md @@ -0,0 +1,70 @@ +# Ethereum 2.0 Phase 1 -- Honest Validator + +__NOTICE__: This document is a work-in-progress for researchers and implementers. This is an accompanying document to [Ethereum 2.0 Phase 0 -- The Beacon Chain](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md) that describes the expected actions of a "validator" participating in the Ethereum 2.0 protocol. + +## Table of Contents + + + +- [Ethereum 2.0 Phase 0 -- Honest Validator](#ethereum-20-phase-0----honest-validator) + - [Table of Contents](#table-of-contents) + - [Introduction](#introduction) + - [Constants](#constants) + - [Time parameters](#time-parameters) + - [Crosslink data root](#crosslink-data-root) + + + +## Introduction + +This document represents the expected behavior of an "honest validator" with respect to Phase 1 of the Ethereum 2.0 protocol. + +## Constants + +### Time parameters + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `CROSSLINK_LOOKBACK` | 2**5 (= 32) | slots | 3.2 minutes | + +## Crosslink data root + +A node should only sign an `attestation` if `attestation.crosslink_data_root` has been reccursively verified for availability using `attestation.previous_crosslink.crosslink_data_root` up to genesis where `crosslink_data_root == ZERO_HASH`. + +Let `store` be the store of observed block headers and bodies and let `get_shard_block_header(store, slot)` and `get_shard_block_body(store, slot)` return the canonical shard block header and body at the specified `slot`. The expected `get_shard_block_body` is then computed as: + +```python +def compute_crosslink_data_root(state: BeaconState, store: Store) -> Bytes32: + start_slot = state.latest_crosslinks[shard].epoch * SLOTS_PER_EPOCH + SLOTS_PER_EPOCH - CROSSLINK_LOOKBACK + end_slot = attestation.data.slot - attestation.data.slot % SLOTS_PER_EPOCH - CROSSLINK_LOOKBACK + + headers = [] + bodies = [] + for slot in range(start_slot, end_slot): + headers = get_shard_block_header(store, slot) + bodies = get_shard_block_body(store, slot) + + return hash( + merkle_root(pad_to_power_of_2([ + merkle_root_of_bytes(zpad(serialize(header), BYTES_PER_SHARD_BLOCK)) for header in headers + ])) + + merkle_root(pad_to_power_of_2([ + merkle_root_of_bytes(body) for body in bodies + ])) + ) +``` + +using the following helpers: + +```python +def is_power_of_two(value: int) -> bool: + return (value > 0) and (value & (value - 1) == 0) + +def pad_to_power_of_2(values: List[bytes]) -> List[bytes]: + while not is_power_of_two(len(values)): + values += [b'\x00' * BYTES_PER_SHARD_BLOCK] + return values + +def merkle_root_of_bytes(data: bytes) -> bytes: + return merkle_root([data[i:i + 32] for i in range(0, len(data), 32)]) +``` From 3f4260518f99dded2f977cb92582fb9cea6ab947 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 22 Mar 2019 10:33:19 +0000 Subject: [PATCH 02/18] Update and rename 1_shard-data-chains-validator.md to 1_custody.md --- specs/validator/1_custody.md | 551 ++++++++++++++++++ .../1_shard-data-chains-validator.md | 70 --- 2 files changed, 551 insertions(+), 70 deletions(-) create mode 100644 specs/validator/1_custody.md delete mode 100644 specs/validator/1_shard-data-chains-validator.md diff --git a/specs/validator/1_custody.md b/specs/validator/1_custody.md new file mode 100644 index 0000000000..7a11fb0074 --- /dev/null +++ b/specs/validator/1_custody.md @@ -0,0 +1,551 @@ +# Ethereum 2.0 Phase 1 -- Custody + +**NOTICE**: This document is a work-in-progress for researchers and implementers. It reflects recent spec changes and takes precedence over the [Python proof-of-concept implementation](https://github.com/ethereum/beacon_chain). + +## Table of contents + + + +- [Ethereum 2.0 Phase 1 -- Custody](#ethereum-20-phase-1----custody) + - [Table of contents](#table-of-contents) + - [Introduction](#introduction) + - [Constants](#constants) + - [Misc](#misc) + - [Time parameters](#time-parameters) + - [Max transactions per block](#max-transactions-per-block) + - [Signature domains](#signature-domains) + - [Data structures](#data-structures) + - [Phase 0 object updates](#phase-0-object-updates) + - [`Validator`](#validator) + - [`BeaconBlockBody`](#beaconblockbody) + - [`BeaconState`](#beaconstate) + - [Custody objects](#custody-objects) + - [`DataChallenge`](#datachallenge) + - [`DataChallengeRecord`](#datachallengerecord) + - [`CustodyChallenge`](#custodychallenge) + - [`CustodyChallengeRecord`](#custodychallengerecord) + - [`BranchResponse`](#branchresponse) + - [`SubkeyReveal`](#subkeyreveal) + - [Helpers](#helpers) + - [`get_attestation_crosslink_length`](#get_attestation_crosslink_length) + - [`get_mix_length_from_attestation`](#get_mix_length_from_attestation) + - [`epoch_to_custody_period`](#epoch_to_custody_period) + - [`slot_to_custody_period`](#slot_to_custody_period) + - [`get_current_custody_period`](#get_current_custody_period) + - [`verify_custody_subkey_reveal`](#verify_custody_subkey_reveal) + - [`slash_validator`](#slash_validator) + - [Per-block processing](#per-block-processing) + - [Transactions](#transactions) + - [Data challenges](#data-challenges) + - [Subkey reveals](#subkey-reveals) + - [Custody challenges](#custody-challenges) + - [Branch responses](#branch-responses) + - [Per-epoch processing](#per-epoch-processing) + - [One-time phase 1 initiation transition](#one-time-phase-1-initiation-transition) + + + +## Introduction + +This document details the beacon chain additions and changes in Phase 1 of Ethereum 2.0 to support custody, building upon the [phase 0](0_beacon-chain.md) specification. + +## Constants + +### Misc + +| Name | Value | +| - | - | +| `BYTES_PER_SHARD_BLOCK` | `2**14` (= 16,384) | +| `BYTES_PER_MIX_CHUNK` | `2**9` (= 512) | +| `MINOR_REWARD_QUOTIENT` | `2**8` (= 256) | +| `EMPTY_PUBKEY` | `int_to_bytes48(0)` | + +### Time parameters + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `MAX_DATA_CHALLENGE_DELAY` | 2**11 (= 2,048) | epochs | ~9 days | +| `CUSTODY_PERIOD_LENGTH` | 2**11 (= 2,048) | epochs | ~9 days | +| `PERSISTENT_COMMITTEE_PERIOD` | 2**11 (= 2,048) | epochs | ~9 days | +| `CHALLENGE_RESPONSE_DEADLINE` | 2**14 (= 16,384) | epochs | ~73 days | + +### Max transactions per block + +| Name | Value | +| - | - | +| `MAX_DATA_CHALLENGES` | `2**2` (= 4) | +| `MAX_CUSTODY_CHALLENGES` | `2**1` (= 2) | +| `MAX_BRANCH_RESPONSES` | `2**5` (= 32) | +| `MAX_EARLY_SUBKEY_REVEALS` | `2**4` (= 16) | + +### Signature domains + +| Name | Value | +| - | - | +| `DOMAIN_SHARD_PROPOSER` | `129` | +| `DOMAIN_SHARD_ATTESTER` | `130` | +| `DOMAIN_CUSTODY_SUBKEY` | `131` | +| `DOMAIN_CUSTODY_CHALLENGE` | `132` | + +## Data structures + +### Phase 0 object updates + +Add the following fields to the end of the specified container objects. + +#### `Validator` + +```python + 'next_subkey_to_reveal': 'uint64', + 'max_reveal_lateness': 'uint64', +``` + +#### `BeaconBlockBody` + +```python + 'data_challenges': [DataChallenge], + 'custody_challenges': [CustodyChallenge], + 'branch_responses': [BranchResponse], + 'subkey_reveals': [SubkeyReveal], +``` + +#### `BeaconState` + +```python + 'data_challenge_records': [DataChallengeRecord], + 'custody_challenge_records': [CustodyChallengeRecord], + 'challenge_index': 'uint64', +``` + +### Custody objects + +#### `DataChallenge` + +```python +{ + 'responder_index': 'uint64', + 'data_index': 'uint64', + 'attestation': SlashableAttestation, +} +``` + +#### `DataChallengeRecord` + +```python +{ + 'challenge_id': 'uint64', + 'challenger_index': 'uint64', + 'responder_index': 'uint64', + 'data_root': 'bytes32', + 'deadline': 'uint64', + 'depth': 'uint64', + 'data_index': 'uint64', +} +``` + +#### `CustodyChallenge` + +```python +{ + 'attestation': SlashableAttestation, + 'challenger_index': 'uint64', + 'responder_index': 'uint64', + 'responder_subkey': 'bytes96', + 'mix': 'bytes', + 'signature': 'bytes96', +} +``` + +#### `CustodyChallengeRecord` + +```python +{ + 'challenge_id': 'uint64', + 'challenger_index': 'uint64', + 'responder_index': 'uint64', + 'data_root': 'bytes32', + 'deadline': 'uint64', + 'challenge_mix': 'bytes', + 'responder_subkey': 'bytes96', +} +``` + +#### `BranchResponse` + +```python +{ + 'challenge_id': 'uint64', + 'data': ['byte', BYTES_PER_MIX_CHUNK], + 'branch': ['bytes32'], + 'data_index': 'uint64', +} +``` + +#### `SubkeyReveal` + +```python +{ + 'revealer_index': 'uint64', + 'period': 'uint64', + 'subkey': 'bytes96', + 'masker_index': 'uint64' + 'mask': 'bytes32', +} +``` + +## Helpers + +### `get_attestation_crosslink_length` + +```python +def get_attestation_crosslink_length(attestation: Attestation) -> int: + start_epoch = attestation.data.latest_crosslink.epoch + end_epoch = slot_to_epoch(attestation.data.slot) + return min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch) +``` + +### `get_mix_length_from_attestation` + +```python +def get_mix_length_from_attestation(attestation: Attestation) -> int: + chunks_per_slot = BYTES_PER_SHARD_BLOCK // BYTES_PER_MIX_CHUNK + return get_attestation_crosslink_length(attestation) * EPOCH_LENGTH * chunks_per_slot +``` + +### `epoch_to_custody_period` + +```python +def epoch_to_custody_period(epoch: Epoch) -> int: + return epoch // CUSTODY_PERIOD_LENGTH +``` + +### `slot_to_custody_period` + +```python +def slot_to_custody_period(slot: Slot) -> int: + return epoch_to_custody_period(slot_to_epoch(slot)) +``` + +### `get_current_custody_period` + +```python +def get_current_custody_period(state: BeaconState) -> int: + return epoch_to_custody_period(get_current_epoch(state)) +``` + +### `verify_custody_subkey_reveal` + +```python +def verify_custody_subkey_reveal(state: BeaconState, + reveal: SubkeyReveal) -> bool + # Case 1: legitimate reveal + pubkeys = [state.validator_registry[reveal.revealer_index].pubkey] + message_hashes = [hash_tree_root(reveal.period)] + + # Case 2: masked punitive early reveal + # Masking prevents proposer stealing the whistleblower reward + # Secure under the aggregate extraction infeasibility assumption + # See pages 11-12 of https://crypto.stanford.edu/~dabo/pubs/papers/aggreg.pdf + if reveal.mask != ZERO_HASH: + pubkeys.append(state.validator_registry[reveal.masker_index].pubkey) + message_hashes.append(reveal.mask) + + return bls_verify_multiple( + pubkeys=pubkeys, + message_hashes=message_hashes, + signature=reveal.subkey, + domain=get_domain( + fork=state.fork, + epoch=reveal.period * CUSTODY_PERIOD_LENGTH, + domain_type=DOMAIN_CUSTODY_SUBKEY, + ), + ) +``` + +### `slash_validator` + +Change the definition of `slash_validator` as follows: + +```python +def slash_validator(state: BeaconState, index: ValidatorIndex, whistleblower_index :ValidatorIndex=None) -> None: + """ + Slash the validator of the given ``index``. + Note that this function mutates ``state``. + """ + exit_validator(state, index) + validator = state.validator_registry[index] + state.latest_slashed_balances[get_current_epoch(state) % LATEST_PENALIZED_EXIT_LENGTH] += get_effective_balance(state, index) + + proposer_index = get_beacon_proposer_index(state, state.slot) + whistleblower_reward = get_effective_balance(state, index) // WHISTLEBLOWER_REWARD_QUOTIENT + if whistleblower_index is None: + increase_balance(state, proposer_index, whistleblower_reward) + else: + proposer_share = whistleblower_reward // INCLUDER_REWARD_QUOTIENT # TODO: Define INCLUDER_REWARD_QUOTIENT + increase_balance(state, proposer_index, proposer_share) + increase_balance(state, whistleblower_index, whistleblower_reward - proposer_share) + + decrease_balance(state, index, whistleblower_reward) + validator.slashed_epoch = get_current_epoch(state) + validator.withdrawable_epoch = get_current_epoch(state) + LATEST_PENALIZED_EXIT_LENGTH +``` + +The only change is that this introduces the possibility of a penalization where the "whistleblower" that takes credit is NOT the block proposer. + +## Per-block processing + +### Transactions + +Add the following transactions to the per-block processing, in order the given below and after all other transactions in phase 0. + +#### Data challenges + +Verify that `len(block.body.data_challenges) <= MAX_DATA_CHALLENGES`. + +For each `challenge` in `block.body.data_challenges`, run the following function: + +```python +def process_data_challenge(state: BeaconState, + challenge: DataChallenge) -> None: + # Check it is not too late to challenge + assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY + assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY + # Check the attestation is valid + assert verify_slashable_attestation(state, challenge.attestation) + # Check the responder participated + assert challenger.responder_index in challenge.attestation.validator_indices + # Check the challenge is not a duplicate + for c in state.data_challenge_records: + assert c.data_root != challenge.attestation.data.crosslink_data_root or c.data_index == challenge.data_index + # Check validity of depth + depth = log2(next_power_of_two(get_mix_length_from_attestation(challenge.attestation))) + assert challenge.data_index < 2**depth + # Add new data challenge record + state.data_challenge_records.append(DataChallengeRecord( + challenge_id=state.challenge_index, + challenger_index=get_beacon_proposer_index(state, state.slot), + data_root=challenge.attestation.data.compute_crosslink_data_root, + depth=depth, + deadline=get_current_epoch(state) + CHALLENGE_RESPONSE_DEADLINE, + data_index=challenge.data_index, + )) + state.challenge_index += 1 +``` + +#### Subkey reveals + +Verify that `len(block.body.early_subkey_reveals) <= MAX_EARLY_SUBKEY_REVEALS`. + +For each `reveal` in `block.body.early_subkey_reveals`, run the following function: + +```python +def process_subkey_reveal(state: BeaconState, + reveal: SubkeyReveal) -> None: + assert verify_custody_subkey_reveal(reveal) + revealer = state.validator_registry[reveal.revealer_index] + + # Case 1: non-early non-punitive non-masked reveal + if reveal.mask == ZERO_HASH: + assert reveal.period == revealer.next_subkey_to_reveal + # Revealer is active or exited + assert is_active_validator(revealer) or revealer.exit_epoch > get_current_epoch(state) + revealer.next_subkey_to_reveal += 1 + revealer.max_reveal_lateness = max(revealer.max_reveal_lateness, get_current_period(state) - reveal.period) + + # Case 2: Early punitive masked reveal + else: + assert reveal.period > get_current_custody_period(state) + assert revealer.slashed is False + slash_validator(state, reveal.revealer_index, reveal.masker_index) + increase_balance(state, reveal.masker_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) + + proposer_index = get_beacon_proposer_index(state, state.slot) + increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) + +``` + +#### Custody challenges + +Verify that `len(block.body.custody_challenges) <= MAX_CUSTODY_CHALLENGES`. + +For each `challenge` in `block.body.custody_challenges`, run the following function: + +```python +def process_custody_challenge(state: BeaconState, + challenge: CustodyChallenge) -> None: + challenger = state.validator_registry[challenge.challenger_index] + responder = state.validator_registry[challenge.responder_index] + # Verify the challenge signature + assert bls_verify( + message_hash=signed_root(challenge), + pubkey=challenger.pubkey, + signature=challenge.signature, + domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_CHALLENGE) + ) + # Verify the challenged attestation + assert verify_slashable_attestation(challenge.attestation, state) + # Check the responder participated in the attestation + assert challenge.responder_index in attestation.validator_indices + # A validator can be the challenger or responder for at most one challenge at a time + for challenge_record in state.custody_challenge_records: + assert challenge_record.challenger_index != challenge.challenger_index + assert challenge_record.responder_index != challenge.responder_index + # Cannot challenge if slashed + assert challenger.slashed is False + # Verify the revealed subkey + assert verify_custody_subkey_reveal(SubkeyReveal( + revealer_index=challenge.responder_index, + period=slot_to_custody_period(attestation.data.slot), + subkey=challenge.responder_subkey, + )) + # Verify that the attestation is still eligible for challenging + min_challengeable_epoch = responder.exit_epoch - CUSTODY_PERIOD_LENGTH * (1 + responder.max_reveal_lateness) + assert min_challengeable_epoch <= slot_to_epoch(challenge.attestation.data.slot) + # Verify the mix's length and that its last bit is the opposite of the custody bit + mix_length = get_mix_length_from_attestation(challenge.attestation) + verify_bitfield(challenge.mix, mix_length) + custody_bit = get_bitfield_bit(attestation.custody_bitfield, attestation.validator_indices.index(responder_index)) + assert custody_bit != get_bitfield_bit(challenge.mix, mix_length - 1) + # Create a new challenge record + state.custody_challenge_records.append(CustodyChallengeRecord( + challenge_id=state.challenge_index, + challenger_index=challenge.challenger_index, + responder_index=challenge.responder_index, + data_root=challenge.attestation.crosslink_data_root, + challenge_mix=challenge.mix, + responder_subkey=responder_subkey, + deadline=get_current_epoch(state) + CHALLENGE_RESPONSE_DEADLINE + )) + state.challenge_index += 1 + # Postpone responder withdrawability + state.validator_registry[responder_index].withdrawable_epoch = FAR_FUTURE_EPOCH +``` + +#### Branch responses + +Verify that `len(block.body.branch_responses) <= MAX_BRANCH_RESPONSES`. + +For each `response` in `block.body.branch_responses`, run the following function: + +```python +def process_branch_response(state: BeaconState, + response: BranchResponse) -> None: + data_challenge = next(c for c in state.data_challenge_records if c.challenge_id == response.challenge_id, None) + if data_challenge is not None: + return process_data_challenge_response(state, response, data_challenge) + + custody_challenge = next(c for c in state.custody_challenge_records if c.challenge_id == response.challenge_id, None) + if custody_challenge is not None: + return process_custody_challenge_response(state, response, custody_challenge) + + assert False +``` + +```python +def process_data_challenge_response(state: BeaconState, + response: BranchResponse, + challenge: DataChallengeRecord) -> None: + assert verify_merkle_branch( + leaf=hash_tree_root(response.data), + branch=response.branch, + depth=challenge.depth, + index=challenge.data_index, + root=challenge.data_root, + ) + # Check data index + assert response.data_index == challenge.data_index + # Must wait at least ENTRY_EXIT_DELAY before responding to a branch challenge + assert get_current_epoch(state) >= challenge.inclusion_epoch + ENTRY_EXIT_DELAY + state.data_challenge_records.remove(challenge) + # Reward the proposer + proposer_index = get_beacon_proposer_index(state, state.slot) + increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) +``` + +A response to a custody challenge proves that a challenger's mix is invalid by pointing to an index where the mix is incorrect. + +```python +def process_custody_challenge_response(state: BeaconState, + response: BranchResponse, + challenge: CustodyChallengeRecord) -> None: + responder = state.validator_registry[challenge.responder_index] + # Check the data index is valid + assert response.data_index < len(challenge.mix) + # Check the provided data is part of the attested data + assert verify_merkle_branch( + leaf=hash_tree_root(response.data), + branch=response.branch, + depth=log2(next_power_of_two(len(challenge.mix))), + index=response.data_index, + root=challenge.data_root, + ) + # Check the mix bit (assert the response identified an invalid data index in the challenge) + mix_bit = get_bitfield_bit(hash(challenge.responder_subkey + response.data), 0) + previous_bit = 0 if response.data_index == 0 else get_bitfield_bit(challenge.mix, response.data_index - 1) + next_bit = get_bitfield_bit(challenge.mix, response.data_index) + assert previous_bit ^ mix_bit != next_bit + # Resolve the challenge in the responder's favor + slash_validator(state, challenge.challenger_index, challenge.responder_index) + state.custody_challenge_records.remove(challenge) +``` + +## Per-epoch processing + +Add the following loop immediately below the `process_ejections` loop: + +```python +def process_challenge_deadlines(state: BeaconState) -> None: + """ + Iterate through the challenges and slash validators that missed their deadline. + """ + for challenge in state.data_challenge_records: + if get_current_epoch(state) > challenge.deadline: + slash_validator(state, challenge.responder_index, challenge.challenger_index) + state.data_challenge_records.remove(challenge) + + for challenge in state.custody_challenge_records: + if get_current_epoch(state) > challenge.deadline: + slash_validator(state, challenge.responder_index, challenge.challenger_index) + state.custody_challenge_records.remove(challenge) + elif get_current_epoch(state) > state.validator_registry[challenge.responder_index].withdrawable_epoch: + slash_validator(state, challenge.challenger_index, challenge.responder_index) + state.custody_challenge_records.remove(challenge) +``` + +In `process_penalties_and_exits`, change the definition of `eligible` to the following (note that it is not a pure function because `state` is declared in the surrounding scope): + +```python +def eligible(index): + validator = state.validator_registry[index] + # Cannot exit if there are still open data challenges + if len([c for c in state.data_challenge_records if c.responder_index == index]) > 0: + return False + # Cannot exit if you have not revealed all of your subkeys + elif validator.next_subkey_to_reveal <= epoch_to_custody_period(validator.exit_epoch): + return False + # Cannot exit if you already have + elif validator.withdrawable_epoch < FAR_FUTURE_EPOCH: + return False + # Return minimum time + else: + return current_epoch >= validator.exit_epoch + MIN_VALIDATOR_WITHDRAWAL_EPOCHS +``` + +## One-time phase 1 initiation transition + +Run the following on the fork block after per-slot processing and before per-block and per-epoch processing. + +For all `validator` in `ValidatorRegistry`, update it to the new format and fill the new member values with: + +```python + 'next_subkey_to_reveal': get_current_custody_period(state), + 'max_reveal_lateness': 0, +``` + +Update the `BeaconState` to the new format and fill the new member values with: + +```python + 'data_challenge_records': [], + 'custody_challenge_records': [], + 'challenge_index': 0, +``` diff --git a/specs/validator/1_shard-data-chains-validator.md b/specs/validator/1_shard-data-chains-validator.md deleted file mode 100644 index 2e3fc7911a..0000000000 --- a/specs/validator/1_shard-data-chains-validator.md +++ /dev/null @@ -1,70 +0,0 @@ -# Ethereum 2.0 Phase 1 -- Honest Validator - -__NOTICE__: This document is a work-in-progress for researchers and implementers. This is an accompanying document to [Ethereum 2.0 Phase 0 -- The Beacon Chain](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md) that describes the expected actions of a "validator" participating in the Ethereum 2.0 protocol. - -## Table of Contents - - - -- [Ethereum 2.0 Phase 0 -- Honest Validator](#ethereum-20-phase-0----honest-validator) - - [Table of Contents](#table-of-contents) - - [Introduction](#introduction) - - [Constants](#constants) - - [Time parameters](#time-parameters) - - [Crosslink data root](#crosslink-data-root) - - - -## Introduction - -This document represents the expected behavior of an "honest validator" with respect to Phase 1 of the Ethereum 2.0 protocol. - -## Constants - -### Time parameters - -| Name | Value | Unit | Duration | -| - | - | :-: | :-: | -| `CROSSLINK_LOOKBACK` | 2**5 (= 32) | slots | 3.2 minutes | - -## Crosslink data root - -A node should only sign an `attestation` if `attestation.crosslink_data_root` has been reccursively verified for availability using `attestation.previous_crosslink.crosslink_data_root` up to genesis where `crosslink_data_root == ZERO_HASH`. - -Let `store` be the store of observed block headers and bodies and let `get_shard_block_header(store, slot)` and `get_shard_block_body(store, slot)` return the canonical shard block header and body at the specified `slot`. The expected `get_shard_block_body` is then computed as: - -```python -def compute_crosslink_data_root(state: BeaconState, store: Store) -> Bytes32: - start_slot = state.latest_crosslinks[shard].epoch * SLOTS_PER_EPOCH + SLOTS_PER_EPOCH - CROSSLINK_LOOKBACK - end_slot = attestation.data.slot - attestation.data.slot % SLOTS_PER_EPOCH - CROSSLINK_LOOKBACK - - headers = [] - bodies = [] - for slot in range(start_slot, end_slot): - headers = get_shard_block_header(store, slot) - bodies = get_shard_block_body(store, slot) - - return hash( - merkle_root(pad_to_power_of_2([ - merkle_root_of_bytes(zpad(serialize(header), BYTES_PER_SHARD_BLOCK)) for header in headers - ])) + - merkle_root(pad_to_power_of_2([ - merkle_root_of_bytes(body) for body in bodies - ])) - ) -``` - -using the following helpers: - -```python -def is_power_of_two(value: int) -> bool: - return (value > 0) and (value & (value - 1) == 0) - -def pad_to_power_of_2(values: List[bytes]) -> List[bytes]: - while not is_power_of_two(len(values)): - values += [b'\x00' * BYTES_PER_SHARD_BLOCK] - return values - -def merkle_root_of_bytes(data: bytes) -> bytes: - return merkle_root([data[i:i + 32] for i in range(0, len(data), 32)]) -``` From f5ccf8eff94ada53943ec1127d564eea0fcf8d44 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 22 Mar 2019 10:44:12 +0000 Subject: [PATCH 03/18] Update 1_custody.md --- specs/validator/1_custody.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/specs/validator/1_custody.md b/specs/validator/1_custody.md index 7a11fb0074..64262eca5f 100644 --- a/specs/validator/1_custody.md +++ b/specs/validator/1_custody.md @@ -58,7 +58,6 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | `BYTES_PER_SHARD_BLOCK` | `2**14` (= 16,384) | | `BYTES_PER_MIX_CHUNK` | `2**9` (= 512) | | `MINOR_REWARD_QUOTIENT` | `2**8` (= 256) | -| `EMPTY_PUBKEY` | `int_to_bytes48(0)` | ### Time parameters @@ -66,7 +65,6 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | - | - | :-: | :-: | | `MAX_DATA_CHALLENGE_DELAY` | 2**11 (= 2,048) | epochs | ~9 days | | `CUSTODY_PERIOD_LENGTH` | 2**11 (= 2,048) | epochs | ~9 days | -| `PERSISTENT_COMMITTEE_PERIOD` | 2**11 (= 2,048) | epochs | ~9 days | | `CHALLENGE_RESPONSE_DEADLINE` | 2**14 (= 16,384) | epochs | ~73 days | ### Max transactions per block @@ -82,10 +80,8 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | | - | - | -| `DOMAIN_SHARD_PROPOSER` | `129` | -| `DOMAIN_SHARD_ATTESTER` | `130` | -| `DOMAIN_CUSTODY_SUBKEY` | `131` | -| `DOMAIN_CUSTODY_CHALLENGE` | `132` | +| `DOMAIN_CUSTODY_SUBKEY` | `6` | +| `DOMAIN_CUSTODY_CHALLENGE` | `7` | ## Data structures From 6274adf4849f357eb3109c9d0f0eb11b8699922c Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 22 Mar 2019 10:46:29 +0000 Subject: [PATCH 04/18] Rename 1_custody.md to 1_custody-game.md --- specs/validator/{1_custody.md => 1_custody-game.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename specs/validator/{1_custody.md => 1_custody-game.md} (100%) diff --git a/specs/validator/1_custody.md b/specs/validator/1_custody-game.md similarity index 100% rename from specs/validator/1_custody.md rename to specs/validator/1_custody-game.md From 66a173b7e83cfeaa7755f2fed8c414bf6dffa7ee Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 22 Mar 2019 10:47:17 +0000 Subject: [PATCH 05/18] Update 1_custody-game.md --- specs/validator/1_custody-game.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/validator/1_custody-game.md b/specs/validator/1_custody-game.md index 64262eca5f..e9a3d65f23 100644 --- a/specs/validator/1_custody-game.md +++ b/specs/validator/1_custody-game.md @@ -1,4 +1,4 @@ -# Ethereum 2.0 Phase 1 -- Custody +# Ethereum 2.0 Phase 1 -- Custody Game **NOTICE**: This document is a work-in-progress for researchers and implementers. It reflects recent spec changes and takes precedence over the [Python proof-of-concept implementation](https://github.com/ethereum/beacon_chain). @@ -6,7 +6,7 @@ -- [Ethereum 2.0 Phase 1 -- Custody](#ethereum-20-phase-1----custody) +- [Ethereum 2.0 Phase 1 -- Custody Game](#ethereum-20-phase-1----custody) - [Table of contents](#table-of-contents) - [Introduction](#introduction) - [Constants](#constants) @@ -47,7 +47,7 @@ ## Introduction -This document details the beacon chain additions and changes in Phase 1 of Ethereum 2.0 to support custody, building upon the [phase 0](0_beacon-chain.md) specification. +This document details the beacon chain additions and changes in Phase 1 of Ethereum 2.0 to support the shard data custody game, building upon the [phase 0](0_beacon-chain.md) specification. ## Constants From d82a6bf715e06b58a4e4ca0d76485d19b01d95fb Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 22 Mar 2019 10:47:33 +0000 Subject: [PATCH 06/18] Update 1_custody-game.md --- specs/validator/1_custody-game.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/validator/1_custody-game.md b/specs/validator/1_custody-game.md index e9a3d65f23..05418d1e45 100644 --- a/specs/validator/1_custody-game.md +++ b/specs/validator/1_custody-game.md @@ -6,7 +6,7 @@ -- [Ethereum 2.0 Phase 1 -- Custody Game](#ethereum-20-phase-1----custody) +- [Ethereum 2.0 Phase 1 -- Custody Game](#ethereum-20-phase-1----custody-game) - [Table of contents](#table-of-contents) - [Introduction](#introduction) - [Constants](#constants) From 231b2eee10d9aea0a19f99b8d64cd031d9b05966 Mon Sep 17 00:00:00 2001 From: Justin Drake Date: Fri, 22 Mar 2019 11:47:25 +0000 Subject: [PATCH 07/18] Move 1_custody-game.md --- specs/{validator => core}/1_custody-game.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename specs/{validator => core}/1_custody-game.md (100%) diff --git a/specs/validator/1_custody-game.md b/specs/core/1_custody-game.md similarity index 100% rename from specs/validator/1_custody-game.md rename to specs/core/1_custody-game.md From fa43725fcc17eeb29a9700bfee4282e0f5e86d56 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 25 Mar 2019 13:34:47 +0000 Subject: [PATCH 08/18] Update 1_custody-game.md --- specs/core/1_custody-game.md | 365 ++++++++++++++++------------------- 1 file changed, 168 insertions(+), 197 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 05418d1e45..876c67fc0c 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -1,6 +1,6 @@ # Ethereum 2.0 Phase 1 -- Custody Game -**NOTICE**: This document is a work-in-progress for researchers and implementers. It reflects recent spec changes and takes precedence over the [Python proof-of-concept implementation](https://github.com/ethereum/beacon_chain). +**NOTICE**: This spec is a work-in-progress for researchers and implementers. ## Table of contents @@ -15,33 +15,31 @@ - [Max transactions per block](#max-transactions-per-block) - [Signature domains](#signature-domains) - [Data structures](#data-structures) - - [Phase 0 object updates](#phase-0-object-updates) + - [Phase 0 updates](#phase-0-updates) - [`Validator`](#validator) - - [`BeaconBlockBody`](#beaconblockbody) - [`BeaconState`](#beaconstate) + - [`BeaconBlockBody`](#beaconblockbody) - [Custody objects](#custody-objects) - [`DataChallenge`](#datachallenge) - [`DataChallengeRecord`](#datachallengerecord) - - [`CustodyChallenge`](#custodychallenge) - - [`CustodyChallengeRecord`](#custodychallengerecord) - - [`BranchResponse`](#branchresponse) - - [`SubkeyReveal`](#subkeyreveal) + - [`MixChallenge`](#mixchallenge) + - [`MixChallengeRecord`](#mixchallengerecord) + - [`CustodyResponse`](#custodyresponse) + - [`CustodyReveal`](#custodyreveal) - [Helpers](#helpers) - - [`get_attestation_crosslink_length`](#get_attestation_crosslink_length) - - [`get_mix_length_from_attestation`](#get_mix_length_from_attestation) + - [`get_attestation_chunk_count`](#get_attestation_chunk_count) - [`epoch_to_custody_period`](#epoch_to_custody_period) - [`slot_to_custody_period`](#slot_to_custody_period) - [`get_current_custody_period`](#get_current_custody_period) - - [`verify_custody_subkey_reveal`](#verify_custody_subkey_reveal) + - [`verify_custody_reveal`](#verify_custody_reveal) - [`slash_validator`](#slash_validator) - [Per-block processing](#per-block-processing) - [Transactions](#transactions) + - [Custody reveals](#custody-reveals) - [Data challenges](#data-challenges) - - [Subkey reveals](#subkey-reveals) - - [Custody challenges](#custody-challenges) - - [Branch responses](#branch-responses) + - [Mix challenges](#mix-challenges) + - [Custody responses](#custody-responses) - [Per-epoch processing](#per-epoch-processing) - - [One-time phase 1 initiation transition](#one-time-phase-1-initiation-transition) @@ -56,7 +54,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | | - | - | | `BYTES_PER_SHARD_BLOCK` | `2**14` (= 16,384) | -| `BYTES_PER_MIX_CHUNK` | `2**9` (= 512) | +| `BYTES_PER_CHUNK` | `2**9` (= 512) | | `MINOR_REWARD_QUOTIENT` | `2**8` (= 256) | ### Time parameters @@ -64,53 +62,53 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | Unit | Duration | | - | - | :-: | :-: | | `MAX_DATA_CHALLENGE_DELAY` | 2**11 (= 2,048) | epochs | ~9 days | -| `CUSTODY_PERIOD_LENGTH` | 2**11 (= 2,048) | epochs | ~9 days | -| `CHALLENGE_RESPONSE_DEADLINE` | 2**14 (= 16,384) | epochs | ~73 days | +| `EPOCHS_PER_CUSTODY_PERIOD` | 2**11 (= 2,048) | epochs | ~9 days | +| `CUSTODY_RESPONSE_DEADLINE` | 2**14 (= 16,384) | epochs | ~73 days | ### Max transactions per block | Name | Value | | - | - | | `MAX_DATA_CHALLENGES` | `2**2` (= 4) | -| `MAX_CUSTODY_CHALLENGES` | `2**1` (= 2) | -| `MAX_BRANCH_RESPONSES` | `2**5` (= 32) | -| `MAX_EARLY_SUBKEY_REVEALS` | `2**4` (= 16) | +| `MAX_MIX_CHALLENGES` | `2**1` (= 2) | +| `MAX_CUSTODY_RESPONSES` | `2**5` (= 32) | +| `MAX_CUSTODY_REVEALS` | `2**4` (= 16) | ### Signature domains | Name | Value | | - | - | -| `DOMAIN_CUSTODY_SUBKEY` | `6` | -| `DOMAIN_CUSTODY_CHALLENGE` | `7` | +| `DOMAIN_CUSTODY_REVEAL` | `6` | +| `DOMAIN_MIX_CHALLENGE` | `7` | ## Data structures -### Phase 0 object updates +### Phase 0 updates -Add the following fields to the end of the specified container objects. +Add the following fields to the end of the specified container objects. Fields of type `uint64` are initialized to `0`, and list fields are initialized to `[]`. #### `Validator` ```python - 'next_subkey_to_reveal': 'uint64', + 'custody_reveals': 'uint64', 'max_reveal_lateness': 'uint64', ``` -#### `BeaconBlockBody` +#### `BeaconState` ```python - 'data_challenges': [DataChallenge], - 'custody_challenges': [CustodyChallenge], - 'branch_responses': [BranchResponse], - 'subkey_reveals': [SubkeyReveal], + 'data_challenge_records': [DataChallengeRecord], + 'mix_challenge_records': [MixChallengeRecord], + 'challenge_index': 'uint64', ``` -#### `BeaconState` +#### `BeaconBlockBody` ```python - 'data_challenge_records': [DataChallengeRecord], - 'custody_challenge_records': [CustodyChallengeRecord], - 'challenge_index': 'uint64', + 'custody_reveals': [CustodyReveal], + 'data_challenges': [DataChallenge], + 'mix_challenges': [MixChallenge], + 'custody_responses': [CustodyResponse], ``` ### Custody objects @@ -119,9 +117,9 @@ Add the following fields to the end of the specified container objects. ```python { - 'responder_index': 'uint64', + 'responder_index': ValidatorIndex, 'data_index': 'uint64', - 'attestation': SlashableAttestation, + 'attestation': Attestation, } ``` @@ -129,90 +127,84 @@ Add the following fields to the end of the specified container objects. ```python { - 'challenge_id': 'uint64', - 'challenger_index': 'uint64', - 'responder_index': 'uint64', - 'data_root': 'bytes32', + 'challenge_index': 'uint64', + 'challenger_index': ValidatorIndex, + 'responder_index': ValidatorIndex, + 'crosslink_data_root': 'bytes32', 'deadline': 'uint64', 'depth': 'uint64', 'data_index': 'uint64', } ``` -#### `CustodyChallenge` +#### `MixChallenge` ```python { - 'attestation': SlashableAttestation, - 'challenger_index': 'uint64', - 'responder_index': 'uint64', - 'responder_subkey': 'bytes96', + 'attestation': Attestation, + 'challenger_index': ValidatorIndex, + 'responder_index': ValidatorIndex, + 'responder_subkey': BLSSignature, 'mix': 'bytes', - 'signature': 'bytes96', + 'signature': BLSSignature, } ``` -#### `CustodyChallengeRecord` +#### `MixChallengeRecord` ```python { - 'challenge_id': 'uint64', - 'challenger_index': 'uint64', - 'responder_index': 'uint64', - 'data_root': 'bytes32', - 'deadline': 'uint64', - 'challenge_mix': 'bytes', - 'responder_subkey': 'bytes96', + 'challenge_index': 'uint64', + 'challenger_index': ValidatorIndex, + 'responder_index': ValidatorIndex, + 'crosslink_data_root': Hash, + 'deadline': Epoch, + 'mix': 'bytes', + 'responder_subkey': BLSSignature, } ``` -#### `BranchResponse` +#### `CustodyResponse` ```python { - 'challenge_id': 'uint64', - 'data': ['byte', BYTES_PER_MIX_CHUNK], - 'branch': ['bytes32'], + 'challenge_index': 'uint64', + 'data': ['byte', BYTES_PER_CHUNK], + 'branch': [Hash], 'data_index': 'uint64', } ``` -#### `SubkeyReveal` +#### `CustodyReveal` ```python { - 'revealer_index': 'uint64', + 'revealer_index': ValidatorIndex, 'period': 'uint64', - 'subkey': 'bytes96', - 'masker_index': 'uint64' + 'subkey': BLSSignature, + 'masker_index': ValidatorIndex, 'mask': 'bytes32', } ``` ## Helpers -### `get_attestation_crosslink_length` - -```python -def get_attestation_crosslink_length(attestation: Attestation) -> int: - start_epoch = attestation.data.latest_crosslink.epoch - end_epoch = slot_to_epoch(attestation.data.slot) - return min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch) -``` - -### `get_mix_length_from_attestation` +### `get_attestation_chunk_count` ```python -def get_mix_length_from_attestation(attestation: Attestation) -> int: - chunks_per_slot = BYTES_PER_SHARD_BLOCK // BYTES_PER_MIX_CHUNK - return get_attestation_crosslink_length(attestation) * EPOCH_LENGTH * chunks_per_slot +def get_attestation_chunk_count(attestation: Attestation) -> int: + attestation_start_epoch = attestation.data.latest_crosslink.epoch + attestation_end_epoch = slot_to_epoch(attestation.data.slot) + attestation_crosslink_length = min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch) + chunks_per_epoch = 2 * BYTES_PER_SHARD_BLOCK * EPOCH_LENGTH // BYTES_PER_CHUNK + return attestation_crosslink_length * chunks_per_epoch ``` ### `epoch_to_custody_period` ```python def epoch_to_custody_period(epoch: Epoch) -> int: - return epoch // CUSTODY_PERIOD_LENGTH + return epoch // EPOCHS_PER_CUSTODY_PERIOD ``` ### `slot_to_custody_period` @@ -229,11 +221,11 @@ def get_current_custody_period(state: BeaconState) -> int: return epoch_to_custody_period(get_current_epoch(state)) ``` -### `verify_custody_subkey_reveal` +### `verify_custody_reveal` ```python -def verify_custody_subkey_reveal(state: BeaconState, - reveal: SubkeyReveal) -> bool +def verify_custody_reveal(state: BeaconState, + reveal: CustodyReveal) -> bool # Case 1: legitimate reveal pubkeys = [state.validator_registry[reveal.revealer_index].pubkey] message_hashes = [hash_tree_root(reveal.period)] @@ -252,8 +244,8 @@ def verify_custody_subkey_reveal(state: BeaconState, signature=reveal.subkey, domain=get_domain( fork=state.fork, - epoch=reveal.period * CUSTODY_PERIOD_LENGTH, - domain_type=DOMAIN_CUSTODY_SUBKEY, + epoch=reveal.period * EPOCHS_PER_CUSTODY_PERIOD, + domain_type=DOMAIN_CUSTODY_REVEAL, ), ) ``` @@ -263,7 +255,7 @@ def verify_custody_subkey_reveal(state: BeaconState, Change the definition of `slash_validator` as follows: ```python -def slash_validator(state: BeaconState, index: ValidatorIndex, whistleblower_index :ValidatorIndex=None) -> None: +def slash_validator(state: BeaconState, index: ValidatorIndex, whistleblower_index: ValidatorIndex=None) -> None: """ Slash the validator of the given ``index``. Note that this function mutates ``state``. @@ -294,58 +286,24 @@ The only change is that this introduces the possibility of a penalization where Add the following transactions to the per-block processing, in order the given below and after all other transactions in phase 0. -#### Data challenges - -Verify that `len(block.body.data_challenges) <= MAX_DATA_CHALLENGES`. - -For each `challenge` in `block.body.data_challenges`, run the following function: - -```python -def process_data_challenge(state: BeaconState, - challenge: DataChallenge) -> None: - # Check it is not too late to challenge - assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY - assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY - # Check the attestation is valid - assert verify_slashable_attestation(state, challenge.attestation) - # Check the responder participated - assert challenger.responder_index in challenge.attestation.validator_indices - # Check the challenge is not a duplicate - for c in state.data_challenge_records: - assert c.data_root != challenge.attestation.data.crosslink_data_root or c.data_index == challenge.data_index - # Check validity of depth - depth = log2(next_power_of_two(get_mix_length_from_attestation(challenge.attestation))) - assert challenge.data_index < 2**depth - # Add new data challenge record - state.data_challenge_records.append(DataChallengeRecord( - challenge_id=state.challenge_index, - challenger_index=get_beacon_proposer_index(state, state.slot), - data_root=challenge.attestation.data.compute_crosslink_data_root, - depth=depth, - deadline=get_current_epoch(state) + CHALLENGE_RESPONSE_DEADLINE, - data_index=challenge.data_index, - )) - state.challenge_index += 1 -``` - -#### Subkey reveals +#### Custody reveals -Verify that `len(block.body.early_subkey_reveals) <= MAX_EARLY_SUBKEY_REVEALS`. +Verify that `len(block.body.early_custody_reveals) <= MAX_CUSTODY_REVEALS`. -For each `reveal` in `block.body.early_subkey_reveals`, run the following function: +For each `reveal` in `block.body.early_custody_reveals`, run the following function: ```python -def process_subkey_reveal(state: BeaconState, - reveal: SubkeyReveal) -> None: - assert verify_custody_subkey_reveal(reveal) +def process_custody_reveal(state: BeaconState, + reveal: CustodyReveal) -> None: + assert verify_custody_reveal(reveal) revealer = state.validator_registry[reveal.revealer_index] - # Case 1: non-early non-punitive non-masked reveal + # Case 1: Non-early non-punitive non-masked reveal if reveal.mask == ZERO_HASH: - assert reveal.period == revealer.next_subkey_to_reveal + assert reveal.period == epoch_to_custody_period(revealer.activation_epoch) + revealer.custody_reveals # Revealer is active or exited - assert is_active_validator(revealer) or revealer.exit_epoch > get_current_epoch(state) - revealer.next_subkey_to_reveal += 1 + assert is_active_validator(revealer, get_current_epoch(state)) or revealer.exit_epoch > get_current_epoch(state) + revealer.custody_reveals += 1 revealer.max_reveal_lateness = max(revealer.max_reveal_lateness, get_current_period(state) - reveal.period) # Case 2: Early punitive masked reveal @@ -360,93 +318,130 @@ def process_subkey_reveal(state: BeaconState, ``` -#### Custody challenges +#### Data challenges + +Verify that `len(block.body.data_challenges) <= MAX_DATA_CHALLENGES`. + +For each `challenge` in `block.body.data_challenges`, run the following function: + +```python +def process_data_challenge(state: BeaconState, + challenge: DataChallenge) -> None: + # Verify the attestation + assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation)) + # Verify it is not too late to challenge + assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY + assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY + # Verify the responder participated + assert challenger.responder_index in challenge.attestation.validator_indices + # Verify the challenge is not a duplicate + for record in state.data_challenge_records: + assert ( + record.crosslink_data_root != challenge.attestation.data.crosslink_crosslink_data_root or + record.data_index != challenge.data_index + ) + # Verify depth + depth = log2(next_power_of_two(get_attestation_chunk_count(challenge.attestation))) + assert challenge.data_index < 2**depth + # Add new data challenge record + state.data_challenge_records.append(DataChallengeRecord( + challenge_index=state.challenge_index, + challenger_index=get_beacon_proposer_index(state, state.slot), + crosslink_data_root=challenge.attestation.data.crosslink_crosslink_data_root, + depth=depth, + deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE, + data_index=challenge.data_index, + )) + state.challenge_index += 1 +``` + +#### Mix challenges -Verify that `len(block.body.custody_challenges) <= MAX_CUSTODY_CHALLENGES`. +Verify that `len(block.body.mix_challenges) <= MAX_MIX_CHALLENGES`. -For each `challenge` in `block.body.custody_challenges`, run the following function: +For each `challenge` in `block.body.mix_challenges`, run the following function: ```python -def process_custody_challenge(state: BeaconState, - challenge: CustodyChallenge) -> None: +def process_mix_challenge(state: BeaconState, + challenge: MixChallenge) -> None: + # Verify challenge signature challenger = state.validator_registry[challenge.challenger_index] - responder = state.validator_registry[challenge.responder_index] - # Verify the challenge signature assert bls_verify( message_hash=signed_root(challenge), pubkey=challenger.pubkey, signature=challenge.signature, - domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_CHALLENGE) + domain=get_domain(state, get_current_epoch(state), DOMAIN_MIX_CHALLENGE) ) - # Verify the challenged attestation - assert verify_slashable_attestation(challenge.attestation, state) - # Check the responder participated in the attestation + # Verify the challenger is not slashed + assert challenger.slashed is False + # Verify attestation + assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation)) + # Verify the attestation is eligible for challenging + responder = state.validator_registry[challenge.responder_index] + min_challengeable_epoch = responder.exit_epoch - EPOCHS_PER_CUSTODY_PERIOD * (1 + responder.max_reveal_lateness) + assert min_challengeable_epoch <= slot_to_epoch(challenge.attestation.data.slot) + # Verify the responder participated in the attestation assert challenge.responder_index in attestation.validator_indices # A validator can be the challenger or responder for at most one challenge at a time - for challenge_record in state.custody_challenge_records: + for challenge_record in state.mix_challenge_records: assert challenge_record.challenger_index != challenge.challenger_index assert challenge_record.responder_index != challenge.responder_index - # Cannot challenge if slashed - assert challenger.slashed is False - # Verify the revealed subkey - assert verify_custody_subkey_reveal(SubkeyReveal( + # Verify the responder subkey + assert verify_custody_reveal(CustodyReveal( revealer_index=challenge.responder_index, period=slot_to_custody_period(attestation.data.slot), subkey=challenge.responder_subkey, )) - # Verify that the attestation is still eligible for challenging - min_challengeable_epoch = responder.exit_epoch - CUSTODY_PERIOD_LENGTH * (1 + responder.max_reveal_lateness) - assert min_challengeable_epoch <= slot_to_epoch(challenge.attestation.data.slot) # Verify the mix's length and that its last bit is the opposite of the custody bit - mix_length = get_mix_length_from_attestation(challenge.attestation) + mix_length = get_attestation_chunk_count(challenge.attestation) verify_bitfield(challenge.mix, mix_length) custody_bit = get_bitfield_bit(attestation.custody_bitfield, attestation.validator_indices.index(responder_index)) assert custody_bit != get_bitfield_bit(challenge.mix, mix_length - 1) - # Create a new challenge record - state.custody_challenge_records.append(CustodyChallengeRecord( - challenge_id=state.challenge_index, + # Add new mix challenge record + state.mix_challenge_records.append(MixChallengeRecord( + challenge_index=state.challenge_index, challenger_index=challenge.challenger_index, responder_index=challenge.responder_index, - data_root=challenge.attestation.crosslink_data_root, - challenge_mix=challenge.mix, + crosslink_data_root=challenge.attestation.crosslink_crosslink_data_root, + mix=challenge.mix, responder_subkey=responder_subkey, - deadline=get_current_epoch(state) + CHALLENGE_RESPONSE_DEADLINE + deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE )) state.challenge_index += 1 # Postpone responder withdrawability - state.validator_registry[responder_index].withdrawable_epoch = FAR_FUTURE_EPOCH + responder.withdrawable_epoch = FAR_FUTURE_EPOCH ``` -#### Branch responses +#### Custody responses -Verify that `len(block.body.branch_responses) <= MAX_BRANCH_RESPONSES`. +Verify that `len(block.body.custody_responses) <= MAX_CUSTODY_RESPONSES`. -For each `response` in `block.body.branch_responses`, run the following function: +For each `response` in `block.body.custody_responses`, run the following function: ```python -def process_branch_response(state: BeaconState, - response: BranchResponse) -> None: - data_challenge = next(c for c in state.data_challenge_records if c.challenge_id == response.challenge_id, None) +def process_custody_response(state: BeaconState, + response: CustodyResponse) -> None: + data_challenge = next(c for c in state.data_challenge_records if c.challenge_index == response.challenge_index, None) if data_challenge is not None: return process_data_challenge_response(state, response, data_challenge) - custody_challenge = next(c for c in state.custody_challenge_records if c.challenge_id == response.challenge_id, None) - if custody_challenge is not None: - return process_custody_challenge_response(state, response, custody_challenge) + mix_challenge = next(c for c in state.mix_challenge_records if c.challenge_index == response.challenge_index, None) + if mix_challenge is not None: + return process_mix_challenge_response(state, response, mix_challenge) assert False ``` ```python def process_data_challenge_response(state: BeaconState, - response: BranchResponse, + response: CustodyResponse, challenge: DataChallengeRecord) -> None: assert verify_merkle_branch( leaf=hash_tree_root(response.data), branch=response.branch, depth=challenge.depth, index=challenge.data_index, - root=challenge.data_root, + root=challenge.crosslink_data_root, ) # Check data index assert response.data_index == challenge.data_index @@ -458,12 +453,10 @@ def process_data_challenge_response(state: BeaconState, increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) ``` -A response to a custody challenge proves that a challenger's mix is invalid by pointing to an index where the mix is incorrect. - ```python -def process_custody_challenge_response(state: BeaconState, - response: BranchResponse, - challenge: CustodyChallengeRecord) -> None: +def process_mix_challenge_response(state: BeaconState, + response: CustodyResponse, + challenge: MixChallengeRecord) -> None: responder = state.validator_registry[challenge.responder_index] # Check the data index is valid assert response.data_index < len(challenge.mix) @@ -473,7 +466,7 @@ def process_custody_challenge_response(state: BeaconState, branch=response.branch, depth=log2(next_power_of_two(len(challenge.mix))), index=response.data_index, - root=challenge.data_root, + root=challenge.crosslink_data_root, ) # Check the mix bit (assert the response identified an invalid data index in the challenge) mix_bit = get_bitfield_bit(hash(challenge.responder_subkey + response.data), 0) @@ -482,7 +475,7 @@ def process_custody_challenge_response(state: BeaconState, assert previous_bit ^ mix_bit != next_bit # Resolve the challenge in the responder's favor slash_validator(state, challenge.challenger_index, challenge.responder_index) - state.custody_challenge_records.remove(challenge) + state.mix_challenge_records.remove(challenge) ``` ## Per-epoch processing @@ -491,21 +484,18 @@ Add the following loop immediately below the `process_ejections` loop: ```python def process_challenge_deadlines(state: BeaconState) -> None: - """ - Iterate through the challenges and slash validators that missed their deadline. - """ for challenge in state.data_challenge_records: if get_current_epoch(state) > challenge.deadline: slash_validator(state, challenge.responder_index, challenge.challenger_index) state.data_challenge_records.remove(challenge) - for challenge in state.custody_challenge_records: + for challenge in state.mix_challenge_records: if get_current_epoch(state) > challenge.deadline: slash_validator(state, challenge.responder_index, challenge.challenger_index) - state.custody_challenge_records.remove(challenge) + state.mix_challenge_records.remove(challenge) elif get_current_epoch(state) > state.validator_registry[challenge.responder_index].withdrawable_epoch: slash_validator(state, challenge.challenger_index, challenge.responder_index) - state.custody_challenge_records.remove(challenge) + state.mix_challenge_records.remove(challenge) ``` In `process_penalties_and_exits`, change the definition of `eligible` to the following (note that it is not a pure function because `state` is declared in the surrounding scope): @@ -517,7 +507,7 @@ def eligible(index): if len([c for c in state.data_challenge_records if c.responder_index == index]) > 0: return False # Cannot exit if you have not revealed all of your subkeys - elif validator.next_subkey_to_reveal <= epoch_to_custody_period(validator.exit_epoch): + elif epoch_to_custody_period(revealer.activation_epoch) + validator.custody_reveals <= epoch_to_custody_period(validator.exit_epoch): return False # Cannot exit if you already have elif validator.withdrawable_epoch < FAR_FUTURE_EPOCH: @@ -526,22 +516,3 @@ def eligible(index): else: return current_epoch >= validator.exit_epoch + MIN_VALIDATOR_WITHDRAWAL_EPOCHS ``` - -## One-time phase 1 initiation transition - -Run the following on the fork block after per-slot processing and before per-block and per-epoch processing. - -For all `validator` in `ValidatorRegistry`, update it to the new format and fill the new member values with: - -```python - 'next_subkey_to_reveal': get_current_custody_period(state), - 'max_reveal_lateness': 0, -``` - -Update the `BeaconState` to the new format and fill the new member values with: - -```python - 'data_challenge_records': [], - 'custody_challenge_records': [], - 'challenge_index': 0, -``` From 11e5ffea584efc9577716f920ab4ec11254831f4 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 25 Mar 2019 13:54:17 +0000 Subject: [PATCH 09/18] Update 1_custody-game.md --- specs/core/1_custody-game.md | 59 +++++++++++++++--------------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 876c67fc0c..30d8597130 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -29,7 +29,6 @@ - [Helpers](#helpers) - [`get_attestation_chunk_count`](#get_attestation_chunk_count) - [`epoch_to_custody_period`](#epoch_to_custody_period) - - [`slot_to_custody_period`](#slot_to_custody_period) - [`get_current_custody_period`](#get_current_custody_period) - [`verify_custody_reveal`](#verify_custody_reveal) - [`slash_validator`](#slash_validator) @@ -61,9 +60,9 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | Unit | Duration | | - | - | :-: | :-: | -| `MAX_DATA_CHALLENGE_DELAY` | 2**11 (= 2,048) | epochs | ~9 days | -| `EPOCHS_PER_CUSTODY_PERIOD` | 2**11 (= 2,048) | epochs | ~9 days | -| `CUSTODY_RESPONSE_DEADLINE` | 2**14 (= 16,384) | epochs | ~73 days | +| `MAX_DATA_CHALLENGE_DELAY` | `2**11` (= 2,048) | epochs | ~9 days | +| `EPOCHS_PER_CUSTODY_PERIOD` | `2**11` (= 2,048) | epochs | ~9 days | +| `CUSTODY_RESPONSE_DEADLINE` | `2**14` (= 16,384) | epochs | ~73 days | ### Max transactions per block @@ -85,7 +84,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether ### Phase 0 updates -Add the following fields to the end of the specified container objects. Fields of type `uint64` are initialized to `0`, and list fields are initialized to `[]`. +Add the following fields to the end of the specified container objects. Fields of type `uint64` are initialized to `0` and list fields are initialized to `[]`. #### `Validator` @@ -118,7 +117,7 @@ Add the following fields to the end of the specified container objects. Fields o ```python { 'responder_index': ValidatorIndex, - 'data_index': 'uint64', + 'chunk_index': 'uint64', 'attestation': Attestation, } ``` @@ -130,10 +129,10 @@ Add the following fields to the end of the specified container objects. Fields o 'challenge_index': 'uint64', 'challenger_index': ValidatorIndex, 'responder_index': ValidatorIndex, - 'crosslink_data_root': 'bytes32', 'deadline': 'uint64', + 'crosslink_data_root': 'bytes32', 'depth': 'uint64', - 'data_index': 'uint64', + 'chunk_index': 'uint64', } ``` @@ -157,8 +156,8 @@ Add the following fields to the end of the specified container objects. Fields o 'challenge_index': 'uint64', 'challenger_index': ValidatorIndex, 'responder_index': ValidatorIndex, - 'crosslink_data_root': Hash, 'deadline': Epoch, + 'crosslink_data_root': Hash, 'mix': 'bytes', 'responder_subkey': BLSSignature, } @@ -171,7 +170,7 @@ Add the following fields to the end of the specified container objects. Fields o 'challenge_index': 'uint64', 'data': ['byte', BYTES_PER_CHUNK], 'branch': [Hash], - 'data_index': 'uint64', + 'chunk_index': 'uint64', } ``` @@ -207,13 +206,6 @@ def epoch_to_custody_period(epoch: Epoch) -> int: return epoch // EPOCHS_PER_CUSTODY_PERIOD ``` -### `slot_to_custody_period` - -```python -def slot_to_custody_period(slot: Slot) -> int: - return epoch_to_custody_period(slot_to_epoch(slot)) -``` - ### `get_current_custody_period` ```python @@ -332,17 +324,17 @@ def process_data_challenge(state: BeaconState, # Verify it is not too late to challenge assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY - # Verify the responder participated + # Verify the responder participated in the attestation assert challenger.responder_index in challenge.attestation.validator_indices # Verify the challenge is not a duplicate for record in state.data_challenge_records: assert ( record.crosslink_data_root != challenge.attestation.data.crosslink_crosslink_data_root or - record.data_index != challenge.data_index + record.chunk_index != challenge.chunk_index ) # Verify depth - depth = log2(next_power_of_two(get_attestation_chunk_count(challenge.attestation))) - assert challenge.data_index < 2**depth + depth = math.log2(next_power_of_two(get_attestation_chunk_count(challenge.attestation))) + assert challenge.chunk_index < 2**depth # Add new data challenge record state.data_challenge_records.append(DataChallengeRecord( challenge_index=state.challenge_index, @@ -350,7 +342,7 @@ def process_data_challenge(state: BeaconState, crosslink_data_root=challenge.attestation.data.crosslink_crosslink_data_root, depth=depth, deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE, - data_index=challenge.data_index, + chunk_index=challenge.chunk_index, )) state.challenge_index += 1 ``` @@ -389,7 +381,7 @@ def process_mix_challenge(state: BeaconState, # Verify the responder subkey assert verify_custody_reveal(CustodyReveal( revealer_index=challenge.responder_index, - period=slot_to_custody_period(attestation.data.slot), + period=epoch_to_custody_period(slot_to_epoch(attestation.data.slot)), subkey=challenge.responder_subkey, )) # Verify the mix's length and that its last bit is the opposite of the custody bit @@ -440,11 +432,11 @@ def process_data_challenge_response(state: BeaconState, leaf=hash_tree_root(response.data), branch=response.branch, depth=challenge.depth, - index=challenge.data_index, + index=challenge.chunk_index, root=challenge.crosslink_data_root, ) # Check data index - assert response.data_index == challenge.data_index + assert response.chunk_index == challenge.chunk_index # Must wait at least ENTRY_EXIT_DELAY before responding to a branch challenge assert get_current_epoch(state) >= challenge.inclusion_epoch + ENTRY_EXIT_DELAY state.data_challenge_records.remove(challenge) @@ -455,25 +447,24 @@ def process_data_challenge_response(state: BeaconState, ```python def process_mix_challenge_response(state: BeaconState, - response: CustodyResponse, - challenge: MixChallengeRecord) -> None: - responder = state.validator_registry[challenge.responder_index] + response: CustodyResponse, + challenge: MixChallengeRecord) -> None: # Check the data index is valid - assert response.data_index < len(challenge.mix) + assert response.chunk_index < len(challenge.mix) # Check the provided data is part of the attested data assert verify_merkle_branch( leaf=hash_tree_root(response.data), branch=response.branch, - depth=log2(next_power_of_two(len(challenge.mix))), - index=response.data_index, + depth=math.log2(next_power_of_two(len(challenge.mix))), + index=response.chunk_index, root=challenge.crosslink_data_root, ) # Check the mix bit (assert the response identified an invalid data index in the challenge) mix_bit = get_bitfield_bit(hash(challenge.responder_subkey + response.data), 0) - previous_bit = 0 if response.data_index == 0 else get_bitfield_bit(challenge.mix, response.data_index - 1) - next_bit = get_bitfield_bit(challenge.mix, response.data_index) + previous_bit = 0 if response.chunk_index == 0 else get_bitfield_bit(challenge.mix, response.chunk_index - 1) + next_bit = get_bitfield_bit(challenge.mix, response.chunk_index) assert previous_bit ^ mix_bit != next_bit - # Resolve the challenge in the responder's favor + # Resolve the challenge in favour of the responder slash_validator(state, challenge.challenger_index, challenge.responder_index) state.mix_challenge_records.remove(challenge) ``` From ea0a6c9e8c7f80aa53a500cbe46a07d4d396a1d4 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 25 Mar 2019 14:01:23 +0000 Subject: [PATCH 10/18] Update 1_custody-game.md --- specs/core/1_custody-game.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 30d8597130..74d7ebf47d 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -29,7 +29,6 @@ - [Helpers](#helpers) - [`get_attestation_chunk_count`](#get_attestation_chunk_count) - [`epoch_to_custody_period`](#epoch_to_custody_period) - - [`get_current_custody_period`](#get_current_custody_period) - [`verify_custody_reveal`](#verify_custody_reveal) - [`slash_validator`](#slash_validator) - [Per-block processing](#per-block-processing) @@ -195,7 +194,7 @@ def get_attestation_chunk_count(attestation: Attestation) -> int: attestation_start_epoch = attestation.data.latest_crosslink.epoch attestation_end_epoch = slot_to_epoch(attestation.data.slot) attestation_crosslink_length = min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch) - chunks_per_epoch = 2 * BYTES_PER_SHARD_BLOCK * EPOCH_LENGTH // BYTES_PER_CHUNK + chunks_per_epoch = 2 * BYTES_PER_SHARD_BLOCK * SLOTS_PER_EPOCH // BYTES_PER_CHUNK return attestation_crosslink_length * chunks_per_epoch ``` @@ -206,13 +205,6 @@ def epoch_to_custody_period(epoch: Epoch) -> int: return epoch // EPOCHS_PER_CUSTODY_PERIOD ``` -### `get_current_custody_period` - -```python -def get_current_custody_period(state: BeaconState) -> int: - return epoch_to_custody_period(get_current_epoch(state)) -``` - ### `verify_custody_reveal` ```python @@ -300,7 +292,7 @@ def process_custody_reveal(state: BeaconState, # Case 2: Early punitive masked reveal else: - assert reveal.period > get_current_custody_period(state) + assert reveal.period > epoch_to_custody_period(get_current_epoch(state)) assert revealer.slashed is False slash_validator(state, reveal.revealer_index, reveal.masker_index) increase_balance(state, reveal.masker_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) From fbf8e7b40201655557d887a7b34f7fccea58d956 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 25 Mar 2019 14:53:06 +0000 Subject: [PATCH 11/18] Update 1_custody-game.md --- specs/core/1_custody-game.md | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 74d7ebf47d..4e4fbe1d5b 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -30,7 +30,6 @@ - [`get_attestation_chunk_count`](#get_attestation_chunk_count) - [`epoch_to_custody_period`](#epoch_to_custody_period) - [`verify_custody_reveal`](#verify_custody_reveal) - - [`slash_validator`](#slash_validator) - [Per-block processing](#per-block-processing) - [Transactions](#transactions) - [Custody reveals](#custody-reveals) @@ -234,36 +233,6 @@ def verify_custody_reveal(state: BeaconState, ) ``` -### `slash_validator` - -Change the definition of `slash_validator` as follows: - -```python -def slash_validator(state: BeaconState, index: ValidatorIndex, whistleblower_index: ValidatorIndex=None) -> None: - """ - Slash the validator of the given ``index``. - Note that this function mutates ``state``. - """ - exit_validator(state, index) - validator = state.validator_registry[index] - state.latest_slashed_balances[get_current_epoch(state) % LATEST_PENALIZED_EXIT_LENGTH] += get_effective_balance(state, index) - - proposer_index = get_beacon_proposer_index(state, state.slot) - whistleblower_reward = get_effective_balance(state, index) // WHISTLEBLOWER_REWARD_QUOTIENT - if whistleblower_index is None: - increase_balance(state, proposer_index, whistleblower_reward) - else: - proposer_share = whistleblower_reward // INCLUDER_REWARD_QUOTIENT # TODO: Define INCLUDER_REWARD_QUOTIENT - increase_balance(state, proposer_index, proposer_share) - increase_balance(state, whistleblower_index, whistleblower_reward - proposer_share) - - decrease_balance(state, index, whistleblower_reward) - validator.slashed_epoch = get_current_epoch(state) - validator.withdrawable_epoch = get_current_epoch(state) + LATEST_PENALIZED_EXIT_LENGTH -``` - -The only change is that this introduces the possibility of a penalization where the "whistleblower" that takes credit is NOT the block proposer. - ## Per-block processing ### Transactions From 082882b5b56e68057aaec49c3e111b9dc9260fda Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 25 Mar 2019 16:32:09 +0000 Subject: [PATCH 12/18] Update 1_custody-game.md --- specs/core/1_custody-game.md | 193 ++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 92 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 4e4fbe1d5b..77af943e3a 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -20,21 +20,22 @@ - [`BeaconState`](#beaconstate) - [`BeaconBlockBody`](#beaconblockbody) - [Custody objects](#custody-objects) - - [`DataChallenge`](#datachallenge) - - [`DataChallengeRecord`](#datachallengerecord) - - [`MixChallenge`](#mixchallenge) - - [`MixChallengeRecord`](#mixchallengerecord) + - [`ChunkChallenge`](#chunkchallenge) + - [`ChunkChallengeRecord`](#chunkchallengerecord) + - [`BitChallenge`](#bitchallenge) + - [`BitChallengeRecord`](#bitchallengerecord) - [`CustodyResponse`](#custodyresponse) - [`CustodyReveal`](#custodyreveal) - [Helpers](#helpers) - - [`get_attestation_chunk_count`](#get_attestation_chunk_count) + - [`get_crosslink_chunk_count`](#get_crosslink_chunk_count) + - [`get_chunk_bit`](#get_chunk_bit) - [`epoch_to_custody_period`](#epoch_to_custody_period) - [`verify_custody_reveal`](#verify_custody_reveal) - [Per-block processing](#per-block-processing) - [Transactions](#transactions) - [Custody reveals](#custody-reveals) - - [Data challenges](#data-challenges) - - [Mix challenges](#mix-challenges) + - [Chunk challenges](#chunk-challenges) + - [Bit challenges](#bit-challenges) - [Custody responses](#custody-responses) - [Per-epoch processing](#per-epoch-processing) @@ -58,7 +59,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | Unit | Duration | | - | - | :-: | :-: | -| `MAX_DATA_CHALLENGE_DELAY` | `2**11` (= 2,048) | epochs | ~9 days | +| `MAX_chunk_challenge_DELAY` | `2**11` (= 2,048) | epochs | ~9 days | | `EPOCHS_PER_CUSTODY_PERIOD` | `2**11` (= 2,048) | epochs | ~9 days | | `CUSTODY_RESPONSE_DEADLINE` | `2**14` (= 16,384) | epochs | ~73 days | @@ -66,8 +67,8 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | | - | - | -| `MAX_DATA_CHALLENGES` | `2**2` (= 4) | -| `MAX_MIX_CHALLENGES` | `2**1` (= 2) | +| `MAX_CHUNK_CHALLENGES` | `2**2` (= 4) | +| `MAX_BIT_CHALLENGES` | `2**1` (= 2) | | `MAX_CUSTODY_RESPONSES` | `2**5` (= 32) | | `MAX_CUSTODY_REVEALS` | `2**4` (= 16) | @@ -76,7 +77,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | | - | - | | `DOMAIN_CUSTODY_REVEAL` | `6` | -| `DOMAIN_MIX_CHALLENGE` | `7` | +| `DOMAIN_BIT_CHALLENGES` | `7` | ## Data structures @@ -94,8 +95,8 @@ Add the following fields to the end of the specified container objects. Fields o #### `BeaconState` ```python - 'data_challenge_records': [DataChallengeRecord], - 'mix_challenge_records': [MixChallengeRecord], + 'chunk_challenge_records': [ChunkChallengeRecord], + 'bit_challenge_records': [BitChallengeRecord], 'challenge_index': 'uint64', ``` @@ -103,24 +104,24 @@ Add the following fields to the end of the specified container objects. Fields o ```python 'custody_reveals': [CustodyReveal], - 'data_challenges': [DataChallenge], - 'mix_challenges': [MixChallenge], + 'chunk_challenges': [ChunkChallenge], + 'bit_challenges': [BitChallenge], 'custody_responses': [CustodyResponse], ``` ### Custody objects -#### `DataChallenge` +#### `ChunkChallenge` ```python { 'responder_index': ValidatorIndex, - 'chunk_index': 'uint64', 'attestation': Attestation, + 'chunk_index': 'uint64', } ``` -#### `DataChallengeRecord` +#### `ChunkChallengeRecord` ```python { @@ -134,20 +135,20 @@ Add the following fields to the end of the specified container objects. Fields o } ``` -#### `MixChallenge` +#### `BitChallenge` ```python { + 'responder_index': ValidatorIndex, 'attestation': Attestation, 'challenger_index': ValidatorIndex, - 'responder_index': ValidatorIndex, 'responder_subkey': BLSSignature, - 'mix': 'bytes', + 'chunk_bits': 'bytes', 'signature': BLSSignature, } ``` -#### `MixChallengeRecord` +#### `BitChallengeRecord` ```python { @@ -156,7 +157,7 @@ Add the following fields to the end of the specified container objects. Fields o 'responder_index': ValidatorIndex, 'deadline': Epoch, 'crosslink_data_root': Hash, - 'mix': 'bytes', + 'chunk_bits': 'bytes', 'responder_subkey': BLSSignature, } ``` @@ -166,9 +167,9 @@ Add the following fields to the end of the specified container objects. Fields o ```python { 'challenge_index': 'uint64', - 'data': ['byte', BYTES_PER_CHUNK], - 'branch': [Hash], 'chunk_index': 'uint64', + 'chunk': ['byte', BYTES_PER_CHUNK], + 'branch': [Hash], } ``` @@ -186,10 +187,10 @@ Add the following fields to the end of the specified container objects. Fields o ## Helpers -### `get_attestation_chunk_count` +### `get_crosslink_chunk_count` ```python -def get_attestation_chunk_count(attestation: Attestation) -> int: +def get_crosslink_chunk_count(attestation: Attestation) -> int: attestation_start_epoch = attestation.data.latest_crosslink.epoch attestation_end_epoch = slot_to_epoch(attestation.data.slot) attestation_crosslink_length = min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch) @@ -197,6 +198,14 @@ def get_attestation_chunk_count(attestation: Attestation) -> int: return attestation_crosslink_length * chunks_per_epoch ``` +### `get_chunk_bit` + +```python +def get_chunk_bit(subkey: BLSSignature, chunk: bytes) -> bool: + # TODO: Replace with something MPC-friendly, e.g. Legendre symbol + return get_bitfield_bit(hash(challenge.responder_subkey + chunk), 0) +``` + ### `epoch_to_custody_period` ```python @@ -208,7 +217,7 @@ def epoch_to_custody_period(epoch: Epoch) -> int: ```python def verify_custody_reveal(state: BeaconState, - reveal: CustodyReveal) -> bool + reveal: CustodyReveal) -> bool # Case 1: legitimate reveal pubkeys = [state.validator_registry[reveal.revealer_index].pubkey] message_hashes = [hash_tree_root(reveal.period)] @@ -251,7 +260,7 @@ def process_custody_reveal(state: BeaconState, assert verify_custody_reveal(reveal) revealer = state.validator_registry[reveal.revealer_index] - # Case 1: Non-early non-punitive non-masked reveal + # Case 1: non-early non-punitive non-masked reveal if reveal.mask == ZERO_HASH: assert reveal.period == epoch_to_custody_period(revealer.activation_epoch) + revealer.custody_reveals # Revealer is active or exited @@ -259,7 +268,7 @@ def process_custody_reveal(state: BeaconState, revealer.custody_reveals += 1 revealer.max_reveal_lateness = max(revealer.max_reveal_lateness, get_current_period(state) - reveal.period) - # Case 2: Early punitive masked reveal + # Case 2: early punitive masked reveal else: assert reveal.period > epoch_to_custody_period(get_current_epoch(state)) assert revealer.slashed is False @@ -271,36 +280,36 @@ def process_custody_reveal(state: BeaconState, ``` -#### Data challenges +#### Chunk challenges -Verify that `len(block.body.data_challenges) <= MAX_DATA_CHALLENGES`. +Verify that `len(block.body.chunk_challenges) <= MAX_CHUNK_CHALLENGES`. -For each `challenge` in `block.body.data_challenges`, run the following function: +For each `challenge` in `block.body.chunk_challenges`, run the following function: ```python -def process_data_challenge(state: BeaconState, - challenge: DataChallenge) -> None: +def process_chunk_challenge(state: BeaconState, + challenge: ChunkChallenge) -> None: # Verify the attestation assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation)) # Verify it is not too late to challenge - assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY - assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_DATA_CHALLENGE_DELAY + assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_chunk_challenge_DELAY + assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_chunk_challenge_DELAY # Verify the responder participated in the attestation assert challenger.responder_index in challenge.attestation.validator_indices # Verify the challenge is not a duplicate - for record in state.data_challenge_records: + for record in state.chunk_challenge_records: assert ( - record.crosslink_data_root != challenge.attestation.data.crosslink_crosslink_data_root or + record.crosslink_data_root != challenge.attestation.data.crosslink_data_root or record.chunk_index != challenge.chunk_index ) # Verify depth - depth = math.log2(next_power_of_two(get_attestation_chunk_count(challenge.attestation))) + depth = math.log2(next_power_of_two(get_crosslink_chunk_count(challenge.attestation))) assert challenge.chunk_index < 2**depth - # Add new data challenge record - state.data_challenge_records.append(DataChallengeRecord( + # Add new chunk challenge record + state.chunk_challenge_records.append(ChunkChallengeRecord( challenge_index=state.challenge_index, challenger_index=get_beacon_proposer_index(state, state.slot), - crosslink_data_root=challenge.attestation.data.crosslink_crosslink_data_root, + crosslink_data_root=challenge.attestation.data.crosslink_data_root, depth=depth, deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE, chunk_index=challenge.chunk_index, @@ -308,22 +317,22 @@ def process_data_challenge(state: BeaconState, state.challenge_index += 1 ``` -#### Mix challenges +#### Bit challenges -Verify that `len(block.body.mix_challenges) <= MAX_MIX_CHALLENGES`. +Verify that `len(block.body.bit_challenges) <= MAX_BIT_CHALLENGES`. -For each `challenge` in `block.body.mix_challenges`, run the following function: +For each `challenge` in `block.body.bit_challenges`, run the following function: ```python -def process_mix_challenge(state: BeaconState, - challenge: MixChallenge) -> None: +def process_bit_challenge(state: BeaconState, + challenge: BitChallenge) -> None: # Verify challenge signature challenger = state.validator_registry[challenge.challenger_index] assert bls_verify( message_hash=signed_root(challenge), pubkey=challenger.pubkey, signature=challenge.signature, - domain=get_domain(state, get_current_epoch(state), DOMAIN_MIX_CHALLENGE) + domain=get_domain(state, get_current_epoch(state), DOMAIN_BIT_CHALLENGES) ) # Verify the challenger is not slashed assert challenger.slashed is False @@ -336,7 +345,7 @@ def process_mix_challenge(state: BeaconState, # Verify the responder participated in the attestation assert challenge.responder_index in attestation.validator_indices # A validator can be the challenger or responder for at most one challenge at a time - for challenge_record in state.mix_challenge_records: + for challenge_record in state.bit_challenge_records: assert challenge_record.challenger_index != challenge.challenger_index assert challenge_record.responder_index != challenge.responder_index # Verify the responder subkey @@ -345,19 +354,23 @@ def process_mix_challenge(state: BeaconState, period=epoch_to_custody_period(slot_to_epoch(attestation.data.slot)), subkey=challenge.responder_subkey, )) - # Verify the mix's length and that its last bit is the opposite of the custody bit - mix_length = get_attestation_chunk_count(challenge.attestation) - verify_bitfield(challenge.mix, mix_length) + # Verify the chunk bits count + chunk_bits_count = get_crosslink_chunk_count(challenge.attestation) + verify_bitfield(challenge.chunk_bits, chunk_bits_count) + # Verify the sum of the chunk bits does not equal the custody bit + chunk_bits_sum = 0b0 + for i in range(chunk_bits_count): + chunk_bits_sum ^ get_bitfield_bit(challenge.chunk_bits, i) custody_bit = get_bitfield_bit(attestation.custody_bitfield, attestation.validator_indices.index(responder_index)) - assert custody_bit != get_bitfield_bit(challenge.mix, mix_length - 1) - # Add new mix challenge record - state.mix_challenge_records.append(MixChallengeRecord( + assert custody_bit != chunk_bits_sum + # Add new bit challenge record + state.bit_challenge_records.append(BitChallengeRecord( challenge_index=state.challenge_index, challenger_index=challenge.challenger_index, responder_index=challenge.responder_index, - crosslink_data_root=challenge.attestation.crosslink_crosslink_data_root, - mix=challenge.mix, - responder_subkey=responder_subkey, + crosslink_data_root=challenge.attestation.crosslink_data_root, + chunk_bits=challenge.chunk_bits, + responder_subkey=challenge.responder_subkey, deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE )) state.challenge_index += 1 @@ -374,80 +387,76 @@ For each `response` in `block.body.custody_responses`, run the following functio ```python def process_custody_response(state: BeaconState, response: CustodyResponse) -> None: - data_challenge = next(c for c in state.data_challenge_records if c.challenge_index == response.challenge_index, None) - if data_challenge is not None: - return process_data_challenge_response(state, response, data_challenge) + chunk_challenge = next(c for c in state.chunk_challenge_records if c.challenge_index == response.challenge_index, None) + if chunk_challenge is not None: + return process_chunk_challenge_response(state, response, chunk_challenge) - mix_challenge = next(c for c in state.mix_challenge_records if c.challenge_index == response.challenge_index, None) - if mix_challenge is not None: - return process_mix_challenge_response(state, response, mix_challenge) + bit_challenge = next(c for c in state.bit_challenge_records if c.challenge_index == response.challenge_index, None) + if bit_challenge is not None: + return process_bit_challenge_response(state, response, bit_challenge) assert False ``` ```python -def process_data_challenge_response(state: BeaconState, +def process_chunk_challenge_response(state: BeaconState, response: CustodyResponse, - challenge: DataChallengeRecord) -> None: + challenge: ChunkChallengeRecord) -> None: assert verify_merkle_branch( - leaf=hash_tree_root(response.data), + leaf=hash_tree_root(response.chunk), branch=response.branch, depth=challenge.depth, index=challenge.chunk_index, root=challenge.crosslink_data_root, ) - # Check data index + # Check chunk index assert response.chunk_index == challenge.chunk_index # Must wait at least ENTRY_EXIT_DELAY before responding to a branch challenge assert get_current_epoch(state) >= challenge.inclusion_epoch + ENTRY_EXIT_DELAY - state.data_challenge_records.remove(challenge) + state.chunk_challenge_records.remove(challenge) # Reward the proposer proposer_index = get_beacon_proposer_index(state, state.slot) increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) ``` ```python -def process_mix_challenge_response(state: BeaconState, +def process_bit_challenge_response(state: BeaconState, response: CustodyResponse, - challenge: MixChallengeRecord) -> None: - # Check the data index is valid - assert response.chunk_index < len(challenge.mix) - # Check the provided data is part of the attested data + challenge: BitChallengeRecord) -> None: + # Check the chunk index + assert response.chunk_index < len(challenge.chunk_bits) + # Check the provided chunk matches the crosslink data root assert verify_merkle_branch( - leaf=hash_tree_root(response.data), + leaf=hash_tree_root(response.chunk), branch=response.branch, - depth=math.log2(next_power_of_two(len(challenge.mix))), + depth=math.log2(next_power_of_two(len(challenge.chunk_bits))), index=response.chunk_index, root=challenge.crosslink_data_root, ) - # Check the mix bit (assert the response identified an invalid data index in the challenge) - mix_bit = get_bitfield_bit(hash(challenge.responder_subkey + response.data), 0) - previous_bit = 0 if response.chunk_index == 0 else get_bitfield_bit(challenge.mix, response.chunk_index - 1) - next_bit = get_bitfield_bit(challenge.mix, response.chunk_index) - assert previous_bit ^ mix_bit != next_bit - # Resolve the challenge in favour of the responder + # Check the chunk bit does not match the challenge + assert get_chunk_bit(challenge.responder_subkey, response.chunk) != get_bitfield_bit(challenge.chunk_bits, response.chunk_index) slash_validator(state, challenge.challenger_index, challenge.responder_index) - state.mix_challenge_records.remove(challenge) + state.bit_challenge_records.remove(challenge) ``` ## Per-epoch processing -Add the following loop immediately below the `process_ejections` loop: +Add the following loop immediately after the `process_ejections` loop: ```python def process_challenge_deadlines(state: BeaconState) -> None: - for challenge in state.data_challenge_records: + for challenge in state.chunk_challenge_records: if get_current_epoch(state) > challenge.deadline: slash_validator(state, challenge.responder_index, challenge.challenger_index) - state.data_challenge_records.remove(challenge) + state.chunk_challenge_records.remove(challenge) - for challenge in state.mix_challenge_records: + for challenge in state.bit_challenge_records: if get_current_epoch(state) > challenge.deadline: slash_validator(state, challenge.responder_index, challenge.challenger_index) - state.mix_challenge_records.remove(challenge) + state.bit_challenge_records.remove(challenge) elif get_current_epoch(state) > state.validator_registry[challenge.responder_index].withdrawable_epoch: slash_validator(state, challenge.challenger_index, challenge.responder_index) - state.mix_challenge_records.remove(challenge) + state.bit_challenge_records.remove(challenge) ``` In `process_penalties_and_exits`, change the definition of `eligible` to the following (note that it is not a pure function because `state` is declared in the surrounding scope): @@ -455,8 +464,8 @@ In `process_penalties_and_exits`, change the definition of `eligible` to the fol ```python def eligible(index): validator = state.validator_registry[index] - # Cannot exit if there are still open data challenges - if len([c for c in state.data_challenge_records if c.responder_index == index]) > 0: + # Cannot exit if there are still open chunk challenges + if len([c for c in state.chunk_challenge_records if c.responder_index == index]) > 0: return False # Cannot exit if you have not revealed all of your subkeys elif epoch_to_custody_period(revealer.activation_epoch) + validator.custody_reveals <= epoch_to_custody_period(validator.exit_epoch): From 53089b9dfa1c100d4b122334b2722ff8c72c18fd Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 25 Mar 2019 18:21:26 +0000 Subject: [PATCH 13/18] Update 1_custody-game.md --- specs/core/1_custody-game.md | 84 +++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 77af943e3a..deffb41ca2 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -59,7 +59,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | Unit | Duration | | - | - | :-: | :-: | -| `MAX_chunk_challenge_DELAY` | `2**11` (= 2,048) | epochs | ~9 days | +| `MAX_CHUNK_CHALLENGE_DELAY` | `2**11` (= 2,048) | epochs | ~9 days | | `EPOCHS_PER_CUSTODY_PERIOD` | `2**11` (= 2,048) | epochs | ~9 days | | `CUSTODY_RESPONSE_DEADLINE` | `2**14` (= 16,384) | epochs | ~73 days | @@ -67,10 +67,10 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | | - | - | +| `MAX_CUSTODY_REVEALS` | `2**4` (= 16) | | `MAX_CHUNK_CHALLENGES` | `2**2` (= 4) | -| `MAX_BIT_CHALLENGES` | `2**1` (= 2) | +| `MAX_BIT_CHALLENGES` | `2**2` (= 4) | | `MAX_CUSTODY_RESPONSES` | `2**5` (= 32) | -| `MAX_CUSTODY_REVEALS` | `2**4` (= 16) | ### Signature domains @@ -88,7 +88,7 @@ Add the following fields to the end of the specified container objects. Fields o #### `Validator` ```python - 'custody_reveals': 'uint64', + 'custody_reveal_index': 'uint64', 'max_reveal_lateness': 'uint64', ``` @@ -128,8 +128,8 @@ Add the following fields to the end of the specified container objects. Fields o 'challenge_index': 'uint64', 'challenger_index': ValidatorIndex, 'responder_index': ValidatorIndex, - 'deadline': 'uint64', - 'crosslink_data_root': 'bytes32', + 'deadline': Epoch, + 'crosslink_data_root': Hash, 'depth': 'uint64', 'chunk_index': 'uint64', } @@ -143,7 +143,7 @@ Add the following fields to the end of the specified container objects. Fields o 'attestation': Attestation, 'challenger_index': ValidatorIndex, 'responder_subkey': BLSSignature, - 'chunk_bits': 'bytes', + 'chunk_bits': Bitfield, 'signature': BLSSignature, } ``` @@ -157,7 +157,7 @@ Add the following fields to the end of the specified container objects. Fields o 'responder_index': ValidatorIndex, 'deadline': Epoch, 'crosslink_data_root': Hash, - 'chunk_bits': 'bytes', + 'chunk_bits': Bitfield, 'responder_subkey': BLSSignature, } ``` @@ -181,7 +181,7 @@ Add the following fields to the end of the specified container objects. Fields o 'period': 'uint64', 'subkey': BLSSignature, 'masker_index': ValidatorIndex, - 'mask': 'bytes32', + 'mask': Hash, } ``` @@ -191,11 +191,11 @@ Add the following fields to the end of the specified container objects. Fields o ```python def get_crosslink_chunk_count(attestation: Attestation) -> int: - attestation_start_epoch = attestation.data.latest_crosslink.epoch - attestation_end_epoch = slot_to_epoch(attestation.data.slot) - attestation_crosslink_length = min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch) + crosslink_start_epoch = attestation.data.latest_crosslink.epoch + crosslink_end_epoch = slot_to_epoch(attestation.data.slot) + crosslink_crosslink_length = min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch) chunks_per_epoch = 2 * BYTES_PER_SHARD_BLOCK * SLOTS_PER_EPOCH // BYTES_PER_CHUNK - return attestation_crosslink_length * chunks_per_epoch + return crosslink_crosslink_length * chunks_per_epoch ``` ### `get_chunk_bit` @@ -218,7 +218,7 @@ def epoch_to_custody_period(epoch: Epoch) -> int: ```python def verify_custody_reveal(state: BeaconState, reveal: CustodyReveal) -> bool - # Case 1: legitimate reveal + # Case 1: non-masked non-punitive non-early reveal pubkeys = [state.validator_registry[reveal.revealer_index].pubkey] message_hashes = [hash_tree_root(reveal.period)] @@ -250,25 +250,25 @@ Add the following transactions to the per-block processing, in order the given b #### Custody reveals -Verify that `len(block.body.early_custody_reveals) <= MAX_CUSTODY_REVEALS`. +Verify that `len(block.body.custody_reveals) <= MAX_CUSTODY_REVEALS`. -For each `reveal` in `block.body.early_custody_reveals`, run the following function: +For each `reveal` in `block.body.custody_reveals`, run the following function: ```python def process_custody_reveal(state: BeaconState, - reveal: CustodyReveal) -> None: + reveal: CustodyReveal) -> None: assert verify_custody_reveal(reveal) revealer = state.validator_registry[reveal.revealer_index] - # Case 1: non-early non-punitive non-masked reveal + # Case 1: non-masked non-punitive non-early reveal if reveal.mask == ZERO_HASH: - assert reveal.period == epoch_to_custody_period(revealer.activation_epoch) + revealer.custody_reveals + assert reveal.period == epoch_to_custody_period(revealer.activation_epoch) + revealer.custody_reveal_index # Revealer is active or exited assert is_active_validator(revealer, get_current_epoch(state)) or revealer.exit_epoch > get_current_epoch(state) - revealer.custody_reveals += 1 + revealer.custody_reveal_index += 1 revealer.max_reveal_lateness = max(revealer.max_reveal_lateness, get_current_period(state) - reveal.period) - # Case 2: early punitive masked reveal + # Case 2: masked punitive early reveal else: assert reveal.period > epoch_to_custody_period(get_current_epoch(state)) assert revealer.slashed is False @@ -288,12 +288,12 @@ For each `challenge` in `block.body.chunk_challenges`, run the following functio ```python def process_chunk_challenge(state: BeaconState, - challenge: ChunkChallenge) -> None: + challenge: ChunkChallenge) -> None: # Verify the attestation assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation)) # Verify it is not too late to challenge - assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_chunk_challenge_DELAY - assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_chunk_challenge_DELAY + assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY + assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY # Verify the responder participated in the attestation assert challenger.responder_index in challenge.attestation.validator_indices # Verify the challenge is not a duplicate @@ -357,12 +357,12 @@ def process_bit_challenge(state: BeaconState, # Verify the chunk bits count chunk_bits_count = get_crosslink_chunk_count(challenge.attestation) verify_bitfield(challenge.chunk_bits, chunk_bits_count) - # Verify the sum of the chunk bits does not equal the custody bit - chunk_bits_sum = 0b0 + # Verify the xor of the chunk bits does not equal the custody bit + chunk_bits_xor = 0b0 for i in range(chunk_bits_count): - chunk_bits_sum ^ get_bitfield_bit(challenge.chunk_bits, i) + chunk_bits_xor ^ get_bitfield_bit(challenge.chunk_bits, i) custody_bit = get_bitfield_bit(attestation.custody_bitfield, attestation.validator_indices.index(responder_index)) - assert custody_bit != chunk_bits_sum + assert custody_bit != chunk_bits_xor # Add new bit challenge record state.bit_challenge_records.append(BitChallengeRecord( challenge_index=state.challenge_index, @@ -386,7 +386,7 @@ For each `response` in `block.body.custody_responses`, run the following functio ```python def process_custody_response(state: BeaconState, - response: CustodyResponse) -> None: + response: CustodyResponse) -> None: chunk_challenge = next(c for c in state.chunk_challenge_records if c.challenge_index == response.challenge_index, None) if chunk_challenge is not None: return process_chunk_challenge_response(state, response, chunk_challenge) @@ -400,8 +400,11 @@ def process_custody_response(state: BeaconState, ```python def process_chunk_challenge_response(state: BeaconState, - response: CustodyResponse, - challenge: ChunkChallengeRecord) -> None: + response: CustodyResponse, + challenge: ChunkChallengeRecord) -> None: + # Verify chunk index + assert response.chunk_index == challenge.chunk_index + # Verify the chunk matches the crosslink data root assert verify_merkle_branch( leaf=hash_tree_root(response.chunk), branch=response.branch, @@ -409,10 +412,7 @@ def process_chunk_challenge_response(state: BeaconState, index=challenge.chunk_index, root=challenge.crosslink_data_root, ) - # Check chunk index - assert response.chunk_index == challenge.chunk_index - # Must wait at least ENTRY_EXIT_DELAY before responding to a branch challenge - assert get_current_epoch(state) >= challenge.inclusion_epoch + ENTRY_EXIT_DELAY + # Clear the challenge state.chunk_challenge_records.remove(challenge) # Reward the proposer proposer_index = get_beacon_proposer_index(state, state.slot) @@ -425,7 +425,7 @@ def process_bit_challenge_response(state: BeaconState, challenge: BitChallengeRecord) -> None: # Check the chunk index assert response.chunk_index < len(challenge.chunk_bits) - # Check the provided chunk matches the crosslink data root + # Check the chunk matches the crosslink data root assert verify_merkle_branch( leaf=hash_tree_root(response.chunk), branch=response.branch, @@ -433,15 +433,16 @@ def process_bit_challenge_response(state: BeaconState, index=response.chunk_index, root=challenge.crosslink_data_root, ) - # Check the chunk bit does not match the challenge + # Check the chunk bit does not match the challenge chunk bit assert get_chunk_bit(challenge.responder_subkey, response.chunk) != get_bitfield_bit(challenge.chunk_bits, response.chunk_index) - slash_validator(state, challenge.challenger_index, challenge.responder_index) + # Clear the challenge state.bit_challenge_records.remove(challenge) + slash_validator(state, challenge.challenger_index, challenge.responder_index) ``` ## Per-epoch processing -Add the following loop immediately after the `process_ejections` loop: +Run `process_challenge_deadlines(state)` immediately after `process_ejections(state)`: ```python def process_challenge_deadlines(state: BeaconState) -> None: @@ -449,6 +450,9 @@ def process_challenge_deadlines(state: BeaconState) -> None: if get_current_epoch(state) > challenge.deadline: slash_validator(state, challenge.responder_index, challenge.challenger_index) state.chunk_challenge_records.remove(challenge) + elif get_current_epoch(state) > state.validator_registry[challenge.responder_index].withdrawable_epoch: + slash_validator(state, challenge.challenger_index, challenge.responder_index) + state.chunk_challenge_records.remove(challenge) for challenge in state.bit_challenge_records: if get_current_epoch(state) > challenge.deadline: @@ -468,7 +472,7 @@ def eligible(index): if len([c for c in state.chunk_challenge_records if c.responder_index == index]) > 0: return False # Cannot exit if you have not revealed all of your subkeys - elif epoch_to_custody_period(revealer.activation_epoch) + validator.custody_reveals <= epoch_to_custody_period(validator.exit_epoch): + elif epoch_to_custody_period(revealer.activation_epoch) + validator.custody_reveal_index <= epoch_to_custody_period(validator.exit_epoch): return False # Cannot exit if you already have elif validator.withdrawable_epoch < FAR_FUTURE_EPOCH: From f8351e54acc4a2bb0048e142460654d4bd2eca66 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 26 Mar 2019 10:29:12 +0000 Subject: [PATCH 14/18] Miscellaneous fixes from Carl and Dankrad --- specs/core/1_custody-game.md | 134 ++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 65 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index deffb41ca2..ee8aaa3134 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -15,17 +15,17 @@ - [Max transactions per block](#max-transactions-per-block) - [Signature domains](#signature-domains) - [Data structures](#data-structures) - - [Phase 0 updates](#phase-0-updates) - - [`Validator`](#validator) - - [`BeaconState`](#beaconstate) - - [`BeaconBlockBody`](#beaconblockbody) - [Custody objects](#custody-objects) - [`ChunkChallenge`](#chunkchallenge) - - [`ChunkChallengeRecord`](#chunkchallengerecord) - [`BitChallenge`](#bitchallenge) + - [`ChunkChallengeRecord`](#chunkchallengerecord) - [`BitChallengeRecord`](#bitchallengerecord) - [`CustodyResponse`](#custodyresponse) - [`CustodyReveal`](#custodyreveal) + - [Phase 0 updates](#phase-0-updates) + - [`Validator`](#validator) + - [`BeaconState`](#beaconstate) + - [`BeaconBlockBody`](#beaconblockbody) - [Helpers](#helpers) - [`get_crosslink_chunk_count`](#get_crosslink_chunk_count) - [`get_chunk_bit`](#get_chunk_bit) @@ -52,7 +52,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | | - | - | | `BYTES_PER_SHARD_BLOCK` | `2**14` (= 16,384) | -| `BYTES_PER_CHUNK` | `2**9` (= 512) | +| `BYTES_PER_CUSTODY_CHUNK` | `2**9` (= 512) | | `MINOR_REWARD_QUOTIENT` | `2**8` (= 256) | ### Time parameters @@ -81,43 +81,28 @@ This document details the beacon chain additions and changes in Phase 1 of Ether ## Data structures -### Phase 0 updates - -Add the following fields to the end of the specified container objects. Fields of type `uint64` are initialized to `0` and list fields are initialized to `[]`. - -#### `Validator` - -```python - 'custody_reveal_index': 'uint64', - 'max_reveal_lateness': 'uint64', -``` - -#### `BeaconState` - -```python - 'chunk_challenge_records': [ChunkChallengeRecord], - 'bit_challenge_records': [BitChallengeRecord], - 'challenge_index': 'uint64', -``` +### Custody objects -#### `BeaconBlockBody` +#### `ChunkChallenge` ```python - 'custody_reveals': [CustodyReveal], - 'chunk_challenges': [ChunkChallenge], - 'bit_challenges': [BitChallenge], - 'custody_responses': [CustodyResponse], +{ + 'responder_index': ValidatorIndex, + 'attestation': Attestation, + 'chunk_index': 'uint64', +} ``` -### Custody objects - -#### `ChunkChallenge` +#### `BitChallenge` ```python { 'responder_index': ValidatorIndex, 'attestation': Attestation, - 'chunk_index': 'uint64', + 'challenger_index': ValidatorIndex, + 'responder_subkey': BLSSignature, + 'chunk_bits': Bitfield, + 'signature': BLSSignature, } ``` @@ -135,19 +120,6 @@ Add the following fields to the end of the specified container objects. Fields o } ``` -#### `BitChallenge` - -```python -{ - 'responder_index': ValidatorIndex, - 'attestation': Attestation, - 'challenger_index': ValidatorIndex, - 'responder_subkey': BLSSignature, - 'chunk_bits': Bitfield, - 'signature': BLSSignature, -} -``` - #### `BitChallengeRecord` ```python @@ -168,7 +140,7 @@ Add the following fields to the end of the specified container objects. Fields o { 'challenge_index': 'uint64', 'chunk_index': 'uint64', - 'chunk': ['byte', BYTES_PER_CHUNK], + 'chunk': ['byte', BYTES_PER_CUSTODY_CHUNK], 'branch': [Hash], } ``` @@ -185,6 +157,34 @@ Add the following fields to the end of the specified container objects. Fields o } ``` +### Phase 0 updates + +Add the following fields to the end of the specified container objects. Fields with underlying type `uint64` are initialized to `0` and list fields are initialized to `[]`. + +#### `Validator` + +```python + 'custody_reveal_index': 'uint64', + 'max_reveal_lateness': 'uint64', +``` + +#### `BeaconState` + +```python + 'chunk_challenge_records': [ChunkChallengeRecord], + 'bit_challenge_records': [BitChallengeRecord], + 'challenge_index': 'uint64', +``` + +#### `BeaconBlockBody` + +```python + 'custody_reveals': [CustodyReveal], + 'chunk_challenges': [ChunkChallenge], + 'bit_challenges': [BitChallenge], + 'custody_responses': [CustodyResponse], +``` + ## Helpers ### `get_crosslink_chunk_count` @@ -194,7 +194,7 @@ def get_crosslink_chunk_count(attestation: Attestation) -> int: crosslink_start_epoch = attestation.data.latest_crosslink.epoch crosslink_end_epoch = slot_to_epoch(attestation.data.slot) crosslink_crosslink_length = min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch) - chunks_per_epoch = 2 * BYTES_PER_SHARD_BLOCK * SLOTS_PER_EPOCH // BYTES_PER_CHUNK + chunks_per_epoch = 2 * BYTES_PER_SHARD_BLOCK * SLOTS_PER_EPOCH // BYTES_PER_CUSTODY_CHUNK return crosslink_crosslink_length * chunks_per_epoch ``` @@ -202,7 +202,7 @@ def get_crosslink_chunk_count(attestation: Attestation) -> int: ```python def get_chunk_bit(subkey: BLSSignature, chunk: bytes) -> bool: - # TODO: Replace with something MPC-friendly, e.g. Legendre symbol + # TODO: Replace with something MPC-friendly, e.g. the Legendre symbol return get_bitfield_bit(hash(challenge.responder_subkey + chunk), 0) ``` @@ -217,7 +217,7 @@ def epoch_to_custody_period(epoch: Epoch) -> int: ```python def verify_custody_reveal(state: BeaconState, - reveal: CustodyReveal) -> bool + reveal: CustodyReveal) -> bool: # Case 1: non-masked non-punitive non-early reveal pubkeys = [state.validator_registry[reveal.revealer_index].pubkey] message_hashes = [hash_tree_root(reveal.period)] @@ -257,7 +257,7 @@ For each `reveal` in `block.body.custody_reveals`, run the following function: ```python def process_custody_reveal(state: BeaconState, reveal: CustodyReveal) -> None: - assert verify_custody_reveal(reveal) + assert verify_custody_reveal(state, reveal) revealer = state.validator_registry[reveal.revealer_index] # Case 1: non-masked non-punitive non-early reveal @@ -273,7 +273,6 @@ def process_custody_reveal(state: BeaconState, assert reveal.period > epoch_to_custody_period(get_current_epoch(state)) assert revealer.slashed is False slash_validator(state, reveal.revealer_index, reveal.masker_index) - increase_balance(state, reveal.masker_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) proposer_index = get_beacon_proposer_index(state, state.slot) increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) @@ -293,9 +292,10 @@ def process_chunk_challenge(state: BeaconState, assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation)) # Verify it is not too late to challenge assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY - assert state.validator_registry[responder_index].exit_epoch >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY + assert state.validator_registry[challenge.responder_index].exit_epoch >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY # Verify the responder participated in the attestation - assert challenger.responder_index in challenge.attestation.validator_indices + attesters = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) + assert challenge.responder_index in attesters # Verify the challenge is not a duplicate for record in state.chunk_challenge_records: assert ( @@ -329,39 +329,42 @@ def process_bit_challenge(state: BeaconState, # Verify challenge signature challenger = state.validator_registry[challenge.challenger_index] assert bls_verify( - message_hash=signed_root(challenge), pubkey=challenger.pubkey, + message_hash=signed_root(challenge), signature=challenge.signature, - domain=get_domain(state, get_current_epoch(state), DOMAIN_BIT_CHALLENGES) + domain=get_domain(state, get_current_epoch(state), DOMAIN_BIT_CHALLENGES), ) # Verify the challenger is not slashed assert challenger.slashed is False - # Verify attestation + # Verify the attestation assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation)) # Verify the attestation is eligible for challenging responder = state.validator_registry[challenge.responder_index] min_challengeable_epoch = responder.exit_epoch - EPOCHS_PER_CUSTODY_PERIOD * (1 + responder.max_reveal_lateness) assert min_challengeable_epoch <= slot_to_epoch(challenge.attestation.data.slot) # Verify the responder participated in the attestation - assert challenge.responder_index in attestation.validator_indices + attesters = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) + assert challenge.responder_index in attesters # A validator can be the challenger or responder for at most one challenge at a time for challenge_record in state.bit_challenge_records: assert challenge_record.challenger_index != challenge.challenger_index assert challenge_record.responder_index != challenge.responder_index # Verify the responder subkey - assert verify_custody_reveal(CustodyReveal( + assert verify_custody_reveal(state, CustodyReveal( revealer_index=challenge.responder_index, period=epoch_to_custody_period(slot_to_epoch(attestation.data.slot)), subkey=challenge.responder_subkey, + masker_index=0, + mask=ZERO_HASH, )) # Verify the chunk bits count chunk_bits_count = get_crosslink_chunk_count(challenge.attestation) - verify_bitfield(challenge.chunk_bits, chunk_bits_count) + assert verify_bitfield(challenge.chunk_bits, chunk_bits_count) # Verify the xor of the chunk bits does not equal the custody bit chunk_bits_xor = 0b0 for i in range(chunk_bits_count): chunk_bits_xor ^ get_bitfield_bit(challenge.chunk_bits, i) - custody_bit = get_bitfield_bit(attestation.custody_bitfield, attestation.validator_indices.index(responder_index)) + custody_bit = get_bitfield_bit(attestation.custody_bitfield, attesters.index(responder_index)) assert custody_bit != chunk_bits_xor # Add new bit challenge record state.bit_challenge_records.append(BitChallengeRecord( @@ -409,7 +412,7 @@ def process_chunk_challenge_response(state: BeaconState, leaf=hash_tree_root(response.chunk), branch=response.branch, depth=challenge.depth, - index=challenge.chunk_index, + index=response.chunk_index, root=challenge.crosslink_data_root, ) # Clear the challenge @@ -423,9 +426,9 @@ def process_chunk_challenge_response(state: BeaconState, def process_bit_challenge_response(state: BeaconState, response: CustodyResponse, challenge: BitChallengeRecord) -> None: - # Check the chunk index + # Verify chunk index assert response.chunk_index < len(challenge.chunk_bits) - # Check the chunk matches the crosslink data root + # Verify the chunk matches the crosslink data root assert verify_merkle_branch( leaf=hash_tree_root(response.chunk), branch=response.branch, @@ -433,10 +436,11 @@ def process_bit_challenge_response(state: BeaconState, index=response.chunk_index, root=challenge.crosslink_data_root, ) - # Check the chunk bit does not match the challenge chunk bit + # Verify the chunk bit does not match the challenge chunk bit assert get_chunk_bit(challenge.responder_subkey, response.chunk) != get_bitfield_bit(challenge.chunk_bits, response.chunk_index) # Clear the challenge state.bit_challenge_records.remove(challenge) + # Slash challenger slash_validator(state, challenge.challenger_index, challenge.responder_index) ``` From 5937546ce2e363fa03a3e8634f4b1d697275b3ce Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 26 Mar 2019 10:39:03 +0000 Subject: [PATCH 15/18] More fixes from review --- specs/core/1_custody-game.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index ee8aaa3134..a1bfef3bac 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -259,6 +259,7 @@ def process_custody_reveal(state: BeaconState, reveal: CustodyReveal) -> None: assert verify_custody_reveal(state, reveal) revealer = state.validator_registry[reveal.revealer_index] + current_custody_period = epoch_to_custody_period(get_current_epoch(state)) # Case 1: non-masked non-punitive non-early reveal if reveal.mask == ZERO_HASH: @@ -266,17 +267,15 @@ def process_custody_reveal(state: BeaconState, # Revealer is active or exited assert is_active_validator(revealer, get_current_epoch(state)) or revealer.exit_epoch > get_current_epoch(state) revealer.custody_reveal_index += 1 - revealer.max_reveal_lateness = max(revealer.max_reveal_lateness, get_current_period(state) - reveal.period) + revealer.max_reveal_lateness = max(revealer.max_reveal_lateness, current_custody_period - reveal.period) + proposer_index = get_beacon_proposer_index(state, state.slot) + increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) # Case 2: masked punitive early reveal else: - assert reveal.period > epoch_to_custody_period(get_current_epoch(state)) + assert reveal.period > current_custody_period assert revealer.slashed is False slash_validator(state, reveal.revealer_index, reveal.masker_index) - - proposer_index = get_beacon_proposer_index(state, state.slot) - increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) - ``` #### Chunk challenges @@ -292,7 +291,8 @@ def process_chunk_challenge(state: BeaconState, assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation)) # Verify it is not too late to challenge assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY - assert state.validator_registry[challenge.responder_index].exit_epoch >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY + responder = state.validator_registry[challenge.responder_index] + assert responder.exit_epoch >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY # Verify the responder participated in the attestation attesters = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) assert challenge.responder_index in attesters @@ -315,6 +315,8 @@ def process_chunk_challenge(state: BeaconState, chunk_index=challenge.chunk_index, )) state.challenge_index += 1 + # Postpone responder withdrawability + responder.withdrawable_epoch = FAR_FUTURE_EPOCH ``` #### Bit challenges From 50dd3411a8b0f0465a8d1f7bbd84b4ebb9482e67 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 26 Mar 2019 10:47:14 +0000 Subject: [PATCH 16/18] Update 1_custody-game.md --- specs/core/1_custody-game.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index a1bfef3bac..c986069424 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -456,17 +456,11 @@ def process_challenge_deadlines(state: BeaconState) -> None: if get_current_epoch(state) > challenge.deadline: slash_validator(state, challenge.responder_index, challenge.challenger_index) state.chunk_challenge_records.remove(challenge) - elif get_current_epoch(state) > state.validator_registry[challenge.responder_index].withdrawable_epoch: - slash_validator(state, challenge.challenger_index, challenge.responder_index) - state.chunk_challenge_records.remove(challenge) for challenge in state.bit_challenge_records: if get_current_epoch(state) > challenge.deadline: slash_validator(state, challenge.responder_index, challenge.challenger_index) state.bit_challenge_records.remove(challenge) - elif get_current_epoch(state) > state.validator_registry[challenge.responder_index].withdrawable_epoch: - slash_validator(state, challenge.challenger_index, challenge.responder_index) - state.bit_challenge_records.remove(challenge) ``` In `process_penalties_and_exits`, change the definition of `eligible` to the following (note that it is not a pure function because `state` is declared in the surrounding scope): From cb7588be75381956302eb088457ee3422735ceff Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 26 Mar 2019 11:04:38 +0000 Subject: [PATCH 17/18] Update 1_custody-game.md --- specs/core/1_custody-game.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index c986069424..b248906bdd 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -309,9 +309,10 @@ def process_chunk_challenge(state: BeaconState, state.chunk_challenge_records.append(ChunkChallengeRecord( challenge_index=state.challenge_index, challenger_index=get_beacon_proposer_index(state, state.slot), + responder_index=challenge.responder_index + deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE, crosslink_data_root=challenge.attestation.data.crosslink_data_root, depth=depth, - deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE, chunk_index=challenge.chunk_index, )) state.challenge_index += 1 @@ -373,10 +374,10 @@ def process_bit_challenge(state: BeaconState, challenge_index=state.challenge_index, challenger_index=challenge.challenger_index, responder_index=challenge.responder_index, + deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE crosslink_data_root=challenge.attestation.crosslink_data_root, chunk_bits=challenge.chunk_bits, responder_subkey=challenge.responder_subkey, - deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE )) state.challenge_index += 1 # Postpone responder withdrawability From e4680aba749cc70fb2b9c9404d685f8d2c329db1 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 26 Mar 2019 12:57:58 +0000 Subject: [PATCH 18/18] Update 1_custody-game.md --- specs/core/1_custody-game.md | 175 +++++++++++++++++++---------------- 1 file changed, 95 insertions(+), 80 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index b248906bdd..fd754634e3 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -9,6 +9,7 @@ - [Ethereum 2.0 Phase 1 -- Custody Game](#ethereum-20-phase-1----custody-game) - [Table of contents](#table-of-contents) - [Introduction](#introduction) + - [Terminology](#terminology) - [Constants](#constants) - [Misc](#misc) - [Time parameters](#time-parameters) @@ -16,21 +17,21 @@ - [Signature domains](#signature-domains) - [Data structures](#data-structures) - [Custody objects](#custody-objects) - - [`ChunkChallenge`](#chunkchallenge) - - [`BitChallenge`](#bitchallenge) - - [`ChunkChallengeRecord`](#chunkchallengerecord) - - [`BitChallengeRecord`](#bitchallengerecord) + - [`CustodyChunkChallenge`](#custodychunkchallenge) + - [`CustodyBitChallenge`](#custodybitchallenge) + - [`CustodyChunkChallengeRecord`](#custodychunkchallengerecord) + - [`CustodyBitChallengeRecord`](#custodybitchallengerecord) - [`CustodyResponse`](#custodyresponse) - - [`CustodyReveal`](#custodyreveal) - - [Phase 0 updates](#phase-0-updates) + - [`CustodyKeyReveal`](#custodykeyreveal) + - [Phase 0 container updates](#phase-0-container-updates) - [`Validator`](#validator) - [`BeaconState`](#beaconstate) - [`BeaconBlockBody`](#beaconblockbody) - [Helpers](#helpers) - [`get_crosslink_chunk_count`](#get_crosslink_chunk_count) - - [`get_chunk_bit`](#get_chunk_bit) + - [`get_custody_chunk_bit`](#get_custody_chunk_bit) - [`epoch_to_custody_period`](#epoch_to_custody_period) - - [`verify_custody_reveal`](#verify_custody_reveal) + - [`verify_custody_key`](#verify_custody_key) - [Per-block processing](#per-block-processing) - [Transactions](#transactions) - [Custody reveals](#custody-reveals) @@ -45,6 +46,21 @@ This document details the beacon chain additions and changes in Phase 1 of Ethereum 2.0 to support the shard data custody game, building upon the [phase 0](0_beacon-chain.md) specification. +## Terminology + +* **Custody game**: +* **Custody period**: +* **Custody chunk**: +* **Custody chunk bit**: +* **Custody chunk challenge**: +* **Custody bit**: +* **Custody bit challenge**: +* **Custody key**: +* **Custody key reveal**: +* **Custody key mask**: +* **Custody response**: +* **Custody response deadline**: + ## Constants ### Misc @@ -67,23 +83,23 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | Name | Value | | - | - | -| `MAX_CUSTODY_REVEALS` | `2**4` (= 16) | -| `MAX_CHUNK_CHALLENGES` | `2**2` (= 4) | -| `MAX_BIT_CHALLENGES` | `2**2` (= 4) | +| `MAX_CUSTODY_KEY_REVEALS` | `2**4` (= 16) | +| `MAX_CUSTODY_CHUNK_CHALLENGES` | `2**2` (= 4) | +| `MAX_CUSTODY_BIT_CHALLENGES` | `2**2` (= 4) | | `MAX_CUSTODY_RESPONSES` | `2**5` (= 32) | ### Signature domains | Name | Value | | - | - | -| `DOMAIN_CUSTODY_REVEAL` | `6` | -| `DOMAIN_BIT_CHALLENGES` | `7` | +| `DOMAIN_CUSTODY_KEY_REVEAL` | `6` | +| `DOMAIN_CUSTODY_BIT_CHALLENGE` | `7` | ## Data structures ### Custody objects -#### `ChunkChallenge` +#### `CustodyChunkChallenge` ```python { @@ -93,20 +109,20 @@ This document details the beacon chain additions and changes in Phase 1 of Ether } ``` -#### `BitChallenge` +#### `CustodyBitChallenge` ```python { 'responder_index': ValidatorIndex, 'attestation': Attestation, 'challenger_index': ValidatorIndex, - 'responder_subkey': BLSSignature, + 'responder_key': BLSSignature, 'chunk_bits': Bitfield, 'signature': BLSSignature, } ``` -#### `ChunkChallengeRecord` +#### `CustodyChunkChallengeRecord` ```python { @@ -120,7 +136,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether } ``` -#### `BitChallengeRecord` +#### `CustodyBitChallengeRecord` ```python { @@ -130,7 +146,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether 'deadline': Epoch, 'crosslink_data_root': Hash, 'chunk_bits': Bitfield, - 'responder_subkey': BLSSignature, + 'responder_key': BLSSignature, } ``` @@ -145,19 +161,19 @@ This document details the beacon chain additions and changes in Phase 1 of Ether } ``` -#### `CustodyReveal` +#### `CustodyKeyReveal` ```python { 'revealer_index': ValidatorIndex, 'period': 'uint64', - 'subkey': BLSSignature, + 'key': BLSSignature, 'masker_index': ValidatorIndex, 'mask': Hash, } ``` -### Phase 0 updates +### Phase 0 container updates Add the following fields to the end of the specified container objects. Fields with underlying type `uint64` are initialized to `0` and list fields are initialized to `[]`. @@ -171,17 +187,17 @@ Add the following fields to the end of the specified container objects. Fields w #### `BeaconState` ```python - 'chunk_challenge_records': [ChunkChallengeRecord], - 'bit_challenge_records': [BitChallengeRecord], - 'challenge_index': 'uint64', + 'custody_chunk_challenge_records': [CustodyChunkChallengeRecord], + 'custody_bit_challenge_records': [CustodyBitChallengeRecord], + 'custody_challenge_index': 'uint64', ``` #### `BeaconBlockBody` ```python - 'custody_reveals': [CustodyReveal], - 'chunk_challenges': [ChunkChallenge], - 'bit_challenges': [BitChallenge], + 'custody_key_reveals': [CustodyKeyReveal], + 'custody_chunk_challenges': [CustodyChunkChallenge], + 'custody_bit_challenges': [CustodyBitChallenge], 'custody_responses': [CustodyResponse], ``` @@ -190,7 +206,7 @@ Add the following fields to the end of the specified container objects. Fields w ### `get_crosslink_chunk_count` ```python -def get_crosslink_chunk_count(attestation: Attestation) -> int: +def get_custody_chunk_count(attestation: Attestation) -> int: crosslink_start_epoch = attestation.data.latest_crosslink.epoch crosslink_end_epoch = slot_to_epoch(attestation.data.slot) crosslink_crosslink_length = min(MAX_CROSSLINK_EPOCHS, end_epoch - start_epoch) @@ -198,12 +214,12 @@ def get_crosslink_chunk_count(attestation: Attestation) -> int: return crosslink_crosslink_length * chunks_per_epoch ``` -### `get_chunk_bit` +### `get_custody_chunk_bit` ```python -def get_chunk_bit(subkey: BLSSignature, chunk: bytes) -> bool: +def get_custody_chunk_bit(key: BLSSignature, chunk: bytes) -> bool: # TODO: Replace with something MPC-friendly, e.g. the Legendre symbol - return get_bitfield_bit(hash(challenge.responder_subkey + chunk), 0) + return get_bitfield_bit(hash(challenge.responder_key + chunk), 0) ``` ### `epoch_to_custody_period` @@ -213,11 +229,10 @@ def epoch_to_custody_period(epoch: Epoch) -> int: return epoch // EPOCHS_PER_CUSTODY_PERIOD ``` -### `verify_custody_reveal` +### `verify_custody_key` ```python -def verify_custody_reveal(state: BeaconState, - reveal: CustodyReveal) -> bool: +def verify_custody_key(state: BeaconState, reveal: CustodyKeyReveal) -> bool: # Case 1: non-masked non-punitive non-early reveal pubkeys = [state.validator_registry[reveal.revealer_index].pubkey] message_hashes = [hash_tree_root(reveal.period)] @@ -233,11 +248,11 @@ def verify_custody_reveal(state: BeaconState, return bls_verify_multiple( pubkeys=pubkeys, message_hashes=message_hashes, - signature=reveal.subkey, + signature=reveal.key, domain=get_domain( fork=state.fork, epoch=reveal.period * EPOCHS_PER_CUSTODY_PERIOD, - domain_type=DOMAIN_CUSTODY_REVEAL, + domain_type=DOMAIN_CUSTODY_KEY_REVEAL, ), ) ``` @@ -250,14 +265,14 @@ Add the following transactions to the per-block processing, in order the given b #### Custody reveals -Verify that `len(block.body.custody_reveals) <= MAX_CUSTODY_REVEALS`. +Verify that `len(block.body.custody_key_reveals) <= MAX_CUSTODY_KEY_REVEALS`. -For each `reveal` in `block.body.custody_reveals`, run the following function: +For each `reveal` in `block.body.custody_key_reveals`, run the following function: ```python def process_custody_reveal(state: BeaconState, - reveal: CustodyReveal) -> None: - assert verify_custody_reveal(state, reveal) + reveal: CustodyKeyReveal) -> None: + assert verify_custody_key(state, reveal) revealer = state.validator_registry[reveal.revealer_index] current_custody_period = epoch_to_custody_period(get_current_epoch(state)) @@ -280,13 +295,13 @@ def process_custody_reveal(state: BeaconState, #### Chunk challenges -Verify that `len(block.body.chunk_challenges) <= MAX_CHUNK_CHALLENGES`. +Verify that `len(block.body.custody_chunk_challenges) <= MAX_CUSTODY_CHUNK_CHALLENGES`. -For each `challenge` in `block.body.chunk_challenges`, run the following function: +For each `challenge` in `block.body.custody_chunk_challenges`, run the following function: ```python def process_chunk_challenge(state: BeaconState, - challenge: ChunkChallenge) -> None: + challenge: CustodyChunkChallenge) -> None: # Verify the attestation assert verify_standalone_attestation(state, convert_to_standalone(state, challenge.attestation)) # Verify it is not too late to challenge @@ -297,17 +312,17 @@ def process_chunk_challenge(state: BeaconState, attesters = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) assert challenge.responder_index in attesters # Verify the challenge is not a duplicate - for record in state.chunk_challenge_records: + for record in state.custody_chunk_challenge_records: assert ( record.crosslink_data_root != challenge.attestation.data.crosslink_data_root or record.chunk_index != challenge.chunk_index ) # Verify depth - depth = math.log2(next_power_of_two(get_crosslink_chunk_count(challenge.attestation))) + depth = math.log2(next_power_of_two(get_custody_chunk_count(challenge.attestation))) assert challenge.chunk_index < 2**depth # Add new chunk challenge record - state.chunk_challenge_records.append(ChunkChallengeRecord( - challenge_index=state.challenge_index, + state.custody_chunk_challenge_records.append(CustodyChunkChallengeRecord( + challenge_index=state.custody_challenge_index, challenger_index=get_beacon_proposer_index(state, state.slot), responder_index=challenge.responder_index deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE, @@ -315,27 +330,27 @@ def process_chunk_challenge(state: BeaconState, depth=depth, chunk_index=challenge.chunk_index, )) - state.challenge_index += 1 + state.custody_challenge_index += 1 # Postpone responder withdrawability responder.withdrawable_epoch = FAR_FUTURE_EPOCH ``` #### Bit challenges -Verify that `len(block.body.bit_challenges) <= MAX_BIT_CHALLENGES`. +Verify that `len(block.body.custody_bit_challenges) <= MAX_CUSTODY_BIT_CHALLENGES`. -For each `challenge` in `block.body.bit_challenges`, run the following function: +For each `challenge` in `block.body.custody_bit_challenges`, run the following function: ```python def process_bit_challenge(state: BeaconState, - challenge: BitChallenge) -> None: + challenge: CustodyBitChallenge) -> None: # Verify challenge signature challenger = state.validator_registry[challenge.challenger_index] assert bls_verify( pubkey=challenger.pubkey, message_hash=signed_root(challenge), signature=challenge.signature, - domain=get_domain(state, get_current_epoch(state), DOMAIN_BIT_CHALLENGES), + domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_BIT_CHALLENGE), ) # Verify the challenger is not slashed assert challenger.slashed is False @@ -349,37 +364,37 @@ def process_bit_challenge(state: BeaconState, attesters = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) assert challenge.responder_index in attesters # A validator can be the challenger or responder for at most one challenge at a time - for challenge_record in state.bit_challenge_records: - assert challenge_record.challenger_index != challenge.challenger_index - assert challenge_record.responder_index != challenge.responder_index - # Verify the responder subkey - assert verify_custody_reveal(state, CustodyReveal( + for record in state.custody_bit_challenge_records: + assert record.challenger_index != challenge.challenger_index + assert record.responder_index != challenge.responder_index + # Verify the responder key + assert verify_custody_key(state, CustodyKeyReveal( revealer_index=challenge.responder_index, period=epoch_to_custody_period(slot_to_epoch(attestation.data.slot)), - subkey=challenge.responder_subkey, + key=challenge.responder_key, masker_index=0, mask=ZERO_HASH, )) - # Verify the chunk bits count - chunk_bits_count = get_crosslink_chunk_count(challenge.attestation) - assert verify_bitfield(challenge.chunk_bits, chunk_bits_count) + # Verify the chunk count + chunk_count = get_custody_chunk_count(challenge.attestation) + assert verify_bitfield(challenge.chunk_bits, chunk_count) # Verify the xor of the chunk bits does not equal the custody bit chunk_bits_xor = 0b0 - for i in range(chunk_bits_count): + for i in range(chunk_count): chunk_bits_xor ^ get_bitfield_bit(challenge.chunk_bits, i) custody_bit = get_bitfield_bit(attestation.custody_bitfield, attesters.index(responder_index)) assert custody_bit != chunk_bits_xor # Add new bit challenge record - state.bit_challenge_records.append(BitChallengeRecord( - challenge_index=state.challenge_index, + state.custody_bit_challenge_records.append(CustodyBitChallengeRecord( + challenge_index=state.custody_challenge_index, challenger_index=challenge.challenger_index, responder_index=challenge.responder_index, deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE crosslink_data_root=challenge.attestation.crosslink_data_root, chunk_bits=challenge.chunk_bits, - responder_subkey=challenge.responder_subkey, + responder_key=challenge.responder_key, )) - state.challenge_index += 1 + state.custody_challenge_index += 1 # Postpone responder withdrawability responder.withdrawable_epoch = FAR_FUTURE_EPOCH ``` @@ -393,11 +408,11 @@ For each `response` in `block.body.custody_responses`, run the following functio ```python def process_custody_response(state: BeaconState, response: CustodyResponse) -> None: - chunk_challenge = next(c for c in state.chunk_challenge_records if c.challenge_index == response.challenge_index, None) + chunk_challenge = next(record for record in state.custody_chunk_challenge_records if record.challenge_index == response.challenge_index, None) if chunk_challenge is not None: return process_chunk_challenge_response(state, response, chunk_challenge) - bit_challenge = next(c for c in state.bit_challenge_records if c.challenge_index == response.challenge_index, None) + bit_challenge = next(record for record in state.custody_bit_challenge_records if record.challenge_index == response.challenge_index, None) if bit_challenge is not None: return process_bit_challenge_response(state, response, bit_challenge) @@ -407,7 +422,7 @@ def process_custody_response(state: BeaconState, ```python def process_chunk_challenge_response(state: BeaconState, response: CustodyResponse, - challenge: ChunkChallengeRecord) -> None: + challenge: CustodyChunkChallengeRecord) -> None: # Verify chunk index assert response.chunk_index == challenge.chunk_index # Verify the chunk matches the crosslink data root @@ -419,7 +434,7 @@ def process_chunk_challenge_response(state: BeaconState, root=challenge.crosslink_data_root, ) # Clear the challenge - state.chunk_challenge_records.remove(challenge) + state.custody_chunk_challenge_records.remove(challenge) # Reward the proposer proposer_index = get_beacon_proposer_index(state, state.slot) increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) @@ -428,7 +443,7 @@ def process_chunk_challenge_response(state: BeaconState, ```python def process_bit_challenge_response(state: BeaconState, response: CustodyResponse, - challenge: BitChallengeRecord) -> None: + challenge: CustodyBitChallengeRecord) -> None: # Verify chunk index assert response.chunk_index < len(challenge.chunk_bits) # Verify the chunk matches the crosslink data root @@ -440,9 +455,9 @@ def process_bit_challenge_response(state: BeaconState, root=challenge.crosslink_data_root, ) # Verify the chunk bit does not match the challenge chunk bit - assert get_chunk_bit(challenge.responder_subkey, response.chunk) != get_bitfield_bit(challenge.chunk_bits, response.chunk_index) + assert get_custody_chunk_bit(challenge.responder_key, response.chunk) != get_bitfield_bit(challenge.chunk_bits, response.chunk_index) # Clear the challenge - state.bit_challenge_records.remove(challenge) + state.custody_bit_challenge_records.remove(challenge) # Slash challenger slash_validator(state, challenge.challenger_index, challenge.responder_index) ``` @@ -453,15 +468,15 @@ Run `process_challenge_deadlines(state)` immediately after `process_ejections(st ```python def process_challenge_deadlines(state: BeaconState) -> None: - for challenge in state.chunk_challenge_records: + for challenge in state.custody_chunk_challenge_records: if get_current_epoch(state) > challenge.deadline: slash_validator(state, challenge.responder_index, challenge.challenger_index) - state.chunk_challenge_records.remove(challenge) + state.custody_chunk_challenge_records.remove(challenge) - for challenge in state.bit_challenge_records: + for challenge in state.custody_bit_challenge_records: if get_current_epoch(state) > challenge.deadline: slash_validator(state, challenge.responder_index, challenge.challenger_index) - state.bit_challenge_records.remove(challenge) + state.custody_bit_challenge_records.remove(challenge) ``` In `process_penalties_and_exits`, change the definition of `eligible` to the following (note that it is not a pure function because `state` is declared in the surrounding scope): @@ -470,9 +485,9 @@ In `process_penalties_and_exits`, change the definition of `eligible` to the fol def eligible(index): validator = state.validator_registry[index] # Cannot exit if there are still open chunk challenges - if len([c for c in state.chunk_challenge_records if c.responder_index == index]) > 0: + if len([record for record in state.custody_chunk_challenge_records if record.responder_index == index]) > 0: return False - # Cannot exit if you have not revealed all of your subkeys + # Cannot exit if you have not revealed all of your custody keys elif epoch_to_custody_period(revealer.activation_epoch) + validator.custody_reveal_index <= epoch_to_custody_period(validator.exit_epoch): return False # Cannot exit if you already have