716 lines
27 KiB
Markdown
716 lines
27 KiB
Markdown
# Ethereum 2.0 Phase 1 -- Custody Game
|
|
|
|
**Notice**: This document is a work-in-progress for researchers and implementers.
|
|
|
|
## Table of contents
|
|
|
|
<!-- TOC -->
|
|
|
|
- [Ethereum 2.0 Phase 1 -- Custody Game](#ethereum-20-phase-1----custody-game)
|
|
- [Table of contents](#table-of-contents)
|
|
- [Introduction](#introduction)
|
|
- [Terminology](#terminology)
|
|
- [Constants](#constants)
|
|
- [Misc](#misc)
|
|
- [Time parameters](#time-parameters)
|
|
- [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)
|
|
- [`CustodyChunkChallenge`](#custodychunkchallenge)
|
|
- [`CustodyBitChallenge`](#custodybitchallenge)
|
|
- [`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)
|
|
- [`BeaconBlockBody`](#beaconblockbody)
|
|
- [Helpers](#helpers)
|
|
- [`typeof`](#typeof)
|
|
- [`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)
|
|
- [`replace_empty_or_append`](#replace_empty_or_append)
|
|
- [Per-block processing](#per-block-processing)
|
|
- [Operations](#operations)
|
|
- [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 -->
|
|
|
|
## Introduction
|
|
|
|
This document details the beacon chain additions and changes in Phase 1 of Ethereum 2.0 to support the shard data custody game, building upon the [Phase 0](0_beacon-chain.md) specification.
|
|
|
|
## Terminology
|
|
|
|
* **Custody game**—
|
|
* **Custody period**—
|
|
* **Custody chunk**—
|
|
* **Custody chunk bit**—
|
|
* **Custody chunk challenge**—
|
|
* **Custody bit**—
|
|
* **Custody bit challenge**—
|
|
* **Custody key**—
|
|
* **Custody key reveal**—
|
|
* **Custody key mask**—
|
|
* **Custody response**—
|
|
* **Custody response deadline**—
|
|
|
|
## Constants
|
|
|
|
### Misc
|
|
|
|
| Name | Value |
|
|
| - | - |
|
|
| `BYTES_PER_SHARD_BLOCK` | `2**14` (= 16,384) |
|
|
| `BYTES_PER_CUSTODY_CHUNK` | `2**9` (= 512) |
|
|
| `MINOR_REWARD_QUOTIENT` | `2**8` (= 256) |
|
|
|
|
### Time parameters
|
|
|
|
| Name | Value | Unit | Duration |
|
|
| - | - | :-: | :-: |
|
|
| `MAX_CHUNK_CHALLENGE_DELAY` | `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
|
|
|
|
| Name | Value |
|
|
| - | - |
|
|
| `EARLY_DERIVED_SECRET_REVEAL_SLOT_REWARD_MULTIPLE` | `2**1` (= 2) |
|
|
|
|
### Signature domains
|
|
|
|
| Name | Value |
|
|
| - | - |
|
|
| `DOMAIN_CUSTODY_BIT_CHALLENGE` | `6` |
|
|
|
|
## Data structures
|
|
|
|
### Custody objects
|
|
|
|
#### `CustodyChunkChallenge`
|
|
|
|
```python
|
|
{
|
|
'responder_index': ValidatorIndex,
|
|
'attestation': Attestation,
|
|
'chunk_index': 'uint64',
|
|
}
|
|
```
|
|
|
|
#### `CustodyBitChallenge`
|
|
|
|
```python
|
|
{
|
|
'responder_index': ValidatorIndex,
|
|
'attestation': Attestation,
|
|
'challenger_index': ValidatorIndex,
|
|
'responder_key': BLSSignature,
|
|
'chunk_bits': Bitfield,
|
|
'signature': BLSSignature,
|
|
}
|
|
```
|
|
|
|
#### `CustodyChunkChallengeRecord`
|
|
|
|
```python
|
|
{
|
|
'challenge_index': 'uint64',
|
|
'challenger_index': ValidatorIndex,
|
|
'responder_index': ValidatorIndex,
|
|
'deadline': Epoch,
|
|
'data_root': Hash,
|
|
'depth': 'uint64',
|
|
'chunk_index': 'uint64',
|
|
}
|
|
```
|
|
|
|
#### `CustodyBitChallengeRecord`
|
|
|
|
```python
|
|
{
|
|
'challenge_index': 'uint64',
|
|
'challenger_index': ValidatorIndex,
|
|
'responder_index': ValidatorIndex,
|
|
'deadline': Epoch,
|
|
'data_root': Hash,
|
|
'chunk_count': 'uint64',
|
|
'chunk_bits_merkle_root': Hash,
|
|
'responder_key': BLSSignature,
|
|
}
|
|
```
|
|
|
|
#### `CustodyResponse`
|
|
|
|
```python
|
|
{
|
|
'challenge_index': 'uint64',
|
|
'chunk_index': 'uint64',
|
|
'chunk': ['byte', BYTES_PER_CUSTODY_CHUNK],
|
|
'data_branch': [Hash],
|
|
'chunk_bits_branch': [Hash],
|
|
'chunk_bits_leaf': Hash,
|
|
}
|
|
```
|
|
|
|
### New beacon operations
|
|
|
|
#### `CustodyKeyReveal`
|
|
|
|
```python
|
|
{
|
|
# 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',
|
|
}
|
|
```
|
|
|
|
### Phase 0 container updates
|
|
|
|
Add the following fields to the end of the specified container objects. Fields with underlying type `uint64` are initialized to `0` and list fields are initialized to `[]`.
|
|
|
|
#### `Validator`
|
|
|
|
```python
|
|
# next_custody_reveal_period is initialized 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',
|
|
```
|
|
|
|
#### `BeaconState`
|
|
|
|
```python
|
|
'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_chunk_challenges': [CustodyChunkChallenge],
|
|
'custody_bit_challenges': [CustodyBitChallenge],
|
|
'custody_responses': [CustodyResponse],
|
|
'custody_key_reveals': [CustodyKeyReveal],
|
|
'early_derived_secret_reveals': [EarlyDerivedSecretReveal],
|
|
```
|
|
|
|
## Helpers
|
|
|
|
### `typeof`
|
|
|
|
The `typeof` function accepts and SSZ object as a single input and returns the corresponding SSZ type.
|
|
|
|
### `empty`
|
|
|
|
The `empty` function accepts and SSZ type as input and returns an object of that type with all fields initialized to default values.
|
|
|
|
### `get_crosslink_chunk_count`
|
|
|
|
```python
|
|
def get_custody_chunk_count(attestation: Attestation) -> int:
|
|
crosslink_start_epoch = attestation.data.latest_crosslink.epoch
|
|
crosslink_end_epoch = slot_to_epoch(attestation.data.slot)
|
|
crosslink_crosslink_length = min(MAX_EPOCHS_PER_CROSSLINK, end_epoch - start_epoch)
|
|
chunks_per_epoch = 2 * BYTES_PER_SHARD_BLOCK * SLOTS_PER_EPOCH // BYTES_PER_CUSTODY_CHUNK
|
|
return crosslink_crosslink_length * chunks_per_epoch
|
|
```
|
|
|
|
### `get_custody_chunk_bit`
|
|
|
|
```python
|
|
def get_custody_chunk_bit(key: BLSSignature, chunk: bytes) -> bool:
|
|
# TODO: Replace with something MPC-friendly, e.g. the Legendre symbol
|
|
return get_bitfield_bit(hash(challenge.responder_key + chunk), 0)
|
|
```
|
|
|
|
### `get_chunk_bits_root`
|
|
|
|
```python
|
|
def get_chunk_bits_root(chunk_bitfield: Bitfield) -> Bytes32:
|
|
aggregated_bits = bytearray([0] * 32)
|
|
for i in range(0, len(chunk_bitfield), 32):
|
|
for j in range(32):
|
|
aggregated_bits[j] ^= chunk_bitfield[i+j]
|
|
return hash(aggregated_bits)
|
|
```
|
|
|
|
### `get_randao_epoch_for_custody_period`
|
|
|
|
```python
|
|
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)):
|
|
if list[i] == empty(typeof(new_element)):
|
|
list[i] = new_element
|
|
return i
|
|
list.append(new_element)
|
|
return len(list) - 1
|
|
```
|
|
|
|
## Per-block processing
|
|
|
|
### Operations
|
|
|
|
Add the following operations to the per-block processing, in the order given below and after all other operations in Phase 0.
|
|
|
|
#### 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_key_reveal(state: BeaconState,
|
|
reveal: CustodyKeyReveal) -> None:
|
|
|
|
"""
|
|
Process ``CustodyKeyReveal`` operation.
|
|
Note that this function mutates ``state``.
|
|
"""
|
|
|
|
revealer = state.validator_registry[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)
|
|
|
|
# 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:
|
|
# 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
|
|
|
|
Verify that `len(block.body.custody_chunk_challenges) <= MAX_CUSTODY_CHUNK_CHALLENGES`.
|
|
|
|
For each `challenge` in `block.body.custody_chunk_challenges`, run the following function:
|
|
|
|
```python
|
|
def process_chunk_challenge(state: BeaconState,
|
|
challenge: CustodyChunkChallenge) -> None:
|
|
# Verify the attestation
|
|
assert verify_indexed_attestation(state, convert_to_indexed(state, challenge.attestation))
|
|
# Verify it is not too late to challenge
|
|
assert slot_to_epoch(challenge.attestation.data.slot) >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY
|
|
responder = state.validator_registry[challenge.responder_index]
|
|
assert responder.exit_epoch >= get_current_epoch(state) - MAX_CHUNK_CHALLENGE_DELAY
|
|
# Verify the responder participated in the attestation
|
|
attesters = get_attesting_indices(state, attestation.data, attestation.aggregation_bitfield)
|
|
assert challenge.responder_index in attesters
|
|
# Verify the challenge is not a duplicate
|
|
for record in state.custody_chunk_challenge_records:
|
|
assert (
|
|
record.data_root != challenge.attestation.data.crosslink.data_root or
|
|
record.chunk_index != challenge.chunk_index
|
|
)
|
|
# Verify depth
|
|
depth = math.log2(next_power_of_two(get_custody_chunk_count(challenge.attestation)))
|
|
assert challenge.chunk_index < 2**depth
|
|
# Add new chunk challenge record
|
|
new_record = CustodyChunkChallengeRecord(
|
|
challenge_index=state.custody_challenge_index,
|
|
challenger_index=get_beacon_proposer_index(state),
|
|
responder_index=challenge.responder_index
|
|
deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE,
|
|
data_root=challenge.attestation.data.crosslink.data_root,
|
|
depth=depth,
|
|
chunk_index=challenge.chunk_index,
|
|
)
|
|
replace_empty_or_append(state.custody_chunk_challenge_records, new_record)
|
|
|
|
state.custody_challenge_index += 1
|
|
# Postpone responder withdrawability
|
|
responder.withdrawable_epoch = FAR_FUTURE_EPOCH
|
|
```
|
|
|
|
#### Bit challenges
|
|
|
|
Verify that `len(block.body.custody_bit_challenges) <= MAX_CUSTODY_BIT_CHALLENGES`.
|
|
|
|
For each `challenge` in `block.body.custody_bit_challenges`, run the following function:
|
|
|
|
```python
|
|
def process_bit_challenge(state: BeaconState,
|
|
challenge: CustodyBitChallenge) -> None:
|
|
|
|
# Verify challenge signature
|
|
challenger = state.validator_registry[challenge.challenger_index]
|
|
assert bls_verify(
|
|
pubkey=challenger.pubkey,
|
|
message_hash=signing_root(challenge),
|
|
signature=challenge.signature,
|
|
domain=get_domain(state, get_current_epoch(state), DOMAIN_CUSTODY_BIT_CHALLENGE),
|
|
)
|
|
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]
|
|
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 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)
|
|
# 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(responder_index))
|
|
assert custody_bit != get_bitfield_bit(get_chunk_bits_root(challenge.chunk_bits), 0)
|
|
# Add new bit challenge record
|
|
new_record = CustodyBitChallengeRecord(
|
|
challenge_index=state.custody_challenge_index,
|
|
challenger_index=challenge.challenger_index,
|
|
responder_index=challenge.responder_index,
|
|
deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE,
|
|
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))),
|
|
responder_key=challenge.responder_key,
|
|
)
|
|
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
|
|
```
|
|
|
|
#### Custody responses
|
|
|
|
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:
|
|
chunk_challenge = next(record for record in state.custody_chunk_challenge_records if record.challenge_index == response.challenge_index, None)
|
|
if chunk_challenge is not None:
|
|
return process_chunk_challenge_response(state, response, chunk_challenge)
|
|
|
|
bit_challenge = next(record for record in state.custody_bit_challenge_records if record.challenge_index == response.challenge_index, None)
|
|
if bit_challenge is not None:
|
|
return process_bit_challenge_response(state, response, bit_challenge)
|
|
|
|
assert False
|
|
```
|
|
|
|
```python
|
|
def process_chunk_challenge_response(state: BeaconState,
|
|
response: CustodyResponse,
|
|
challenge: CustodyChunkChallengeRecord) -> None:
|
|
# Verify chunk index
|
|
assert response.chunk_index == challenge.chunk_index
|
|
# Verify bit challenge data is null
|
|
assert response.chunk_bits_branch == [] and response.chunk_bits_leaf == ZERO_HASH
|
|
# Verify the chunk matches the crosslink data root
|
|
assert verify_merkle_branch(
|
|
leaf=hash_tree_root(response.chunk),
|
|
branch=response.data_branch,
|
|
depth=challenge.depth,
|
|
index=response.chunk_index,
|
|
root=challenge.data_root,
|
|
)
|
|
# Clear the challenge
|
|
records = state.custody_chunk_challenge_records
|
|
records[records.index(challenge)] = CustodyChunkChallengeRecord()
|
|
# Reward the proposer
|
|
proposer_index = get_beacon_proposer_index(state)
|
|
increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT)
|
|
```
|
|
|
|
```python
|
|
def process_bit_challenge_response(state: BeaconState,
|
|
response: CustodyResponse,
|
|
challenge: CustodyBitChallengeRecord) -> None:
|
|
# Verify chunk index
|
|
assert response.chunk_index < challenge.chunk_count
|
|
# Verify the chunk matches the crosslink data root
|
|
assert verify_merkle_branch(
|
|
leaf=hash_tree_root(response.chunk),
|
|
branch=response.data_branch,
|
|
depth=math.log2(next_power_of_two(challenge.chunk_count)),
|
|
index=response.chunk_index,
|
|
root=challenge.data_root,
|
|
)
|
|
# Verify the chunk bit leaf matches the challenge data
|
|
assert verify_merkle_branch(
|
|
leaf=response.chunk_bits_leaf,
|
|
branch=response.chunk_bits_branch,
|
|
depth=math.log2(next_power_of_two(challenge.chunk_count) // 256),
|
|
index=response.chunk_index // 256,
|
|
root=challenge.chunk_bits_merkle_root
|
|
)
|
|
# Verify the chunk bit does not match the challenge chunk bit
|
|
assert get_custody_chunk_bit(challenge.responder_key, response.chunk) != get_bitfield_bit(challenge.chunk_bits_leaf, response.chunk_index % 256)
|
|
# Clear the challenge
|
|
records = state.custody_bit_challenge_records
|
|
records[records.index(challenge)] = CustodyBitChallengeRecord()
|
|
# Slash challenger
|
|
slash_validator(state, challenge.challenger_index, challenge.responder_index)
|
|
```
|
|
|
|
## Per-epoch processing
|
|
|
|
### 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:
|
|
for challenge in state.custody_chunk_challenge_records:
|
|
if get_current_epoch(state) > challenge.deadline:
|
|
slash_validator(state, challenge.responder_index, challenge.challenger_index)
|
|
records = state.custody_chunk_challenge_records
|
|
records[records.index(challenge)] = CustodyChunkChallengeRecord()
|
|
|
|
for challenge in state.custody_bit_challenge_records:
|
|
if get_current_epoch(state) > challenge.deadline:
|
|
slash_validator(state, challenge.responder_index, challenge.challenger_index)
|
|
records = state.custody_bit_challenge_records
|
|
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(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 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:
|
|
return False
|
|
# Return minimum time
|
|
else:
|
|
return current_epoch >= validator.exit_epoch + MIN_VALIDATOR_WITHDRAWAL_EPOCHS
|
|
```
|