Skip to content

Latest commit

 

History

History
529 lines (440 loc) · 19 KB

1_custody-game.md

File metadata and controls

529 lines (440 loc) · 19 KB

Ethereum 2.0 Phase 1 -- Custody Game

NOTICE: This spec is a work-in-progress for researchers and implementers.

Table of contents

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 operations 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

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

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

replace_empty_or_append

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

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

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

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)
        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_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.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
    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,
        crosslink_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:

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),
    )
    # 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_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 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
    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
        crosslink_data_root=challenge.attestation.crosslink_data_root,
        chunk_bits=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:

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
    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)
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
    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

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)
            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()

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