18 KiB
Ethereum 2.0 Phase 1 -- Custody Game
NOTICE: This spec is a work-in-progress for researchers and implementers.
Table of contents
- Ethereum 2.0 Phase 1 -- Custody Game
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 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 |
EPOCHS_PER_CUSTODY_PERIOD |
2**11 (= 2,048) |
epochs | ~9 days |
CUSTODY_RESPONSE_DEADLINE |
2**14 (= 16,384) |
epochs | ~73 days |
Max transactions per block
Name | Value |
---|---|
MAX_CUSTODY_KEY_REVEALS |
2**4 (= 16) |
MAX_CUSTODY_CHUNK_CHALLENGES |
2**2 (= 4) |
MAX_CUSTODY_BIT_CHALLENGES |
2**2 (= 4) |
MAX_CUSTODY_RESPONSES |
2**5 (= 32) |
Signature domains
Name | Value |
---|---|
DOMAIN_CUSTODY_KEY_REVEAL |
6 |
DOMAIN_CUSTODY_BIT_CHALLENGE |
7 |
Data structures
Custody objects
CustodyChunkChallenge
{
'responder_index': ValidatorIndex,
'attestation': Attestation,
'chunk_index': 'uint64',
}
CustodyBitChallenge
{
'responder_index': ValidatorIndex,
'attestation': Attestation,
'challenger_index': ValidatorIndex,
'responder_key': BLSSignature,
'chunk_bits': Bitfield,
'signature': BLSSignature,
}
CustodyChunkChallengeRecord
{
'challenge_index': 'uint64',
'challenger_index': ValidatorIndex,
'responder_index': ValidatorIndex,
'deadline': Epoch,
'crosslink_data_root': Hash,
'depth': 'uint64',
'chunk_index': 'uint64',
}
CustodyBitChallengeRecord
{
'challenge_index': 'uint64',
'challenger_index': ValidatorIndex,
'responder_index': ValidatorIndex,
'deadline': Epoch,
'crosslink_data_root': Hash,
'chunk_bits': Bitfield,
'responder_key': BLSSignature,
}
CustodyResponse
{
'challenge_index': 'uint64',
'chunk_index': 'uint64',
'chunk': ['byte', BYTES_PER_CUSTODY_CHUNK],
'branch': [Hash],
}
CustodyKeyReveal
{
'revealer_index': ValidatorIndex,
'period': 'uint64',
'key': BLSSignature,
'masker_index': ValidatorIndex,
'mask': Hash,
}
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
'custody_reveal_index': 'uint64',
'max_reveal_lateness': 'uint64',
BeaconState
'custody_chunk_challenge_records': [CustodyChunkChallengeRecord],
'custody_bit_challenge_records': [CustodyBitChallengeRecord],
'custody_challenge_index': 'uint64',
BeaconBlockBody
'custody_key_reveals': [CustodyKeyReveal],
'custody_chunk_challenges': [CustodyChunkChallenge],
'custody_bit_challenges': [CustodyBitChallenge],
'custody_responses': [CustodyResponse],
Helpers
get_crosslink_chunk_count
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_CROSSLINK_EPOCHS, 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
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)
epoch_to_custody_period
def epoch_to_custody_period(epoch: Epoch) -> int:
return epoch // EPOCHS_PER_CUSTODY_PERIOD
verify_custody_key
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
Transactions
Add the following transactions to the per-block processing, in order the given below and after all other transactions in phase 0.
Custody 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:
def process_custody_reveal(state: BeaconState,
reveal: CustodyKeyReveal) -> None:
assert verify_custody_key(state, reveal)
revealer = state.validator_registry[reveal.revealer_index]
current_custody_period = epoch_to_custody_period(get_current_epoch(state))
# 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, state.slot)
increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT)
# Case 2: masked punitive early reveal
else:
assert reveal.period > current_custody_period
assert revealer.slashed is False
slash_validator(state, reveal.revealer_index, reveal.masker_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:
def process_chunk_challenge(state: BeaconState,
challenge: CustodyChunkChallenge) -> None:
# Verify the attestation
assert verify_standalone_attestation(state, convert_to_standalone(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_attestation_participants(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.crosslink_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
state.custody_chunk_challenge_records.append(CustodyChunkChallengeRecord(
challenge_index=state.custody_challenge_index,
challenger_index=get_beacon_proposer_index(state, state.slot),
responder_index=challenge.responder_index
deadline=get_current_epoch(state) + CUSTODY_RESPONSE_DEADLINE,
crosslink_data_root=challenge.attestation.data.crosslink_data_root,
depth=depth,
chunk_index=challenge.chunk_index,
))
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:
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=signed_root(challenge),
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
# Verify the attestation
assert verify_standalone_attestation(state, convert_to_standalone(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)
# Verify the responder participated in the attestation
attesters = get_attestation_participants(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 chunk count
chunk_count = get_custody_chunk_count(challenge.attestation)
assert verify_bitfield(challenge.chunk_bits, chunk_count)
# Verify the xor of the chunk bits does not equal the custody bit
chunk_bits_xor = 0b0
for i in range(chunk_count):
chunk_bits_xor ^ get_bitfield_bit(challenge.chunk_bits, i)
custody_bit = get_bitfield_bit(attestation.custody_bitfield, attesters.index(responder_index))
assert custody_bit != chunk_bits_xor
# Add new bit challenge record
state.custody_bit_challenge_records.append(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
crosslink_data_root=challenge.attestation.crosslink_data_root,
chunk_bits=challenge.chunk_bits,
responder_key=challenge.responder_key,
))
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:
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
def process_chunk_challenge_response(state: BeaconState,
response: CustodyResponse,
challenge: CustodyChunkChallengeRecord) -> None:
# Verify chunk index
assert response.chunk_index == challenge.chunk_index
# Verify the chunk matches the crosslink data root
assert verify_merkle_branch(
leaf=hash_tree_root(response.chunk),
branch=response.branch,
depth=challenge.depth,
index=response.chunk_index,
root=challenge.crosslink_data_root,
)
# Clear the challenge
state.custody_chunk_challenge_records.remove(challenge)
# Reward the proposer
proposer_index = get_beacon_proposer_index(state, state.slot)
increase_balance(state, proposer_index, base_reward(state, index) // MINOR_REWARD_QUOTIENT)
def process_bit_challenge_response(state: BeaconState,
response: CustodyResponse,
challenge: CustodyBitChallengeRecord) -> None:
# Verify chunk index
assert response.chunk_index < len(challenge.chunk_bits)
# Verify the chunk matches the crosslink data root
assert verify_merkle_branch(
leaf=hash_tree_root(response.chunk),
branch=response.branch,
depth=math.log2(next_power_of_two(len(challenge.chunk_bits))),
index=response.chunk_index,
root=challenge.crosslink_data_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, response.chunk_index)
# Clear the challenge
state.custody_bit_challenge_records.remove(challenge)
# Slash challenger
slash_validator(state, challenge.challenger_index, challenge.responder_index)
Per-epoch processing
Run process_challenge_deadlines(state)
immediately after process_ejections(state)
:
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)
state.custody_chunk_challenge_records.remove(challenge)
for challenge in state.custody_bit_challenge_records:
if get_current_epoch(state) > challenge.deadline:
slash_validator(state, challenge.responder_index, challenge.challenger_index)
state.custody_bit_challenge_records.remove(challenge)
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):
def eligible(index):
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 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):
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