From c0f345309393eb8842fbd96fce65a71d5245ce62 Mon Sep 17 00:00:00 2001 From: dankrad Date: Fri, 3 May 2019 11:30:55 +0200 Subject: [PATCH] RANDAO reveal slashing, custody period staggering and integration of custody and RANDAO reveals (#880) * Add RandaoRevealSlashing for early Randao reveals * add some randao reveal slashing tests * Unifying RANDAO reveal slashing and Custody reveal slashing; implemented more lenient penalty for not-to-early reveals * Fix custody reveal domain * Also test exposed_randao_reveals * Re-add exposed_randao_reveals to validator initialization * Fix tests * Unified Randao Reveal slashing mechanism and Custody Reveal, CUSTODY_PERIOD_TO_RANDAO_PADDING = EPOCHS_PER_CUSTODY_PERIOD * Move exposed_randao_reveals into separate data structure to keep validator record clean * new shiny staggered custody periods * Fixes style and type hinting * removes whitespace * Cleans up multi-line conditionals * Make RANDAO key reveal penalties proportional to block rewards * Minor typos * Minor typos * Fixes off-by one error * Removes unnecicary whitepsace * Clean up comments; add test for key reveal too far in the future * Reduce the CUSTODY_PERIOD_TO_RANDAO_PADDING again * Fix max_proposer_slot_reward * Fix types * Move test * Fix RandaoKeyReveal tests * Move all RANDAO key reveal to phase 1 * Factor out signature checking * Some fixes * Update specs/core/1_custody-game.md Co-Authored-By: dankrad * Addressing Vitalik's suggestions: Separate RANDAO and Custody key reveals; append the cleanup of RANDAO reveals instead of adding a new function * Remove remnants of verify_custody_key * RandaoKeyReveal -> EarlyDerivedSecretReveal * Make penalty proportional to number of secrets already exposed * Update specs/core/1_custody-game.md Co-Authored-By: dankrad * Update specs/core/1_custody-game.md Co-Authored-By: dankrad * Update specs/core/1_custody-game.md Co-Authored-By: dankrad --- specs/core/1_custody-game.md | 315 +++++++++++++----- .../phase1_test_process_randao_key_reveal.py | 109 ++++++ test_libs/pyspec/tests/helpers_phase1.py | 50 +++ 3 files changed, 397 insertions(+), 77 deletions(-) create mode 100644 test_libs/pyspec/tests/block_processing_phase1/phase1_test_process_randao_key_reveal.py create mode 100644 test_libs/pyspec/tests/helpers_phase1.py diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index 0f1555e62..d56526611 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -14,6 +14,7 @@ - [Misc](#misc) - [Time parameters](#time-parameters) - [Max operations per block](#max-operations-per-block) + - [Reward and penalty quotients](#reward-and-penalty-quotients) - [Signature domains](#signature-domains) - [Data structures](#data-structures) - [Custody objects](#custody-objects) @@ -22,7 +23,9 @@ - [`CustodyChunkChallengeRecord`](#custodychunkchallengerecord) - [`CustodyBitChallengeRecord`](#custodybitchallengerecord) - [`CustodyResponse`](#custodyresponse) + - [New Beacon operations](#new-beacon-operations) - [`CustodyKeyReveal`](#custodykeyreveal) + - [`EarlyDerivedSecretReveal`](#earlyderivedsecretreveal) - [Phase 0 container updates](#phase-0-container-updates) - [`Validator`](#validator) - [`BeaconState`](#beaconstate) @@ -32,17 +35,19 @@ - [`empty`](#empty) - [`get_crosslink_chunk_count`](#get_crosslink_chunk_count) - [`get_custody_chunk_bit`](#get_custody_chunk_bit) + - [`get_randao_epoch_for_custody_period`](#get_randao_epoch_for_custody_period) + - [`get_validators_custody_reveal_period`](#get_validators_custody_reveal_period) - [`get_chunk_bits_root`](#get_chunk_bits_root) - - [`epoch_to_custody_period`](#epoch_to_custody_period) - [`replace_empty_or_append`](#replace_empty_or_append) - - [`verify_custody_key`](#verify_custody_key) - [Per-block processing](#per-block-processing) - [Operations](#operations) - - [Custody reveals](#custody-reveals) + - [Custody key reveals](#custody-key-reveals) + - [Early derived secret reveals](#early-derived-secret-reveals) - [Chunk challenges](#chunk-challenges) - [Bit challenges](#bit-challenges) - [Custody responses](#custody-responses) - [Per-epoch processing](#per-epoch-processing) + - [Handling of custody-related deadlines](#handling-of-custody-related-deadlines) @@ -80,24 +85,32 @@ 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 | -| `EPOCHS_PER_CUSTODY_PERIOD` | `2**11` (= 2,048) | epochs | ~9 days | | `CUSTODY_RESPONSE_DEADLINE` | `2**14` (= 16,384) | epochs | ~73 days | +| `RANDAO_PENALTY_EPOCHS` | `2**1` (= 2) | epochs | 12.8 minutes | +| `EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS` | `2**14` | epochs | ~73 days | +| `EPOCHS_PER_CUSTODY_PERIOD` | `2**11` (= 2,048) | epochs | ~9 days | +| `CUSTODY_PERIOD_TO_RANDAO_PADDING` | `2**11` (= 2,048) | epochs | ~9 days | +| `MAX_REVEAL_LATENESS_DECREMENT` | `2**7` (= 128) | epochs | ~14 hours | ### Max operations per block | Name | Value | | - | - | | `MAX_CUSTODY_KEY_REVEALS` | `2**4` (= 16) | +| `MAX_EARLY_DERIVED_SECRET_REVEALS` | `1` | | `MAX_CUSTODY_CHUNK_CHALLENGES` | `2**2` (= 4) | | `MAX_CUSTODY_BIT_CHALLENGES` | `2**2` (= 4) | | `MAX_CUSTODY_RESPONSES` | `2**5` (= 32) | +### Reward and penalty quotients + +| `EARLY_DERIVED_SECRET_REVEAL_SLOT_REWARD_MULTIPLE` | `2**1` (= 2) | + ### Signature domains | Name | Value | | - | - | -| `DOMAIN_CUSTODY_KEY_REVEAL` | `6` | -| `DOMAIN_CUSTODY_BIT_CHALLENGE` | `7` | +| `DOMAIN_CUSTODY_BIT_CHALLENGE` | `6` | ## Data structures @@ -168,15 +181,35 @@ This document details the beacon chain additions and changes in Phase 1 of Ether } ``` +### New Beacon operations + #### `CustodyKeyReveal` ```python { - 'revealer_index': ValidatorIndex, - 'period': 'uint64', - 'key': BLSSignature, - 'masker_index': ValidatorIndex, - 'mask': Hash, + # Index of the validator whose key is being revealed + 'revealer_index': 'uint64', + # Reveal (masked signature) + 'reveal': 'bytes96', +} +``` + +#### `EarlyDerivedSecretReveal` + +Represents an early (punishable) reveal of one of the derived secrets, where derived secrets are RANDAO reveals and custody reveals (both are part of the same domain). + +```python +{ + # Index of the validator whose key is being revealed + 'revealed_index': 'uint64', + # RANDAO epoch of the key that is being revealed + 'epoch': 'uint64', + # Reveal (masked signature) + 'reveal': 'bytes96', + # Index of the validator who revealed (whistleblower) + 'masker_index': 'uint64', + # Mask used to hide the actual reveal signature (prevent reveal from being stolen) + 'mask': 'bytes32', } ``` @@ -187,7 +220,10 @@ Add the following fields to the end of the specified container objects. Fields w #### `Validator` ```python - 'custody_reveal_index': 'uint64', + # 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(...) + 'next_custody_reveal_period': 'uint64', 'max_reveal_lateness': 'uint64', ``` @@ -197,15 +233,20 @@ Add the following fields to the end of the specified container objects. Fields w 'custody_chunk_challenge_records': [CustodyChunkChallengeRecord], 'custody_bit_challenge_records': [CustodyBitChallengeRecord], 'custody_challenge_index': 'uint64', + + # Future derived secrets already exposed; contains the indices of the exposed validator + # at RANDAO reveal period % EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS + 'exposed_derived_secrets': [['uint64'], EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS], ``` #### `BeaconBlockBody` ```python - 'custody_key_reveals': [CustodyKeyReveal], 'custody_chunk_challenges': [CustodyChunkChallenge], 'custody_bit_challenges': [CustodyBitChallenge], 'custody_responses': [CustodyResponse], + 'custody_key_reveals': [CustodyKeyReveal], + 'early_derived_secret_reveals': [EarlyDerivedSecretReveal], ``` ## Helpers @@ -248,15 +289,33 @@ def get_chunk_bits_root(chunk_bitfield: Bitfield) -> Bytes32: return hash(aggregated_bits) ``` -### `epoch_to_custody_period` +### `get_randao_epoch_for_custody_period` ```python -def epoch_to_custody_period(epoch: Epoch) -> int: - return epoch // EPOCHS_PER_CUSTODY_PERIOD +def get_randao_epoch_for_custody_period(period: int, validator_index: ValidatorIndex) -> Epoch: + next_period_start = (period + 1) * EPOCHS_PER_CUSTODY_PERIOD - validator_index % EPOCHS_PER_CUSTODY_PERIOD + return next_period_start + CUSTODY_PERIOD_TO_RANDAO_PADDING +``` + +### `get_validators_custody_reveal_period` + + ```python +def get_validators_custody_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 + ''' + epoch = get_current_epoch(state) if epoch is None else epoch + return (epoch + validator_index % EPOCHS_PER_CUSTODY_PERIOD) // EPOCHS_PER_CUSTODY_PERIOD ``` ### `replace_empty_or_append` + ```python def replace_empty_or_append(list: List[Any], new_element: Any) -> int: for i in range(len(list)): @@ -267,68 +326,131 @@ def replace_empty_or_append(list: List[Any], new_element: Any) -> int: return len(list) - 1 ``` -### `verify_custody_key` - -```python -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)] - - # 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.key, - domain=get_domain( - fork=state.fork, - epoch=reveal.period * EPOCHS_PER_CUSTODY_PERIOD, - domain_type=DOMAIN_CUSTODY_KEY_REVEAL, - ), - ) -``` - ## Per-block processing ### Operations Add the following operations to the per-block processing, in order the given below and after all other operations in phase 0. -#### Custody reveals +#### Custody key reveals 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_reveal(state: BeaconState, +def process_custody_key_reveal(state: BeaconState, reveal: CustodyKeyReveal) -> None: - assert verify_custody_key(state, reveal) + + """ + Process ``CustodyKeyReveal`` operation. + Note that this function mutates ``state``. + """ + revealer = state.validator_registry[reveal.revealer_index] - current_custody_period = epoch_to_custody_period(get_current_epoch(state)) + epoch_to_sign = get_randao_epoch_for_custody_period(revealer.next_custody_reveal_period, reveal.revealed_index) - # 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_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_reveal_index += 1 - revealer.max_reveal_lateness = max(revealer.max_reveal_lateness, current_custody_period - reveal.period) - proposer_index = get_beacon_proposer_index(state) - increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) + assert revealer.next_custody_reveal_period < get_validators_custody_reveal_period(state, reveal.revealed_index) - # Case 2: masked punitive early reveal + # Revealed validator is active or exited, but not withdrawn + assert is_slashable_validator(revealer, get_current_epoch(state)) + + # Verify signature + assert bls_verify( + pubkey=revealer.pubkey, + message_hash=hash_tree_root(epoch_to_sign), + signature=reveal.reveal, + domain=get_domain( + state=state, + domain_type=DOMAIN_RANDAO, + message_epoch=epoch_to_sign, + ), + ) + + # Decrement max reveal lateness if response is timely + if revealer.next_custody_reveal_period == get_validators_custody_reveal_period(state, reveal.revealer_index) - 2: + revealer.max_reveal_lateness -= MAX_REVEAL_LATENESS_DECREMENT + revealer.max_reveal_lateness = max(revealed_validator.max_reveal_lateness, get_validators_custody_reveal_period(state, reveal.revealed_index) - revealer.next_custody_reveal_period) + + # Process reveal + revealer.next_custody_reveal_period += 1 + + # Reward Block Preposer + proposer_index = get_beacon_proposer_index(state) + increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT) +``` + +##### Early derived secret reveals + +Verify that `len(block.body.early_derived_secret_reveals) <= MAX_EARLY_DERIVED_SECRET_REVEALS`. + +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: + """ + Process ``EarlyDerivedSecretReveal`` operation. + Note that this function mutates ``state``. + """ + + revealed_validator = state.validator_registry[reveal.revealed_index] + masker = state.validator_registry[reveal.masker_index] + + assert reveal.epoch >= get_current_epoch(state) + RANDAO_PENALTY_EPOCHS + assert reveal.epoch < get_current_epoch(state) + EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS + assert revealed_validator.slashed is False + assert reveal.revealed_index not in state.exposed_derived_secrets[reveal.epoch % EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS] + + # Verify signature correctness + masker = state.validator_registry[reveal.masker_index] + pubkeys = [revealed_validator.pubkey, masker.pubkey] + message_hashes = [ + hash_tree_root(reveal.epoch), + reveal.mask, + ] + + assert bls_verify_multiple( + pubkeys=pubkeys, + message_hashes=message_hashes, + signature=reveal.reveal, + domain=get_domain( + state=state, + domain_type=DOMAIN_RANDAO, + message_epoch=reveal.epoch, + ), + ) + + if reveal.epoch >= get_current_epoch(state) + CUSTODY_PERIOD_TO_RANDAO_PADDING: + # Full slashing when the secret was revealed so early it may be a valid custody + # round key + slash_validator(state, reveal.revealed_index, reveal.masker_index) else: - assert reveal.period > current_custody_period - assert revealer.slashed is False - slash_validator(state, reveal.revealer_index, reveal.masker_index) + # Only a small penalty proportional to proposer slot reward for RANDAO reveal + # that does not interfere with the custody period + # The penalty is proportional to the max proposer reward + + # Calculate penalty + max_proposer_slot_reward = ( + get_base_reward(state, reveal.revealed_index) * + SLOTS_PER_EPOCH // + len(get_active_validator_indices(state, get_current_epoch(state))) // + PROPOSER_REWARD_QUOTIENT + ) + penalty = max_proposer_slot_reward * EARLY_DERIVED_SECRET_REVEAL_SLOT_REWARD_MULTIPLE * (len(state.exposed_derived_secrets[reveal.epoch % EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS]) + 1) + + # Apply penalty + proposer_index = get_beacon_proposer_index(state) + whistleblower_index = reveal.masker_index + whistleblowing_reward = penalty // WHISTLEBLOWING_REWARD_QUOTIENT + proposer_reward = whistleblowing_reward // PROPOSER_REWARD_QUOTIENT + increase_balance(state, proposer_index, proposer_reward) + increase_balance(state, whistleblower_index, whistleblowing_reward - proposer_reward) + decrease_balance(state, reveal.revealed_index, penalty) + + # Mark this derived secret as exposed so validator cannot be punished repeatedly + state.exposed_derived_secrets[reveal.epoch % EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS].append(reveal.revealed_index) + ``` #### Chunk challenges @@ -384,6 +506,7 @@ For each `challenge` in `block.body.custody_bit_challenges`, run the following f ```python def process_bit_challenge(state: BeaconState, challenge: CustodyBitChallenge) -> None: + # Verify challenge signature challenger = state.validator_registry[challenge.challenger_index] assert bls_verify( @@ -392,29 +515,43 @@ def process_bit_challenge(state: BeaconState, signature=challenge.signature, domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_BIT_CHALLENGE), ) - # Verify the challenger is not slashed - assert challenger.slashed is False + assert is_slashable_validator(challenger, get_current_epoch(state)) + # Verify the attestation assert verify_indexed_attestation(state, convert_to_indexed(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) + assert (slot_to_epoch(challenge.attestation.data.slot) + responder.max_reveal_lateness <= + get_validators_custody_reveal_period(state, challenge.responder_index)) + # Verify the 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 or responder for at most one challenge at a time 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)), - key=challenge.responder_key, - masker_index=0, - mask=ZERO_HASH, - )) + + # Verify the responder is a valid custody key + epoch_to_sign = get_randao_epoch_for_custody_period( + get_validators_custody_reveal_period( + state=state, + index=challenge.responder_index, + epoch=slot_to_epoch(attestation.data.slot), + 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, + ), + ) + # Verify the chunk count chunk_count = get_custody_chunk_count(challenge.attestation) assert verify_bitfield(challenge.chunk_bits, chunk_count) @@ -426,7 +563,7 @@ def process_bit_challenge(state: BeaconState, challenge_index=state.custody_challenge_index, challenger_index=challenge.challenger_index, responder_index=challenge.responder_index, - deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE + deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE, crosslink_data_root=challenge.attestation.data.crosslink_data_root, chunk_count=chunk_count, chunk_bits_merkle_root=merkle_root(pad_to_power_of_2((challenge.chunk_bits))), @@ -434,6 +571,7 @@ 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 ``` @@ -515,7 +653,20 @@ def process_bit_challenge_response(state: BeaconState, ## Per-epoch processing -Run `process_challenge_deadlines(state)` immediately after `process_ejections(state)`: +### Handling of custody-related deadlines + + Run `process_reveal_deadlines(state)` immediately after `process_ejections(state)`: + + ```python +def process_reveal_deadlines(state: BeaconState) -> None: + for index, validator in enumerate(state.validator_registry): + if (validator.latest_custody_reveal_period + + (CUSTODY_RESPONSE_DEADLINE // EPOCHS_PER_CUSTODY_PERIOD) < + get_validators_custody_reveal_period(state, index)): + slash_validator(state, index) +``` + +Run `process_challenge_deadlines(state)` immediately after `process_reveal_deadlines(state)`: ```python def process_challenge_deadlines(state: BeaconState) -> None: @@ -532,16 +683,26 @@ def process_challenge_deadlines(state: BeaconState) -> None: records[records.index(challenge)] = CustodyBitChallengeRecord() ``` +Append this to `process_final_updates(state)`: + +```python + # Clean up exposed RANDAO key reveals + state.exposed_derived_secrets[current_epoch % EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS] = [] +``` + 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): +def eligible(state: BeaconState, index: ValidatorIndex) -> bool: validator = state.validator_registry[index] # Cannot exit if there are still open chunk challenges if len([record for record in state.custody_chunk_challenge_records if record.responder_index == index]) > 0: return False + # Cannot exit if there are still open bit challenges + if len([record for record in state.custody_bit_challenge_records if record.responder_index == index]) > 0: + return False # 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): + elif validator.next_custody_reveal_period <= get_validators_custody_reveal_period(state, index, validator.exit_epoch): return False # Cannot exit if you already have elif validator.withdrawable_epoch < FAR_FUTURE_EPOCH: diff --git a/test_libs/pyspec/tests/block_processing_phase1/phase1_test_process_randao_key_reveal.py b/test_libs/pyspec/tests/block_processing_phase1/phase1_test_process_randao_key_reveal.py new file mode 100644 index 000000000..0be8ab4a9 --- /dev/null +++ b/test_libs/pyspec/tests/block_processing_phase1/phase1_test_process_randao_key_reveal.py @@ -0,0 +1,109 @@ +from copy import deepcopy +import pytest + +import eth2spec.phase1.spec as spec +from eth2spec.phase1.spec import ( + get_current_epoch, + process_randao_key_reveal, + RANDAO_PENALTY_EPOCHS, + CUSTODY_PERIOD_TO_RANDAO_PADDING, + RANDAO_PENALTY_MAX_FUTURE_EPOCHS, +) +from tests.helpers_phase1 import ( + get_valid_randao_key_reveal, +) + +mark entire file as 'randao_key_reveals' +pytestmark = pytest.mark.randao_key_reveals + + +def run_randao_key_reveal_processing(state, randao_key_reveal, valid=True): + """ + Run ``process_randao_key_reveal`` returning the pre and post state. + If ``valid == False``, run expecting ``AssertionError`` + """ + post_state = deepcopy(state) + + if not valid: + with pytest.raises(AssertionError): + process_randao_key_reveal(post_state, randao_key_reveal) + return state, None + + process_randao_key_reveal(post_state, randao_key_reveal) + + slashed_validator = post_state.validator_registry[randao_key_reveal.revealed_index] + + if randao_key_reveal.epoch >= get_current_epoch(state) + CUSTODY_PERIOD_TO_RANDAO_PADDING: + assert slashed_validator.slashed + assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + # lost whistleblower reward + # FIXME: Currently broken because get_base_reward in genesis epoch is 0 + assert ( + post_state.balances[randao_key_reveal.revealed_index] < + state.balances[randao_key_reveal.revealed_index] + ) + + return state, post_state + + +def test_success(state): + randao_key_reveal = get_valid_randao_key_reveal(state) + + pre_state, post_state = run_randao_key_reveal_processing(state, randao_key_reveal) + + return pre_state, randao_key_reveal, post_state + + +def test_reveal_from_current_epoch(state): + randao_key_reveal = get_valid_randao_key_reveal(state, get_current_epoch(state)) + + pre_state, post_state = run_randao_key_reveal_processing(state, randao_key_reveal, False) + + return pre_state, randao_key_reveal, post_state + +# Not currently possible as we are testing at epoch 0 +# +#def test_reveal_from_past_epoch(state): +# randao_key_reveal = get_valid_randao_key_reveal(state, get_current_epoch(state) - 1) +# +# pre_state, post_state = run_randao_key_reveal_processing(state, randao_key_reveal, False) +# +# return pre_state, randao_key_reveal, post_state + +def test_reveal_with_custody_padding(state): + randao_key_reveal = get_valid_randao_key_reveal(state, get_current_epoch(state) + CUSTODY_PERIOD_TO_RANDAO_PADDING) + pre_state, post_state = run_randao_key_reveal_processing(state, randao_key_reveal, True) + + return pre_state, randao_key_reveal, post_state + +def test_reveal_with_custody_padding_minus_one(state): + randao_key_reveal = get_valid_randao_key_reveal(state, get_current_epoch(state) + CUSTODY_PERIOD_TO_RANDAO_PADDING - 1) + pre_state, post_state = run_randao_key_reveal_processing(state, randao_key_reveal, True) + + return pre_state, randao_key_reveal, post_state + +def test_double_reveal(state): + + randao_key_reveal1 = get_valid_randao_key_reveal(state, get_current_epoch(state) + RANDAO_PENALTY_EPOCHS + 1) + pre_state, intermediate_state = run_randao_key_reveal_processing(state, randao_key_reveal1) + + randao_key_reveal2 = get_valid_randao_key_reveal(intermediate_state, get_current_epoch(pre_state) + RANDAO_PENALTY_EPOCHS + 1) + intermediate_state_, post_state = run_randao_key_reveal_processing(intermediate_state, randao_key_reveal2, False) + + return pre_state, [randao_key_reveal1, randao_key_reveal2], post_state + +def test_revealer_is_slashed(state): + randao_key_reveal = get_valid_randao_key_reveal(state, get_current_epoch(state)) + state.validator_registry[randao_key_reveal.revealed_index].slashed = True + + pre_state, post_state = run_randao_key_reveal_processing(state, randao_key_reveal, False) + + return pre_state, randao_key_reveal, post_state + +def test_far_future_epoch(state): + randao_key_reveal = get_valid_randao_key_reveal(state, get_current_epoch(state) + RANDAO_PENALTY_MAX_FUTURE_EPOCHS) + + pre_state, post_state = run_randao_key_reveal_processing(state, randao_key_reveal, False) + + return pre_state, randao_key_reveal, post_state diff --git a/test_libs/pyspec/tests/helpers_phase1.py b/test_libs/pyspec/tests/helpers_phase1.py new file mode 100644 index 000000000..aba93e159 --- /dev/null +++ b/test_libs/pyspec/tests/helpers_phase1.py @@ -0,0 +1,50 @@ +from py_ecc import bls + +import eth2spec.phase1.spec as spec +from eth2spec.phase0.spec import ( + # constants + ZERO_HASH, + CUSTODY_PERIOD_TO_RANDAO_PADDING, + # SSZ + RandaoKeyReveal, + # functions + get_active_validator_indices, + get_current_epoch, + get_domain, + hash_tree_root, +) + +def get_valid_randao_key_reveal(state, epoch=None): + current_epoch = get_current_epoch(state) + revealed_index = get_active_validator_indices(state, current_epoch)[-1] + masker_index = get_active_validator_indices(state, current_epoch)[0] + + if epoch is None: + epoch = current_epoch + CUSTODY_PERIOD_TO_RANDAO_PADDING + + reveal = bls.sign( + message_hash=hash_tree_root(epoch), + privkey=pubkey_to_privkey[state.validator_registry[revealed_index].pubkey], + domain=get_domain( + state=state, + domain_type=spec.DOMAIN_RANDAO, + message_epoch=epoch, + ), + ) + mask = bls.sign( + message_hash=hash_tree_root(epoch), + privkey=pubkey_to_privkey[state.validator_registry[masker_index].pubkey], + domain=get_domain( + state=state, + domain_type=spec.DOMAIN_RANDAO, + message_epoch=epoch, + ), + ) + + return RandaoKeyReveal( + revealed_index=revealed_index, + epoch=epoch, + reveal=reveal, + masker_index=masker_index, + mask=mask, + )