eth2.0-specs/specs/phase1/custody-game.md

24 KiB

Ethereum 2.0 Phase 1 -- Custody Game

Notice: This document 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.

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_CHUNK_CHALLENGE_RESPONSES 2**4 (= 16)
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

CustodyChunkChallenge

class CustodyChunkChallenge(Container):
    responder_index: ValidatorIndex
    shard_transition: ShardTransition
    attestation: Attestation
    data_index: uint64
    chunk_index: uint64

CustodyChunkChallengeRecord

class CustodyChunkChallengeRecord(Container):
    challenge_index: uint64
    challenger_index: ValidatorIndex
    responder_index: ValidatorIndex
    inclusion_epoch: Epoch
    data_root: Root
    chunk_index: uint64

CustodyChunkResponse

class CustodyChunkResponse(Container):
    challenge_index: uint64
    chunk_index: uint64
    chunk: ByteVector[BYTES_PER_CUSTODY_CHUNK]
    branch: Vector[Root, CUSTODY_RESPONSE_DEPTH]

CustodySlashing

class CustodySlashing(Container):
    # Attestation.custody_bits_blocks[data_index][committee.index(malefactor_index)] is the target custody bit to check.
    # (Attestation.data.shard_transition_root as ShardTransition).shard_data_roots[data_index] is the root of the data.
    data_index: uint64
    malefactor_index: ValidatorIndex
    malefactor_secret: BLSSignature
    whistleblower_index: ValidatorIndex
    shard_transition: ShardTransition
    attestation: Attestation
    data: ByteList[MAX_SHARD_BLOCK_SIZE]

SignedCustodySlashing

class SignedCustodySlashing(Container):
    message: CustodySlashing
    signature: BLSSignature

CustodyKeyReveal

class CustodyKeyReveal(Container):
    # Index of the validator whose key is being revealed
    revealer_index: ValidatorIndex
    # Reveal (masked signature)
    reveal: BLSSignature

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

class EarlyDerivedSecretReveal(Container):
    # Index of the validator whose key is being revealed
    revealed_index: ValidatorIndex
    # RANDAO epoch of the key that is being revealed
    epoch: Epoch
    # Reveal (masked signature)
    reveal: BLSSignature
    # Index of the validator who revealed (whistleblower)
    masker_index: ValidatorIndex
    # Mask used to hide the actual reveal signature (prevent reveal from being stolen)
    mask: Bytes32

Helpers

replace_empty_or_append

def replace_empty_or_append(list: List, new_element: Any) -> int:
    for i in range(len(list)):
        if list[i] == type(new_element)():
            assert False
            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.

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.

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

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

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

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

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

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.chunk_challenges, process_chunk_challenge)
    for_ops(body.chunk_challenge_responses, process_chunk_challenge)
    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

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 != 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

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, Gwei(get_base_reward(state, proposer_index) // MINOR_REWARD_QUOTIENT))

Custody key reveals

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

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

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

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

# 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:

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)