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 <dankrad@ethereum.org> * 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 <dankrad@ethereum.org> * Update specs/core/1_custody-game.md Co-Authored-By: dankrad <dankrad@ethereum.org> * Update specs/core/1_custody-game.md Co-Authored-By: dankrad <dankrad@ethereum.org>
This commit is contained in:
parent
4ca2f11827
commit
c0f3453093
|
@ -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)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
)
|
Loading…
Reference in New Issue