515 lines
21 KiB
Markdown
515 lines
21 KiB
Markdown
# Ethereum 2.0 Phase 1 -- Custody Game
|
|
|
|
**Notice**: This document is a work-in-progress for researchers and implementers.
|
|
|
|
## Table of contents
|
|
|
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
|
|
|
|
- [Introduction](#introduction)
|
|
- [Constants](#constants)
|
|
- [Misc](#misc)
|
|
- [Configuration](#configuration)
|
|
- [Time parameters](#time-parameters)
|
|
- [Max operations per block](#max-operations-per-block)
|
|
- [Reward and penalty quotients](#reward-and-penalty-quotients)
|
|
- [Signature domain types](#signature-domain-types)
|
|
- [Data structures](#data-structures)
|
|
- [New Beacon Chain operations](#new-beacon-chain-operations)
|
|
- [Helpers](#helpers)
|
|
- [`replace_empty_or_append`](#replace_empty_or_append)
|
|
- [`legendre_bit`](#legendre_bit)
|
|
- [`get_custody_atoms`](#get_custody_atoms)
|
|
- [`get_custody_secrets`](#get_custody_secrets)
|
|
- [`compute_custody_bit`](#compute_custody_bit)
|
|
- [`get_randao_epoch_for_custody_period`](#get_randao_epoch_for_custody_period)
|
|
- [`get_custody_period_for_validator`](#get_custody_period_for_validator)
|
|
- [Per-block processing](#per-block-processing)
|
|
- [Custody Game Operations](#custody-game-operations)
|
|
- [Chunk challenges](#chunk-challenges)
|
|
- [Custody chunk response](#custody-chunk-response)
|
|
- [Custody key reveals](#custody-key-reveals)
|
|
- [Early derived secret reveals](#early-derived-secret-reveals)
|
|
- [Custody Slashings](#custody-slashings)
|
|
- [Per-epoch processing](#per-epoch-processing)
|
|
- [Handling of reveal deadlines](#handling-of-reveal-deadlines)
|
|
- [Final updates](#final-updates)
|
|
|
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
|
|
## 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](../phase0/beacon-chain.md) specification.
|
|
|
|
## Constants
|
|
|
|
### Misc
|
|
|
|
| Name | Value | Unit |
|
|
| - | - | - |
|
|
| `CUSTODY_PRIME` | `2 ** 256 - 189` | - |
|
|
| `CUSTODY_SECRETS` | `3` | - |
|
|
| `BYTES_PER_CUSTODY_ATOM` | `32` | bytes |
|
|
|
|
## Configuration
|
|
|
|
### Time parameters
|
|
|
|
| Name | Value | Unit | Duration |
|
|
| - | - | :-: | :-: |
|
|
| `RANDAO_PENALTY_EPOCHS` | `2**1` (= 2) | epochs | 12.8 minutes |
|
|
| `EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS` | `2**14` (= 16,384) | 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 |
|
|
| `CHUNK_RESPONSE_DEADLINE` | `2**14` (= 16,384) | epochs | ~73 days |
|
|
| `MAX_CHUNK_CHALLENGE_DELAY` | `2**11` (= 16,384) | epochs | ~9 days |
|
|
| `CUSTODY_RESPONSE_DEADLINE` | `2**14` (= 16,384) | epochs | ~73 days |
|
|
|
|
### Max operations per block
|
|
|
|
| Name | Value |
|
|
| - | - |
|
|
| `MAX_CUSTODY_KEY_REVEALS` | `2**8` (= 256) |
|
|
| `MAX_EARLY_DERIVED_SECRET_REVEALS` | `1` |
|
|
| `MAX_CUSTODY_CHUNK_CHALLENGES` | `2**2` (= 4) |
|
|
| `MAX_CUSTODY_SLASHINGS` | `1` |
|
|
|
|
### Reward and penalty quotients
|
|
|
|
| Name | Value |
|
|
| - | - |
|
|
| `EARLY_DERIVED_SECRET_REVEAL_SLOT_REWARD_MULTIPLE` | `2**1` (= 2) |
|
|
| `MINOR_REWARD_QUOTIENT` | `2**8` (= 256) |
|
|
|
|
### Signature domain types
|
|
|
|
The following types are defined, mapping into `DomainType` (little endian):
|
|
|
|
| Name | Value |
|
|
| - | - |
|
|
| `DOMAIN_CUSTODY_BIT_SLASHING` | `DomainType('0x83000000')` |
|
|
|
|
## Data structures
|
|
|
|
### New Beacon Chain operations
|
|
|
|
## Helpers
|
|
|
|
### `replace_empty_or_append`
|
|
|
|
```python
|
|
def replace_empty_or_append(list: List, 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
|
|
```
|
|
|
|
### `legendre_bit`
|
|
|
|
Returns the Legendre symbol `(a/q)` normalizes as a bit (i.e. `((a/q) + 1) // 2`). In a production implementation, a well-optimized library (e.g. GMP) should be used for this.
|
|
|
|
```python
|
|
def legendre_bit(a: int, q: int) -> int:
|
|
if a >= q:
|
|
return legendre_bit(a % q, q)
|
|
if a == 0:
|
|
return 0
|
|
assert(q > a > 0 and q % 2 == 1)
|
|
t = 1
|
|
n = q
|
|
while a != 0:
|
|
while a % 2 == 0:
|
|
a //= 2
|
|
r = n % 8
|
|
if r == 3 or r == 5:
|
|
t = -t
|
|
a, n = n, a
|
|
if a % 4 == n % 4 == 3:
|
|
t = -t
|
|
a %= n
|
|
if n == 1:
|
|
return (t + 1) // 2
|
|
else:
|
|
return 0
|
|
```
|
|
|
|
### `get_custody_atoms`
|
|
|
|
Given one set of data, return the custody atoms: each atom will be combined with one legendre bit.
|
|
|
|
```python
|
|
def get_custody_atoms(bytez: bytes) -> Sequence[bytes]:
|
|
bytez += b'\x00' * (-len(bytez) % BYTES_PER_CUSTODY_ATOM) # right-padding
|
|
return [bytez[i:i + BYTES_PER_CUSTODY_ATOM]
|
|
for i in range(0, len(bytez), BYTES_PER_CUSTODY_ATOM)]
|
|
```
|
|
|
|
### `get_custody_secrets`
|
|
|
|
Extract the custody secrets from the signature
|
|
|
|
```python
|
|
def get_custody_secrets(key: BLSSignature) -> Sequence[int]:
|
|
full_G2_element = bls.signature_to_G2(key)
|
|
signature = full_G2_element[0].coeffs
|
|
signature_bytes = b"".join(x.to_bytes(48, "little") for x in signature)
|
|
secrets = [int.from_bytes(signature_bytes[i:i + BYTES_PER_CUSTODY_ATOM], "little")
|
|
for i in range(0, len(signature_bytes), 32)]
|
|
return secrets
|
|
```
|
|
|
|
### `compute_custody_bit`
|
|
|
|
```python
|
|
def compute_custody_bit(key: BLSSignature, data: ByteList[MAX_SHARD_BLOCK_SIZE]) -> bit:
|
|
secrets = get_custody_secrets(key)
|
|
custody_atoms = get_custody_atoms(data)
|
|
n = len(custody_atoms)
|
|
uhf = (sum(secrets[i % CUSTODY_SECRETS]**i * int.from_bytes(atom, "little") % CUSTODY_PRIME
|
|
for i, atom in enumerate(custody_atoms)) + secrets[n % CUSTODY_SECRETS]**n) % CUSTODY_PRIME
|
|
return legendre_bit(uhf + secrets[0], CUSTODY_PRIME)
|
|
```
|
|
|
|
### `get_randao_epoch_for_custody_period`
|
|
|
|
```python
|
|
def get_randao_epoch_for_custody_period(period: uint64, validator_index: ValidatorIndex) -> Epoch:
|
|
next_period_start = (period + 1) * EPOCHS_PER_CUSTODY_PERIOD - validator_index % EPOCHS_PER_CUSTODY_PERIOD
|
|
return Epoch(next_period_start + CUSTODY_PERIOD_TO_RANDAO_PADDING)
|
|
```
|
|
|
|
### `get_custody_period_for_validator`
|
|
|
|
```python
|
|
def get_custody_period_for_validator(validator_index: ValidatorIndex, epoch: Epoch) -> int:
|
|
'''
|
|
Return the reveal period for a given validator.
|
|
'''
|
|
return (epoch + validator_index % EPOCHS_PER_CUSTODY_PERIOD) // EPOCHS_PER_CUSTODY_PERIOD
|
|
```
|
|
|
|
|
|
## Per-block processing
|
|
|
|
### Custody Game Operations
|
|
|
|
```python
|
|
def process_custody_game_operations(state: BeaconState, body: BeaconBlockBody) -> None:
|
|
def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
|
|
for operation in operations:
|
|
fn(state, operation)
|
|
|
|
for_ops(body.custody_key_reveals, process_custody_key_reveal)
|
|
for_ops(body.early_derived_secret_reveals, process_early_derived_secret_reveal)
|
|
for_ops(body.custody_slashings, process_custody_slashing)
|
|
```
|
|
|
|
#### 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 is_valid_indexed_attestation(state, get_indexed_attestation(state, challenge.attestation))
|
|
# Verify it is not too late to challenge
|
|
assert (challenge.attestation.data.target.epoch + MAX_CHUNK_CHALLENGE_DELAY
|
|
>= get_current_epoch(state))
|
|
responder = state.validators[challenge.responder_index]
|
|
assert (responder.exit_epoch == FAR_FUTURE_EPOCH
|
|
or responder.exit_epoch + MAX_CHUNK_CHALLENGE_DELAY >= get_current_epoch(state))
|
|
# Verify responder is slashable
|
|
assert is_slashable_validator(responder, get_current_epoch(state))
|
|
# Verify the responder participated in the attestation
|
|
attesters = get_attesting_indices(state, challenge.attestation.data, challenge.attestation.aggregation_bits)
|
|
assert challenge.responder_index in attesters
|
|
# Verify shard transition is correctly given
|
|
assert hash_tree_root(challenge.shard_transition) == challenge.attestation.data.shard_transition_root
|
|
data_root = challenge.shard_transition.shard_data_roots[challenge.data_index]
|
|
# 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
|
|
transition_chunks = (challenge.shard_transition.shard_block_lengths[challenge.data_index] + BYTES_PER_CUSTODY_CHUNK - 1) // BYTES_PER_CUSTODY_CHUNK
|
|
assert challenge.chunk_index < transition_chunks
|
|
# Add new chunk challenge record
|
|
new_record = CustodyChunkChallengeRecord(
|
|
challenge_index=state.custody_chunk_challenge_index,
|
|
challenger_index=get_beacon_proposer_index(state),
|
|
responder_index=challenge.responder_index,
|
|
inclusion_epoch=get_current_epoch(state),
|
|
data_root=challenge.shard_transition.shard_data_roots[challenge.data_index],
|
|
chunk_index=challenge.chunk_index,
|
|
)
|
|
replace_empty_or_append(state.custody_chunk_challenge_records, new_record)
|
|
|
|
state.custody_chunk_challenge_index += 1
|
|
# Postpone responder withdrawability
|
|
responder.withdrawable_epoch = FAR_FUTURE_EPOCH
|
|
```
|
|
|
|
#### Custody chunk response
|
|
|
|
```python
|
|
def process_chunk_challenge_response(state: BeaconState,
|
|
response: CustodyChunkResponse) -> None:
|
|
|
|
challenge = next((record for record in state.custody_chunk_challenge_records if record.challenge_index == response.challenge_index), None)
|
|
assert(challenge is not None)
|
|
|
|
# Verify chunk index
|
|
assert response.chunk_index == challenge.chunk_index
|
|
# Verify the chunk matches the crosslink data root
|
|
assert is_valid_merkle_branch(
|
|
leaf=hash_tree_root(response.chunk),
|
|
branch=response.branch,
|
|
depth=CUSTODY_RESPONSE_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, get_base_reward(state, proposer_index) // MINOR_REWARD_QUOTIENT)
|
|
```
|
|
|
|
#### Custody key reveals
|
|
|
|
```python
|
|
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_secret_to_reveal, reveal.revealer_index)
|
|
|
|
custody_reveal_period = get_custody_period_for_validator(reveal.revealer_index, get_current_epoch(state))
|
|
# Only past custody periods can be revealed, except after exiting the exit
|
|
# period can be revealed
|
|
assert (revealer.next_custody_secret_to_reveal < custody_reveal_period
|
|
or (revealer.exit_epoch <= get_current_epoch(state) and
|
|
revealer.next_custody_secret_to_reveal
|
|
<= get_custody_period_for_validator(reveal.revealer_index, revealer.exit_epoch - 1)))
|
|
|
|
# Revealed validator is active or exited, but not withdrawn
|
|
assert is_slashable_validator(revealer, get_current_epoch(state))
|
|
|
|
# Verify signature
|
|
domain = get_domain(state, DOMAIN_RANDAO, epoch_to_sign)
|
|
signing_root = compute_signing_root(epoch_to_sign, domain)
|
|
assert bls.Verify(revealer.pubkey, signing_root, reveal.reveal)
|
|
|
|
# Process reveal
|
|
if (revealer.exit_epoch <= get_current_epoch(state) and
|
|
revealer.next_custody_secret_to_reveal
|
|
== get_custody_period_for_validator(reveal.revealer_index, revealer.exit_epoch - 1)):
|
|
revealer.all_custody_secrets_revealed_epoch = get_current_epoch(state)
|
|
revealer.next_custody_secret_to_reveal += 1
|
|
|
|
# Reward Block Proposer
|
|
proposer_index = get_beacon_proposer_index(state)
|
|
increase_balance(
|
|
state,
|
|
proposer_index,
|
|
Gwei(get_base_reward(state, reveal.revealer_index) // MINOR_REWARD_QUOTIENT)
|
|
)
|
|
```
|
|
|
|
#### Early derived secret reveals
|
|
|
|
```python
|
|
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
|
|
|
|
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 not revealed_validator.slashed
|
|
assert reveal.revealed_index not in state.exposed_derived_secrets[derived_secret_location]
|
|
|
|
# Verify signature correctness
|
|
masker = state.validators[reveal.masker_index]
|
|
pubkeys = [revealed_validator.pubkey, masker.pubkey]
|
|
|
|
domain = get_domain(state, DOMAIN_RANDAO, reveal.epoch)
|
|
signing_roots = [compute_signing_root(root, domain) for root in [hash_tree_root(reveal.epoch), reveal.mask]]
|
|
assert bls.AggregateVerify(zip(pubkeys, signing_roots), reveal.reveal)
|
|
|
|
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 = Gwei(
|
|
max_proposer_slot_reward
|
|
* EARLY_DERIVED_SECRET_REVEAL_SLOT_REWARD_MULTIPLE
|
|
* (len(state.exposed_derived_secrets[derived_secret_location]) + 1)
|
|
)
|
|
|
|
# Apply penalty
|
|
proposer_index = get_beacon_proposer_index(state)
|
|
whistleblower_index = reveal.masker_index
|
|
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)
|
|
decrease_balance(state, reveal.revealed_index, penalty)
|
|
|
|
# Mark this derived secret as exposed so validator cannot be punished repeatedly
|
|
state.exposed_derived_secrets[derived_secret_location].append(reveal.revealed_index)
|
|
```
|
|
|
|
#### Custody Slashings
|
|
|
|
```python
|
|
def process_custody_slashing(state: BeaconState, signed_custody_slashing: SignedCustodySlashing) -> None:
|
|
custody_slashing = signed_custody_slashing.message
|
|
attestation = custody_slashing.attestation
|
|
|
|
# Any signed custody-slashing should result in at least one slashing.
|
|
# If the custody bits are valid, then the claim itself is slashed.
|
|
malefactor = state.validators[custody_slashing.malefactor_index]
|
|
whistleblower = state.validators[custody_slashing.whistleblower_index]
|
|
domain = get_domain(state, DOMAIN_CUSTODY_BIT_SLASHING, get_current_epoch(state))
|
|
signing_root = compute_signing_root(custody_slashing, domain)
|
|
assert bls.Verify(whistleblower.pubkey, signing_root, signed_custody_slashing.signature)
|
|
# Verify that the whistleblower is slashable
|
|
assert is_slashable_validator(whistleblower, get_current_epoch(state))
|
|
# Verify that the claimed malefactor is slashable
|
|
assert is_slashable_validator(malefactor, get_current_epoch(state))
|
|
|
|
# Verify the attestation
|
|
assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation))
|
|
|
|
# TODO: custody_slashing.data is not chunked like shard blocks yet, result is lots of padding.
|
|
# ??? What does this mean?
|
|
|
|
# TODO: can do a single combined merkle proof of data being attested.
|
|
# Verify the shard transition is indeed attested by the attestation
|
|
shard_transition = custody_slashing.shard_transition
|
|
assert hash_tree_root(shard_transition) == attestation.data.shard_transition_root
|
|
# Verify that the provided data matches the shard-transition
|
|
assert custody_slashing.data.get_backing().get_left().merkle_root() == shard_transition.shard_data_roots[custody_slashing.data_index]
|
|
assert len(custody_slashing.data) == shard_transition.shard_block_lengths[custody_slashing.data_index]
|
|
|
|
# Verify existence and participation of claimed malefactor
|
|
attesters = get_attesting_indices(state, attestation.data, attestation.aggregation_bits)
|
|
assert custody_slashing.malefactor_index in attesters
|
|
|
|
# Verify the malefactor custody key
|
|
epoch_to_sign = get_randao_epoch_for_custody_period(
|
|
get_custody_period_for_validator(custody_slashing.malefactor_index, attestation.data.target.epoch),
|
|
custody_slashing.malefactor_index,
|
|
)
|
|
domain = get_domain(state, DOMAIN_RANDAO, epoch_to_sign)
|
|
signing_root = compute_signing_root(epoch_to_sign, domain)
|
|
assert bls.Verify(malefactor.pubkey, signing_root, custody_slashing.malefactor_secret)
|
|
|
|
# Get the custody bit
|
|
custody_bits = attestation.custody_bits_blocks[custody_slashing.data_index]
|
|
committee = get_beacon_committee(state, attestation.data.slot, attestation.data.index)
|
|
claimed_custody_bit = custody_bits[committee.index(custody_slashing.malefactor_index)]
|
|
|
|
# Compute the custody bit
|
|
computed_custody_bit = compute_custody_bit(custody_slashing.malefactor_secret, custody_slashing.data)
|
|
|
|
# Verify the claim
|
|
if claimed_custody_bit != computed_custody_bit:
|
|
# Slash the malefactor, reward the other committee members
|
|
slash_validator(state, custody_slashing.malefactor_index)
|
|
others_count = len(committee) - 1
|
|
whistleblower_reward = Gwei(malefactor.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT // others_count)
|
|
for attester_index in attesters:
|
|
if attester_index != custody_slashing.malefactor_index:
|
|
increase_balance(state, attester_index, whistleblower_reward)
|
|
# No special whisteblower reward: it is expected to be an attester. Others are free to slash too however.
|
|
else:
|
|
# The claim was false, the custody bit was correct. Slash the whistleblower that induced this work.
|
|
slash_validator(state, custody_slashing.whistleblower_index)
|
|
```
|
|
|
|
|
|
## Per-epoch processing
|
|
|
|
### Handling of reveal deadlines
|
|
|
|
Run `process_reveal_deadlines(state)` after `process_registry_updates(state)`:
|
|
|
|
```python
|
|
def process_reveal_deadlines(state: BeaconState) -> None:
|
|
epoch = get_current_epoch(state)
|
|
for index, validator in enumerate(state.validators):
|
|
if get_custody_period_for_validator(ValidatorIndex(index), epoch) > validator.next_custody_secret_to_reveal + (CUSTODY_RESPONSE_DEADLINE // EPOCHS_PER_CUSTODY_PERIOD):
|
|
slash_validator(state, ValidatorIndex(index))
|
|
```
|
|
|
|
Run `process_challenge_deadlines(state)` immediately after `process_reveal_deadlines(state)`:
|
|
|
|
```python
|
|
# begin insert @process_challenge_deadlines
|
|
process_challenge_deadlines(state)
|
|
# end insert @process_challenge_deadlines
|
|
def process_challenge_deadlines(state: BeaconState) -> None:
|
|
for custody_chunk_challenge in state.custody_chunk_challenge_records:
|
|
if get_current_epoch(state) > custody_chunk_challenge.inclusion_epoch + CUSTODY_RESPONSE_DEADLINE:
|
|
slash_validator(state, custody_chunk_challenge.responder_index, custody_chunk_challenge.challenger_index)
|
|
records = state.custody_chunk_challenge_records
|
|
records[records.index(custody_chunk_challenge)] = CustodyChunkChallengeRecord()
|
|
```
|
|
|
|
### Final updates
|
|
|
|
After `process_final_updates(state)`, additional updates are made for the custody game:
|
|
|
|
```python
|
|
def process_custody_final_updates(state: BeaconState) -> None:
|
|
# Clean up exposed RANDAO key reveals
|
|
state.exposed_derived_secrets[get_current_epoch(state) % EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS] = []
|
|
|
|
# Reset withdrawable epochs if challenge records are empty
|
|
records = state.custody_chunk_challenge_records
|
|
validator_indices_in_records = set(
|
|
[record.responder_index for record in records]
|
|
)
|
|
for index, validator in enumerate(state.validators):
|
|
if validator.exit_epoch != FAR_FUTURE_EPOCH:
|
|
if (index in validator_indices_in_records
|
|
or validator.all_custody_secrets_revealed_epoch == FAR_FUTURE_EPOCH):
|
|
# Delay withdrawable epochs if challenge records are not empty or not all
|
|
# custody secrets revealed
|
|
validator.withdrawable_epoch = FAR_FUTURE_EPOCH
|
|
else:
|
|
# Reset withdrawable epochs if challenge records are empty
|
|
if validator.withdrawable_epoch == FAR_FUTURE_EPOCH:
|
|
validator.withdrawable_epoch = Epoch(validator.all_custody_secrets_revealed_epoch
|
|
+ MIN_VALIDATOR_WITHDRAWABILITY_DELAY)
|
|
```
|