From a5b22e13b8b4bf8247b2e130b12917eee026e0f6 Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Sat, 22 Jun 2019 16:56:16 +0200 Subject: [PATCH 1/6] Resolves make masker sign mask --- specs/core/1_custody-game.md | 2 +- .../pyspec/eth2spec/test/helpers/custody.py | 13 +++++++++---- .../test_process_early_derived_secret_reveal.py | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 3fe132c07..b051d13c6 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -204,7 +204,7 @@ class EarlyDerivedSecretReveal(Container): # Index of the validator who revealed (whistleblower) masker_index: ValidatorIndex # Mask used to hide the actual reveal signature (prevent reveal from being stolen) - mask: Bytes32 + mask: Hash ``` ### Phase 0 container updates diff --git a/test_libs/pyspec/eth2spec/test/helpers/custody.py b/test_libs/pyspec/eth2spec/test/helpers/custody.py index 67df12fcd..0167edc6a 100644 --- a/test_libs/pyspec/eth2spec/test/helpers/custody.py +++ b/test_libs/pyspec/eth2spec/test/helpers/custody.py @@ -1,5 +1,5 @@ from eth2spec.test.helpers.keys import privkeys -from eth2spec.utils.bls import bls_sign +from eth2spec.utils.bls import bls_sign, bls_aggregate_signatures def get_valid_early_derived_secret_reveal(spec, state, epoch=None): @@ -10,6 +10,7 @@ def get_valid_early_derived_secret_reveal(spec, state, epoch=None): if epoch is None: epoch = current_epoch + spec.CUSTODY_PERIOD_TO_RANDAO_PADDING + # Generate the secret that is being revealed reveal = bls_sign( message_hash=spec.hash_tree_root(epoch), privkey=privkeys[revealed_index], @@ -19,8 +20,11 @@ def get_valid_early_derived_secret_reveal(spec, state, epoch=None): message_epoch=epoch, ), ) - mask = bls_sign( - message_hash=spec.hash_tree_root(epoch), + # Generate the mask (any random 32 bytes will do) + mask = reveal[:32] + # Generate masker's signature on the mask + masker_signature = bls_sign( + message_hash=mask, privkey=privkeys[masker_index], domain=spec.get_domain( state=state, @@ -28,11 +32,12 @@ def get_valid_early_derived_secret_reveal(spec, state, epoch=None): message_epoch=epoch, ), ) + masked_reveal = bls_aggregate_signatures([reveal, masker_signature]) return spec.EarlyDerivedSecretReveal( revealed_index=revealed_index, epoch=epoch, - reveal=reveal, + reveal=masked_reveal, masker_index=masker_index, mask=mask, ) diff --git a/test_libs/pyspec/eth2spec/test/phase_1/block_processing/test_process_early_derived_secret_reveal.py b/test_libs/pyspec/eth2spec/test/phase_1/block_processing/test_process_early_derived_secret_reveal.py index 87297d443..831ad35a5 100644 --- a/test_libs/pyspec/eth2spec/test/phase_1/block_processing/test_process_early_derived_secret_reveal.py +++ b/test_libs/pyspec/eth2spec/test/phase_1/block_processing/test_process_early_derived_secret_reveal.py @@ -1,7 +1,13 @@ from eth2spec.test.helpers.custody import get_valid_early_derived_secret_reveal from eth2spec.test.helpers.block import apply_empty_block from eth2spec.test.helpers.state import next_epoch, get_balance -from eth2spec.test.context import with_all_phases_except, spec_state_test, expect_assertion_error +from eth2spec.test.context import ( + with_all_phases_except, + spec_state_test, + expect_assertion_error, + always_bls, + never_bls, +) def run_early_derived_secret_reveal_processing(spec, state, randao_key_reveal, valid=True): @@ -36,6 +42,7 @@ def run_early_derived_secret_reveal_processing(spec, state, randao_key_reveal, v @with_all_phases_except(['phase0']) +@always_bls @spec_state_test def test_success(spec, state): randao_key_reveal = get_valid_early_derived_secret_reveal(spec, state) @@ -44,6 +51,7 @@ def test_success(spec, state): @with_all_phases_except(['phase0']) +@never_bls @spec_state_test def test_reveal_from_current_epoch(spec, state): randao_key_reveal = get_valid_early_derived_secret_reveal(spec, state, spec.get_current_epoch(state)) @@ -52,6 +60,7 @@ def test_reveal_from_current_epoch(spec, state): @with_all_phases_except(['phase0']) +@never_bls @spec_state_test def test_reveal_from_past_epoch(spec, state): next_epoch(spec, state) @@ -62,6 +71,7 @@ def test_reveal_from_past_epoch(spec, state): @with_all_phases_except(['phase0']) +@always_bls @spec_state_test def test_reveal_with_custody_padding(spec, state): randao_key_reveal = get_valid_early_derived_secret_reveal( @@ -73,6 +83,7 @@ def test_reveal_with_custody_padding(spec, state): @with_all_phases_except(['phase0']) +@always_bls @spec_state_test def test_reveal_with_custody_padding_minus_one(spec, state): randao_key_reveal = get_valid_early_derived_secret_reveal( @@ -84,6 +95,7 @@ def test_reveal_with_custody_padding_minus_one(spec, state): @with_all_phases_except(['phase0']) +@never_bls @spec_state_test def test_double_reveal(spec, state): randao_key_reveal1 = get_valid_early_derived_secret_reveal( @@ -108,6 +120,7 @@ def test_double_reveal(spec, state): @with_all_phases_except(['phase0']) +@never_bls @spec_state_test def test_revealer_is_slashed(spec, state): randao_key_reveal = get_valid_early_derived_secret_reveal(spec, state, spec.get_current_epoch(state)) @@ -117,6 +130,7 @@ def test_revealer_is_slashed(spec, state): @with_all_phases_except(['phase0']) +@never_bls @spec_state_test def test_far_future_epoch(spec, state): randao_key_reveal = get_valid_early_derived_secret_reveal( From d9644f518b6ce8a9269b08c18942d5ff1d6773f1 Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Mon, 24 Jun 2019 16:08:13 +0200 Subject: [PATCH 2/6] mask is hash() in tests Co-Authored-By: dankrad --- test_libs/pyspec/eth2spec/test/helpers/custody.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_libs/pyspec/eth2spec/test/helpers/custody.py b/test_libs/pyspec/eth2spec/test/helpers/custody.py index 0167edc6a..8037755bc 100644 --- a/test_libs/pyspec/eth2spec/test/helpers/custody.py +++ b/test_libs/pyspec/eth2spec/test/helpers/custody.py @@ -21,7 +21,7 @@ def get_valid_early_derived_secret_reveal(spec, state, epoch=None): ), ) # Generate the mask (any random 32 bytes will do) - mask = reveal[:32] + mask = hash(reveal) # Generate masker's signature on the mask masker_signature = bls_sign( message_hash=mask, From 139d0f56f10972d6677b70683702f819b5c2fe02 Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Mon, 24 Jun 2019 16:26:21 +0200 Subject: [PATCH 3/6] Finishes moving mask to hash() --- test_libs/pyspec/eth2spec/test/helpers/custody.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test_libs/pyspec/eth2spec/test/helpers/custody.py b/test_libs/pyspec/eth2spec/test/helpers/custody.py index 8037755bc..9ca770c11 100644 --- a/test_libs/pyspec/eth2spec/test/helpers/custody.py +++ b/test_libs/pyspec/eth2spec/test/helpers/custody.py @@ -1,5 +1,6 @@ from eth2spec.test.helpers.keys import privkeys from eth2spec.utils.bls import bls_sign, bls_aggregate_signatures +from eth2spec.utils.hash_function import hash def get_valid_early_derived_secret_reveal(spec, state, epoch=None): @@ -20,7 +21,7 @@ def get_valid_early_derived_secret_reveal(spec, state, epoch=None): message_epoch=epoch, ), ) - # Generate the mask (any random 32 bytes will do) + # Generate the mask (any random 32 bytes that don't reveal the masker's secret will do) mask = hash(reveal) # Generate masker's signature on the mask masker_signature = bls_sign( From c764202a5768003bfaf354e90e591f66c48c9679 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Fri, 28 Jun 2019 21:35:26 +0800 Subject: [PATCH 4/6] Slashing penalty calculation change (#1217) If the exit queue is very long, then a validator may take many months to exit. With the code as currently written, however, self-slashing is a potentially lucrative route to get one's money out faster, because one can exit in 36 days. This PR changes it so that slashing can only extend your withdrawal time, not contract it. Also, instead of the slashed balances used to calculate one's slashing penalty being those in `[withdrawal - 54 days ... withdrawal - 18 days]`, we now run the penalization algorithm once every 36 days that a validator is slashed but not withdrawn, so that it covers the 36-day period where the validator was actually slashed. It also moves the minimum slashing penalty to the `slash_validator` function so that it is only applied once. We also simplify the `slashed_balances` logic to be per-epoch. --- configs/constant_presets/mainnet.yaml | 4 +- configs/constant_presets/minimal.yaml | 4 +- specs/core/0_beacon-chain.md | 46 ++++------- specs/core/1_custody-game.md | 2 +- .../test_process_attester_slashing.py | 82 ++++++++++++++++--- 5 files changed, 92 insertions(+), 46 deletions(-) diff --git a/configs/constant_presets/mainnet.yaml b/configs/constant_presets/mainnet.yaml index 9f7ca950f..38f10c8fc 100644 --- a/configs/constant_presets/mainnet.yaml +++ b/configs/constant_presets/mainnet.yaml @@ -76,7 +76,7 @@ MIN_EPOCHS_TO_INACTIVITY_PENALTY: 4 # 2**16 (= 65,536) epochs ~0.8 years EPOCHS_PER_HISTORICAL_VECTOR: 65536 # 2**13 (= 8,192) epochs ~36 days -EPOCHS_PER_SLASHED_BALANCES_VECTOR: 8192 +EPOCHS_PER_SLASHINGS_VECTOR: 8192 # 2**24 (= 16,777,216) historical roots, ~26,131 years HISTORICAL_ROOTS_LIMIT: 16777216 # 2**40 (= 1,099,511,627,776) validator spots @@ -88,7 +88,7 @@ VALIDATOR_REGISTRY_LIMIT: 1099511627776 # 2**5 (= 32) BASE_REWARD_FACTOR: 32 # 2**9 (= 512) -WHISTLEBLOWING_REWARD_QUOTIENT: 512 +WHISTLEBLOWER_REWARD_QUOTIENT: 512 # 2**3 (= 8) PROPOSER_REWARD_QUOTIENT: 8 # 2**25 (= 33,554,432) diff --git a/configs/constant_presets/minimal.yaml b/configs/constant_presets/minimal.yaml index 3e3f7ccb4..3aa4a6b71 100644 --- a/configs/constant_presets/minimal.yaml +++ b/configs/constant_presets/minimal.yaml @@ -77,7 +77,7 @@ EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS: 4096 # [customized] smaller state EPOCHS_PER_HISTORICAL_VECTOR: 64 # [customized] smaller state -EPOCHS_PER_SLASHED_BALANCES_VECTOR: 64 +EPOCHS_PER_SLASHINGS_VECTOR: 64 # 2**24 (= 16,777,216) historical roots HISTORICAL_ROOTS_LIMIT: 16777216 # 2**40 (= 1,099,511,627,776) validator spots @@ -89,7 +89,7 @@ VALIDATOR_REGISTRY_LIMIT: 1099511627776 # 2**5 (= 32) BASE_REWARD_FACTOR: 32 # 2**9 (= 512) -WHISTLEBLOWING_REWARD_QUOTIENT: 512 +WHISTLEBLOWER_REWARD_QUOTIENT: 512 # 2**3 (= 8) PROPOSER_REWARD_QUOTIENT: 8 # 2**25 (= 33,554,432) diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 1b7cda0a4..61ef8b742 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -234,7 +234,7 @@ The following values are (non-configurable) constants used throughout the specif | Name | Value | Unit | Duration | | - | - | :-: | :-: | | `EPOCHS_PER_HISTORICAL_VECTOR` | `2**16` (= 65,536) | epochs | ~0.8 years | -| `EPOCHS_PER_SLASHED_BALANCES_VECTOR` | `2**13` (= 8,192) | epochs | ~36 days | +| `EPOCHS_PER_SLASHINGS_VECTOR` | `2**13` (= 8,192) | epochs | ~36 days | | `HISTORICAL_ROOTS_LIMIT` | `2**24` (= 16,777,216) | historical roots | ~26,131 years | | `VALIDATOR_REGISTRY_LIMIT` | `2**40` (= 1,099,511,627,776) | validator spots | | @@ -243,7 +243,7 @@ The following values are (non-configurable) constants used throughout the specif | Name | Value | | - | - | | `BASE_REWARD_FACTOR` | `2**6` (= 64) | -| `WHISTLEBLOWING_REWARD_QUOTIENT` | `2**9` (= 512) | +| `WHISTLEBLOWER_REWARD_QUOTIENT` | `2**9` (= 512) | | `PROPOSER_REWARD_QUOTIENT` | `2**3` (= 8) | | `INACTIVITY_PENALTY_QUOTIENT` | `2**25` (= 33,554,432) | | `MIN_SLASHING_PENALTY_QUOTIENT` | `2**5` (= 32) | @@ -520,7 +520,7 @@ class BeaconState(Container): randao_mixes: Vector[Hash, EPOCHS_PER_HISTORICAL_VECTOR] active_index_roots: Vector[Hash, EPOCHS_PER_HISTORICAL_VECTOR] # Active registry digests for light clients # Slashings - slashed_balances: Vector[Gwei, EPOCHS_PER_SLASHED_BALANCES_VECTOR] # Sums of slashed effective balances + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances # Attestations previous_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] current_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] @@ -1097,21 +1097,22 @@ def slash_validator(state: BeaconState, """ Slash the validator with index ``slashed_index``. """ - current_epoch = get_current_epoch(state) + epoch = get_current_epoch(state) initiate_validator_exit(state, slashed_index) - state.validators[slashed_index].slashed = True - state.validators[slashed_index].withdrawable_epoch = Epoch(current_epoch + EPOCHS_PER_SLASHED_BALANCES_VECTOR) - slashed_balance = state.validators[slashed_index].effective_balance - state.slashed_balances[current_epoch % EPOCHS_PER_SLASHED_BALANCES_VECTOR] += slashed_balance + validator = state.validators[slashed_index] + validator.slashed = True + validator.withdrawable_epoch = max(validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR)) + state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance + decrease_balance(state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT) + # Apply proposer and whistleblower rewards proposer_index = get_beacon_proposer_index(state) if whistleblower_index is None: whistleblower_index = proposer_index - whistleblowing_reward = Gwei(slashed_balance // WHISTLEBLOWING_REWARD_QUOTIENT) - proposer_reward = Gwei(whistleblowing_reward // PROPOSER_REWARD_QUOTIENT) + whistleblower_reward = Gwei(validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT) + proposer_reward = Gwei(whistleblower_reward // PROPOSER_REWARD_QUOTIENT) increase_balance(state, proposer_index, proposer_reward) - increase_balance(state, whistleblower_index, whistleblowing_reward - proposer_reward) - decrease_balance(state, slashed_index, whistleblowing_reward) + increase_balance(state, whistleblower_index, whistleblower_reward - proposer_reward) ``` ## Genesis @@ -1174,7 +1175,7 @@ def get_genesis_beacon_state(deposits: Sequence[Deposit], genesis_time: int, eth validator.activation_eligibility_epoch = GENESIS_EPOCH validator.activation_epoch = GENESIS_EPOCH - # Populate active_index_roots + # Populate active_index_roots genesis_active_index_root = hash_tree_root( List[ValidatorIndex, VALIDATOR_REGISTRY_LIMIT](get_active_validator_indices(state, GENESIS_EPOCH)) ) @@ -1493,18 +1494,9 @@ def process_registry_updates(state: BeaconState) -> None: def process_slashings(state: BeaconState) -> None: epoch = get_current_epoch(state) total_balance = get_total_active_balance(state) - - # Compute slashed balances in the current epoch - total_at_start = state.slashed_balances[(epoch + 1) % EPOCHS_PER_SLASHED_BALANCES_VECTOR] - total_at_end = state.slashed_balances[epoch % EPOCHS_PER_SLASHED_BALANCES_VECTOR] - total_penalties = total_at_end - total_at_start - for index, validator in enumerate(state.validators): - if validator.slashed and epoch + EPOCHS_PER_SLASHED_BALANCES_VECTOR // 2 == validator.withdrawable_epoch: - penalty = max( - validator.effective_balance * min(total_penalties * 3, total_balance) // total_balance, - validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT - ) + if validator.slashed and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch: + penalty = validator.effective_balance * min(sum(state.slashings) * 3, total_balance) // total_balance decrease_balance(state, ValidatorIndex(index), penalty) ``` @@ -1532,10 +1524,8 @@ def process_final_updates(state: BeaconState) -> None: get_active_validator_indices(state, Epoch(next_epoch + ACTIVATION_EXIT_DELAY)) ) ) - # Set total slashed balances - state.slashed_balances[next_epoch % EPOCHS_PER_SLASHED_BALANCES_VECTOR] = ( - state.slashed_balances[current_epoch % EPOCHS_PER_SLASHED_BALANCES_VECTOR] - ) + # Reset slashings + state.slashings[next_epoch % EPOCHS_PER_SLASHINGS_VECTOR] = Gwei(0) # Set randao mix state.randao_mixes[next_epoch % EPOCHS_PER_HISTORICAL_VECTOR] = get_randao_mix(state, current_epoch) # Set historical root accumulator diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 47d1578c5..c29033fe3 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -453,7 +453,7 @@ def process_early_derived_secret_reveal(state: BeaconState, # Apply penalty proposer_index = get_beacon_proposer_index(state) whistleblower_index = reveal.masker_index - whistleblowing_reward = Gwei(penalty // WHISTLEBLOWING_REWARD_QUOTIENT) + whistleblowing_reward = Gwei(penalty // WHISTLEBLOWER_REWARD_QUOTIENT) proposer_reward = Gwei(whistleblowing_reward // PROPOSER_REWARD_QUOTIENT) increase_balance(state, proposer_index, proposer_reward) increase_balance(state, whistleblower_index, whistleblowing_reward - proposer_reward) diff --git a/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py b/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py index e2b50ea0b..e78e1a866 100644 --- a/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py +++ b/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py @@ -25,31 +25,56 @@ def run_attester_slashing_processing(spec, state, attester_slashing, valid=True) yield 'post', None return - slashed_index = attester_slashing.attestation_1.custody_bit_0_indices[0] - pre_slashed_balance = get_balance(state, slashed_index) + slashed_indices = ( + attester_slashing.attestation_1.custody_bit_0_indices + + attester_slashing.attestation_1.custody_bit_1_indices + ) proposer_index = spec.get_beacon_proposer_index(state) pre_proposer_balance = get_balance(state, proposer_index) + pre_slashings = {slashed_index: get_balance(state, slashed_index) for slashed_index in slashed_indices} + pre_withdrawalable_epochs = { + slashed_index: state.validators[slashed_index].withdrawable_epoch + for slashed_index in slashed_indices + } + + total_proposer_rewards = sum( + balance // spec.WHISTLEBLOWER_REWARD_QUOTIENT + for balance in pre_slashings.values() + ) # Process slashing spec.process_attester_slashing(state, attester_slashing) - slashed_validator = state.validators[slashed_index] + for slashed_index in slashed_indices: + pre_withdrawalable_epoch = pre_withdrawalable_epochs[slashed_index] + slashed_validator = state.validators[slashed_index] - # Check slashing - assert slashed_validator.slashed - assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH - assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + # Check slashing + assert slashed_validator.slashed + assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH + if pre_withdrawalable_epoch < spec.FAR_FUTURE_EPOCH: + expected_withdrawable_epoch = max( + pre_withdrawalable_epoch, + spec.get_current_epoch(state) + spec.EPOCHS_PER_SLASHINGS_VECTOR + ) + assert slashed_validator.withdrawable_epoch == expected_withdrawable_epoch + else: + assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + assert get_balance(state, slashed_index) < pre_slashings[slashed_index] - if slashed_index != proposer_index: - # lost whistleblower reward - assert get_balance(state, slashed_index) < pre_slashed_balance + if proposer_index not in slashed_indices: # gained whistleblower reward - assert get_balance(state, proposer_index) > pre_proposer_balance + assert get_balance(state, proposer_index) == pre_proposer_balance + total_proposer_rewards else: # gained rewards for all slashings, which may include others. And only lost that of themselves. - # Netto at least 0, if more people where slashed, a balance increase. - assert get_balance(state, slashed_index) >= pre_slashed_balance + expected_balance = ( + pre_proposer_balance + + total_proposer_rewards + - pre_slashings[proposer_index] // spec.MIN_SLASHING_PENALTY_QUOTIENT + ) + + assert get_balance(state, proposer_index) == expected_balance yield 'post', state @@ -82,6 +107,37 @@ def test_success_surround(spec, state): yield from run_attester_slashing_processing(spec, state, attester_slashing) +@with_all_phases +@always_bls +@spec_state_test +def test_success_already_exited_recent(spec, state): + attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=True) + slashed_indices = ( + attester_slashing.attestation_1.custody_bit_0_indices + + attester_slashing.attestation_1.custody_bit_1_indices + ) + for index in slashed_indices: + spec.initiate_validator_exit(state, index) + + yield from run_attester_slashing_processing(spec, state, attester_slashing) + + +@with_all_phases +@always_bls +@spec_state_test +def test_success_already_exited_long_ago(spec, state): + attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=True) + slashed_indices = ( + attester_slashing.attestation_1.custody_bit_0_indices + + attester_slashing.attestation_1.custody_bit_1_indices + ) + for index in slashed_indices: + spec.initiate_validator_exit(state, index) + state.validators[index].withdrawable_epoch = spec.get_current_epoch(state) + 2 + + yield from run_attester_slashing_processing(spec, state, attester_slashing) + + @with_all_phases @always_bls @spec_state_test From 2739767a7147f6b2efd6537189f3f3437b885058 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 28 Jun 2019 14:43:44 +0100 Subject: [PATCH 5/6] Hardened Eth 1.0 voting strategy (#1218) --- specs/validator/0_beacon-chain-validator.md | 31 +++++++++++++-------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/specs/validator/0_beacon-chain-validator.md b/specs/validator/0_beacon-chain-validator.md index aa8350b66..44b5e2a3c 100644 --- a/specs/validator/0_beacon-chain-validator.md +++ b/specs/validator/0_beacon-chain-validator.md @@ -221,19 +221,26 @@ epoch_signature = bls_sign( ##### Eth1 Data -`block.eth1_data` is a mechanism used by block proposers vote on a recent Ethereum 1.0 block hash and an associated deposit root found in the Ethereum 1.0 deposit contract. When consensus is formed, `state.eth1_data` is updated, and validator deposits up to this root can be processed. The deposit root can be calculated by calling the `get_deposit_root()` function of the deposit contract using the post-state of the block hash. +The `block.eth1_data` field is for block proposers to vote on recent Eth 1.0 data. This recent data contains an Eth 1.0 block hash as well as the associated deposit root (as calculated by the `get_deposit_root()` method of the deposit contract) and deposit count after execution of the corresponding Eth 1.0 block. If over half of the block proposers in the current Eth 1.0 voting period vote for the same `eth1_data` then `state.eth1_data` updates at the end of the voting period. Each deposit in `block.body.deposits` must verify against `state.eth1_data.eth1_deposit_root`. -* Let `D` be the list of `Eth1DataVote` objects `vote` in `state.eth1_data_votes` where: - * `vote.eth1_data.block_hash` is the hash of an Eth 1.0 block that is (i) part of the canonical chain, (ii) >= `ETH1_FOLLOW_DISTANCE` blocks behind the head, and (iii) newer than `state.eth1_data.block_hash`. - * `vote.eth1_data.deposit_count` is the deposit count of the Eth 1.0 deposit contract at the block defined by `vote.eth1_data.block_hash`. - * `vote.eth1_data.deposit_root` is the deposit root of the Eth 1.0 deposit contract at the block defined by `vote.eth1_data.block_hash`. -* If `D` is empty: - * Let `block_hash` be the block hash of the `ETH1_FOLLOW_DISTANCE`'th ancestor of the head of the canonical Eth 1.0 chain. - * Let `deposit_root` and `deposit_count` be the deposit root and deposit count of the Eth 1.0 deposit contract in the post-state of the block referenced by `block_hash` - * Let `best_vote_data = Eth1Data(block_hash=block_hash, deposit_root=deposit_root, deposit_count=deposit_count)`. -* If `D` is nonempty: - * Let `best_vote_data` be the `eth1_data` member of `D` that has the highest vote count (`D.count(eth1_data)`), breaking ties by favoring block hashes with higher associated block height. -* Set `block.eth1_data = best_vote_data`. +Let `get_eth1_data(distance: int) -> Eth1Data` be the (subjective) function that returns the Eth 1.0 data at distance `distance` relative to the Eth 1.0 head at the start of the current Eth 1.0 voting period. Let `previous_eth1_distance` be the distance relative to the Eth 1.0 block corresponding to `state.eth1_data.block_hash` at the start of the current Eth 1.0 voting period. An honest block proposer sets `block.eth1_data = get_eth1_vote(state, previous_eth1_distance)` where: + +```python +def get_eth1_vote(state: BeaconState, previous_eth1_distance: uint64) -> Eth1Data: + new_eth1_data = [get_eth1_data(distance) for distance in range(ETH1_FOLLOW_DISTANCE, 2 * ETH1_FOLLOW_DISTANCE)] + all_eth1_data = [get_eth1_data(distance) for distance in range(ETH1_FOLLOW_DISTANCE, previous_eth1_distance)] + + valid_votes = [] + for slot, vote in enumerate(state.eth1_data_votes): + period_tail = slot % SLOTS_PER_ETH1_VOTING_PERIOD >= integer_square_root(SLOTS_PER_ETH1_VOTING_PERIOD) + if vote in new_eth1_data or (period_tail and vote in all_eth1_data): + valid_votes.append(vote) + + return max(valid_votes, + key=lambda v: (valid_votes.count(v), -all_eth1_data.index(v)), # Tiebreak by smallest distance + default=get_eth1_data(ETH1_FOLLOW_DISTANCE), + ) +``` ##### Signature From dcb0244a4f99f50dfa82884a81bd14e1eabb316e Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Fri, 28 Jun 2019 10:19:59 -0500 Subject: [PATCH 6/6] get_attesting_indices set instead of sorted (#1225) --- specs/core/0_beacon-chain.md | 14 +++--- specs/core/1_custody-game.md | 94 ++++++++++++------------------------ 2 files changed, 39 insertions(+), 69 deletions(-) diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 61ef8b742..abca72ff8 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -866,13 +866,13 @@ def get_crosslink_committee(state: BeaconState, epoch: Epoch, shard: Shard) -> S ### `get_attesting_indices` ```python -def get_attesting_indices(state: BeaconState, data: AttestationData, bitfield: bytes) -> Sequence[ValidatorIndex]: +def get_attesting_indices(state: BeaconState, data: AttestationData, bitfield: bytes) -> Set[ValidatorIndex]: """ - Return the sorted attesting indices corresponding to ``data`` and ``bitfield``. + Return the set of attesting indices corresponding to ``data`` and ``bitfield``. """ committee = get_crosslink_committee(state, data.target.epoch, data.crosslink.shard) assert verify_bitfield(bitfield, len(committee)) - return sorted([index for i, index in enumerate(committee) if get_bitfield_bit(bitfield, i) == 0b1]) + return set(index for i, index in enumerate(committee) if get_bitfield_bit(bitfield, i) == 0b1) ``` ### `int_to_bytes` @@ -950,12 +950,12 @@ def convert_to_indexed(state: BeaconState, attestation: Attestation) -> IndexedA """ attesting_indices = get_attesting_indices(state, attestation.data, attestation.aggregation_bitfield) custody_bit_1_indices = get_attesting_indices(state, attestation.data, attestation.custody_bitfield) - assert set(custody_bit_1_indices).issubset(attesting_indices) - custody_bit_0_indices = [index for index in attesting_indices if index not in custody_bit_1_indices] + assert custody_bit_1_indices.issubset(attesting_indices) + custody_bit_0_indices = attesting_indices.difference(custody_bit_1_indices) return IndexedAttestation( - custody_bit_0_indices=custody_bit_0_indices, - custody_bit_1_indices=custody_bit_1_indices, + custody_bit_0_indices=sorted(custody_bit_0_indices), + custody_bit_1_indices=sorted(custody_bit_1_indices), data=attestation.data, signature=attestation.signature, ) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index c29033fe3..9b17b39ed 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -36,7 +36,7 @@ - [`get_custody_chunk_bit`](#get_custody_chunk_bit) - [`get_chunk_bits_root`](#get_chunk_bits_root) - [`get_randao_epoch_for_custody_period`](#get_randao_epoch_for_custody_period) - - [`get_validators_custody_reveal_period`](#get_validators_custody_reveal_period) + - [`get_reveal_period`](#get_reveal_period) - [`replace_empty_or_append`](#replace_empty_or_append) - [Per-block processing](#per-block-processing) - [Operations](#operations) @@ -224,7 +224,7 @@ Add the following fields to the end of the specified container objects. Fields w class Validator(Container): # next_custody_reveal_period is initialised to the custody period # (of the particular validator) in which the validator is activated - # = get_validators_custody_reveal_period(...) + # = get_reveal_period(...) next_custody_reveal_period: uint64 max_reveal_lateness: uint64 ``` @@ -299,17 +299,12 @@ def get_randao_epoch_for_custody_period(period: int, validator_index: ValidatorI return Epoch(next_period_start + CUSTODY_PERIOD_TO_RANDAO_PADDING) ``` -### `get_validators_custody_reveal_period` +### `get_reveal_period` ```python -def get_validators_custody_reveal_period(state: BeaconState, - validator_index: ValidatorIndex, - epoch: Epoch=None) -> int: +def get_reveal_period(state: BeaconState, validator_index: ValidatorIndex, epoch: Epoch=None) -> int: ''' - This function returns the reveal period for a given validator. - If no epoch is supplied, the current epoch is assumed. - Note: This function implicitly requires that validators are not removed from the - validator set in fewer than EPOCHS_PER_CUSTODY_PERIOD epochs + Return the reveal period for a given validator. ''' epoch = get_current_epoch(state) if epoch is None else epoch return (epoch + validator_index % EPOCHS_PER_CUSTODY_PERIOD) // EPOCHS_PER_CUSTODY_PERIOD @@ -340,17 +335,15 @@ Verify that `len(block.body.custody_key_reveals) <= MAX_CUSTODY_KEY_REVEALS`. For each `reveal` in `block.body.custody_key_reveals`, run the following function: ```python -def process_custody_key_reveal(state: BeaconState, - reveal: CustodyKeyReveal) -> None: +def process_custody_key_reveal(state: BeaconState, reveal: CustodyKeyReveal) -> None: """ Process ``CustodyKeyReveal`` operation. Note that this function mutates ``state``. """ - revealer = state.validators[reveal.revealer_index] epoch_to_sign = get_randao_epoch_for_custody_period(revealer.next_custody_reveal_period, reveal.revealed_index) - assert revealer.next_custody_reveal_period < get_validators_custody_reveal_period(state, reveal.revealed_index) + assert revealer.next_custody_reveal_period < get_reveal_period(state, reveal.revealed_index) # Revealed validator is active or exited, but not withdrawn assert is_slashable_validator(revealer, get_current_epoch(state)) @@ -368,11 +361,11 @@ def process_custody_key_reveal(state: BeaconState, ) # Decrement max reveal lateness if response is timely - if revealer.next_custody_reveal_period == get_validators_custody_reveal_period(state, reveal.revealer_index) - 2: + if revealer.next_custody_reveal_period == get_reveal_period(state, reveal.revealer_index) - 2: revealer.max_reveal_lateness -= MAX_REVEAL_LATENESS_DECREMENT revealer.max_reveal_lateness = max( revealer.max_reveal_lateness, - get_validators_custody_reveal_period(state, reveal.revealed_index) - revealer.next_custody_reveal_period + get_reveal_period(state, reveal.revealed_index) - revealer.next_custody_reveal_period ) # Process reveal @@ -394,13 +387,11 @@ Verify that `len(block.body.early_derived_secret_reveals) <= MAX_EARLY_DERIVED_S For each `reveal` in `block.body.early_derived_secret_reveals`, run the following function: ```python -def process_early_derived_secret_reveal(state: BeaconState, - reveal: EarlyDerivedSecretReveal) -> None: +def process_early_derived_secret_reveal(state: BeaconState, reveal: EarlyDerivedSecretReveal) -> None: """ Process ``EarlyDerivedSecretReveal`` operation. Note that this function mutates ``state``. """ - revealed_validator = state.validators[reveal.revealed_index] derived_secret_location = reveal.epoch % EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS @@ -470,8 +461,7 @@ Verify that `len(block.body.custody_chunk_challenges) <= MAX_CUSTODY_CHUNK_CHALL For each `challenge` in `block.body.custody_chunk_challenges`, run the following function: ```python -def process_chunk_challenge(state: BeaconState, - challenge: CustodyChunkChallenge) -> None: +def process_chunk_challenge(state: BeaconState, challenge: CustodyChunkChallenge) -> None: # Verify the attestation validate_indexed_attestation(state, convert_to_indexed(state, challenge.attestation)) # Verify it is not too late to challenge @@ -514,59 +504,41 @@ Verify that `len(block.body.custody_bit_challenges) <= MAX_CUSTODY_BIT_CHALLENGE For each `challenge` in `block.body.custody_bit_challenges`, run the following function: ```python -def process_bit_challenge(state: BeaconState, - challenge: CustodyBitChallenge) -> None: +def process_bit_challenge(state: BeaconState, challenge: CustodyBitChallenge) -> None: + attestation = challenge.attestation + epoch = slot_to_epoch(attestation.data.slot) + shard = attestation.data.crosslink.shard # Verify challenge signature challenger = state.validators[challenge.challenger_index] - assert bls_verify( - pubkey=challenger.pubkey, - message_hash=signing_root(challenge), - signature=challenge.signature, - domain=get_domain(state, DOMAIN_CUSTODY_BIT_CHALLENGE, get_current_epoch(state)), - ) + domain = get_domain(state, DOMAIN_CUSTODY_BIT_CHALLENGE, get_current_epoch(state)) + assert bls_verify(challenger.pubkey, signing_root(challenge), challenge.signature, domain) + # Verify challenger is slashable assert is_slashable_validator(challenger, get_current_epoch(state)) - - # Verify the attestation - attestation = challenge.attestation + # Verify attestation validate_indexed_attestation(state, convert_to_indexed(state, attestation)) - # Verify the attestation is eligible for challenging + # Verify attestation is eligible for challenging responder = state.validators[challenge.responder_index] - assert (slot_to_epoch(attestation.data.slot) + responder.max_reveal_lateness <= - get_validators_custody_reveal_period(state, challenge.responder_index)) - - # Verify the responder participated in the attestation + assert epoch + responder.max_reveal_lateness <= get_reveal_period(state, challenge.responder_index) + # Verify responder participated in the attestation attesters = get_attesting_indices(state, attestation.data, attestation.aggregation_bitfield) assert challenge.responder_index in attesters - - # A validator can be the challenger for at most one challenge at a time + # Verifier challenger is not already challenging for record in state.custody_bit_challenge_records: assert record.challenger_index != challenge.challenger_index - - # Verify the responder is a valid custody key + # Verify the responder custody key epoch_to_sign = get_randao_epoch_for_custody_period( - get_validators_custody_reveal_period( - state, - challenge.responder_index, - epoch=slot_to_epoch(attestation.data.slot)), - challenge.responder_index + get_reveal_period(state, challenge.responder_index, epoch), + challenge.responder_index, ) - assert bls_verify( - pubkey=responder.pubkey, - message_hash=hash_tree_root(epoch_to_sign), - signature=challenge.responder_key, - domain=get_domain( - state=state, - domain_type=DOMAIN_RANDAO, - message_epoch=epoch_to_sign, - ), - ) - + domain = get_domain(state, DOMAIN_RANDAO, epoch_to_sign) + assert bls_verify(responder.pubkey, hash_tree_root(epoch_to_sign), challenge.responder_key, domain) # Verify the chunk count chunk_count = get_custody_chunk_count(attestation.data.crosslink) assert verify_bitfield(challenge.chunk_bits, chunk_count) # Verify the first bit of the hash of the chunk bits does not equal the custody bit - custody_bit = get_bitfield_bit(attestation.custody_bitfield, attesters.index(challenge.responder_index)) + committee = get_crosslink_committee(state, epoch, shard) + custody_bit = get_bitfield_bit(attestation.custody_bitfield, committee.index(challenge.responder_index)) assert custody_bit != get_bitfield_bit(get_chunk_bits_root(challenge.chunk_bits), 0) # Add new bit challenge record new_record = CustodyBitChallengeRecord( @@ -581,7 +553,6 @@ def process_bit_challenge(state: BeaconState, ) replace_empty_or_append(state.custody_bit_challenge_records, new_record) state.custody_challenge_index += 1 - # Postpone responder withdrawability responder.withdrawable_epoch = FAR_FUTURE_EPOCH ``` @@ -593,8 +564,7 @@ Verify that `len(block.body.custody_responses) <= MAX_CUSTODY_RESPONSES`. For each `response` in `block.body.custody_responses`, run the following function: ```python -def process_custody_response(state: BeaconState, - response: CustodyResponse) -> None: +def process_custody_response(state: BeaconState, response: CustodyResponse) -> 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: @@ -682,7 +652,7 @@ Run `process_reveal_deadlines(state)` immediately after `process_registry_update def process_reveal_deadlines(state: BeaconState) -> None: for index, validator in enumerate(state.validators): deadline = validator.next_custody_reveal_period + (CUSTODY_RESPONSE_DEADLINE // EPOCHS_PER_CUSTODY_PERIOD) - if get_validators_custody_reveal_period(state, ValidatorIndex(index)) > deadline: + if get_reveal_period(state, ValidatorIndex(index)) > deadline: slash_validator(state, ValidatorIndex(index)) ```