Skip to content

Whisk -- The Beacon Chain

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 of to support the Whisk SSLE.

Note: This specification is built upon capella and is under active development.

Constants

Domain types

Name Value
DOMAIN_WHISK_CANDIDATE_SELECTION DomainType('0x07000000')
DOMAIN_WHISK_SHUFFLE DomainType('0x07100000')
DOMAIN_WHISK_PROPOSER_SELECTION DomainType('0x07200000')

Preset

Name Value Description
CURDLEPROOFS_N_BLINDERS uint64(4) number of blinders for curdleproofs
WHISK_CANDIDATE_TRACKERS_COUNT uint64(2**14) (= 16,384) number of candidate trackers
WHISK_PROPOSER_TRACKERS_COUNT uint64(2**13) (= 8,192) number of proposer trackers
WHISK_VALIDATORS_PER_SHUFFLE uint64(2**7 - 4) (= 124) number of validators shuffled per shuffle step
WHISK_MAX_SHUFFLE_PROOF_SIZE uint64(2**15) max size of a shuffle proof
WHISK_MAX_OPENING_PROOF_SIZE uint64(2**10) max size of a opening proof

Configuration

Name Value Description
WHISK_EPOCHS_PER_SHUFFLING_PHASE Epoch(2**8) (= 256) epochs per shuffling phase
WHISK_PROPOSER_SELECTION_GAP Epoch(2) gap between proposer selection and the block proposal phase

Cryptography

BLS

Name SSZ equivalent Description
BLSFieldElement uint256 BLS12-381 scalar
BLSG1Point Bytes48 compressed BLS12-381 G1 point
WhiskShuffleProof ByteList[WHISK_MAX_SHUFFLE_PROOF_SIZE] Serialized shuffle proof
WhiskTrackerProof ByteList[WHISK_MAX_OPENING_PROOF_SIZE] Serialized tracker proof

Note: A subgroup check MUST be performed when deserializing a BLSG1Point for use in any of the functions below.

def BLSG1ScalarMultiply(scalar: BLSFieldElement, point: BLSG1Point) -> BLSG1Point:
    return bls.G1_to_bytes48(bls.multiply(bls.bytes48_to_G1(point), scalar))
1
2
3
4
5
6
7
def bytes_to_bls_field(b: Bytes32) -> BLSFieldElement:
    """
    Convert bytes to a BLS field scalar. The output is not uniform over the BLS field.
    TODO: Deneb will introduces this helper too. Should delete it once it's rebased to post-Deneb.
    """
    field_element = int.from_bytes(b, ENDIANNESS)
    return BLSFieldElement(field_element % BLS_MODULUS)
Name Value
BLS_G1_GENERATOR BLSG1Point('0x97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb') # noqa: E501
BLS_MODULUS 52435875175126190479447740508185965837690552500527637822603658699938581184513
CURDLEPROOFS_CRS TBD

Curdleproofs and opening proofs

Note that Curdleproofs (Whisk Shuffle Proofs), the tracker opening proofs and all related data structures and verifier code (along with tests) is specified in curdleproofs.pie repository.

def IsValidWhiskShuffleProof(pre_shuffle_trackers: Sequence[WhiskTracker],
                             post_shuffle_trackers: Sequence[WhiskTracker],
                             shuffle_proof: WhiskShuffleProof) -> bool:
    """
    Verify `post_shuffle_trackers` is a permutation of `pre_shuffle_trackers`.
    Defined in https://github.com/nalinbhardwaj/curdleproofs.pie/blob/dev/curdleproofs/curdleproofs/whisk_interface.py.
    """
    return curdleproofs.IsValidWhiskShuffleProof(
        CURDLEPROOFS_CRS,
        pre_shuffle_trackers,
        post_shuffle_trackers,
        shuffle_proof,
    )
1
2
3
4
5
6
7
8
def IsValidWhiskOpeningProof(tracker: WhiskTracker,
                             k_commitment: BLSG1Point,
                             tracker_proof: WhiskTrackerProof) -> bool:
    """
    Verify knowledge of `k` such that `tracker.k_r_G == k * tracker.r_G` and `k_commitment == k * BLS_G1_GENERATOR`.
    Defined in https://github.com/nalinbhardwaj/curdleproofs.pie/blob/dev/curdleproofs/curdleproofs/whisk_interface.py.
    """
    return curdleproofs.IsValidWhiskOpeningProof(tracker, k_commitment, tracker_proof)

Epoch processing

WhiskTracker

1
2
3
class WhiskTracker(Container):
    r_G: BLSG1Point  # r * G
    k_r_G: BLSG1Point  # k * r * G

BeaconState

class BeaconState(Container):
    # Versioning
    genesis_time: uint64
    genesis_validators_root: Root
    slot: Slot
    fork: Fork
    # History
    latest_block_header: BeaconBlockHeader
    block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
    state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
    historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT]  # Frozen in Capella, replaced by historical_summaries
    # Eth1
    eth1_data: Eth1Data
    eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
    eth1_deposit_index: uint64
    # Registry
    validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
    balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
    # Randomness
    randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]
    # Slashings
    slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR]  # Per-epoch sums of slashed effective balances
    # Participation
    previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
    current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
    # Finality
    justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH]  # Bit set for every recent justified epoch
    previous_justified_checkpoint: Checkpoint
    current_justified_checkpoint: Checkpoint
    finalized_checkpoint: Checkpoint
    # Inactivity
    inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT]
    # Sync
    current_sync_committee: SyncCommittee
    next_sync_committee: SyncCommittee
    # Execution
    latest_execution_payload_header: ExecutionPayloadHeader
    # Withdrawals
    next_withdrawal_index: WithdrawalIndex
    next_withdrawal_validator_index: ValidatorIndex
    # Deep history valid from Capella onwards
    historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]
    # Whisk
    whisk_candidate_trackers: Vector[WhiskTracker, WHISK_CANDIDATE_TRACKERS_COUNT]  # [New in Whisk]
    whisk_proposer_trackers: Vector[WhiskTracker, WHISK_PROPOSER_TRACKERS_COUNT]  # [New in Whisk]
    whisk_trackers: List[WhiskTracker, VALIDATOR_REGISTRY_LIMIT]  # [New in Whisk]
    whisk_k_commitments: List[BLSG1Point, VALIDATOR_REGISTRY_LIMIT]  # [New in Whisk]
def select_whisk_proposer_trackers(state: BeaconState, epoch: Epoch) -> None:
    # Select proposer trackers from candidate trackers
    proposer_seed = get_seed(
        state,
        Epoch(saturating_sub(epoch, WHISK_PROPOSER_SELECTION_GAP)),
        DOMAIN_WHISK_PROPOSER_SELECTION
    )
    for i in range(WHISK_PROPOSER_TRACKERS_COUNT):
        index = compute_shuffled_index(uint64(i), uint64(len(state.whisk_candidate_trackers)), proposer_seed)
        state.whisk_proposer_trackers[i] = state.whisk_candidate_trackers[index]
1
2
3
4
5
6
7
def select_whisk_candidate_trackers(state: BeaconState, epoch: Epoch) -> None:
    # Select candidate trackers from active validator trackers
    active_validator_indices = get_active_validator_indices(state, epoch)
    for i in range(WHISK_CANDIDATE_TRACKERS_COUNT):
        seed = hash(get_seed(state, epoch, DOMAIN_WHISK_CANDIDATE_SELECTION) + uint_to_bytes(uint64(i)))
        candidate_index = compute_proposer_index(state, active_validator_indices, seed)  # sample by effective balance
        state.whisk_candidate_trackers[i] = state.whisk_trackers[candidate_index]
1
2
3
4
5
def process_whisk_updates(state: BeaconState) -> None:
    next_epoch = Epoch(get_current_epoch(state) + 1)
    if next_epoch % WHISK_EPOCHS_PER_SHUFFLING_PHASE == 0:  # select trackers at the start of shuffling phases
        select_whisk_proposer_trackers(state, next_epoch)
        select_whisk_candidate_trackers(state, next_epoch)
def process_epoch(state: BeaconState) -> None:
    process_justification_and_finalization(state)
    process_inactivity_updates(state)
    process_rewards_and_penalties(state)
    process_registry_updates(state)
    process_slashings(state)
    process_eth1_data_reset(state)
    process_effective_balance_updates(state)
    process_slashings_reset(state)
    process_randao_mixes_reset(state)
    process_historical_summaries_update(state)
    process_participation_flag_updates(state)
    process_sync_committee_updates(state)
    process_whisk_updates(state)  # [New in Whisk]

Block processing

Block header

1
2
3
4
def process_whisk_opening_proof(state: BeaconState, block: BeaconBlock) -> None:
    tracker = state.whisk_proposer_trackers[state.slot % WHISK_PROPOSER_TRACKERS_COUNT]
    k_commitment = state.whisk_k_commitments[block.proposer_index]
    assert IsValidWhiskOpeningProof(tracker, k_commitment, block.body.whisk_opening_proof)

Removed assert block.proposer_index == get_beacon_proposer_index(state) check in Whisk.

def process_block_header(state: BeaconState, block: BeaconBlock) -> None:
    # Verify that the slots match
    assert block.slot == state.slot
    # Verify that the block is newer than latest block header
    assert block.slot > state.latest_block_header.slot

    # # Verify that proposer index is the correct index
    # assert block.proposer_index == get_beacon_proposer_index(state)

    # Verify that the parent matches
    assert block.parent_root == hash_tree_root(state.latest_block_header)
    # Cache current block as the new latest block
    state.latest_block_header = BeaconBlockHeader(
        slot=block.slot,
        proposer_index=block.proposer_index,
        parent_root=block.parent_root,
        state_root=Bytes32(),  # Overwritten in the next process_slot call
        body_root=hash_tree_root(block.body),
    )

    # Verify proposer is not slashed
    proposer = state.validators[block.proposer_index]
    assert not proposer.slashed
    process_whisk_opening_proof(state, block)   # [New in Whisk]

Whisk

BeaconBlockBody

class BeaconBlockBody(Container):
    randao_reveal: BLSSignature
    eth1_data: Eth1Data  # Eth1 data vote
    graffiti: Bytes32  # Arbitrary data
    # Operations
    proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
    attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]
    attestations: List[Attestation, MAX_ATTESTATIONS]
    deposits: List[Deposit, MAX_DEPOSITS]
    voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
    sync_aggregate: SyncAggregate
    # Execution
    execution_payload: ExecutionPayload
    bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES]
    # Whisk
    whisk_opening_proof: WhiskTrackerProof  # [New in Whisk]
    whisk_post_shuffle_trackers: Vector[WhiskTracker, WHISK_VALIDATORS_PER_SHUFFLE]  # [New in Whisk]
    whisk_shuffle_proof: WhiskShuffleProof  # [New in Whisk]
    whisk_registration_proof: WhiskTrackerProof  # [New in Whisk]
    whisk_tracker: WhiskTracker  # [New in Whisk]
    whisk_k_commitment: BLSG1Point  # k * BLS_G1_GENERATOR [New in Whisk]
def get_shuffle_indices(randao_reveal: BLSSignature) -> Sequence[uint64]:
    """
    Given a `randao_reveal` return the list of indices that got shuffled from the entire candidate set.
    """
    indices = []
    for i in range(0, WHISK_VALIDATORS_PER_SHUFFLE):
        # XXX ensure we are not suffering from modulo bias
        pre_image = randao_reveal + uint_to_bytes(uint64(i))
        shuffle_index = bytes_to_uint64(hash(pre_image)[0:8]) % WHISK_CANDIDATE_TRACKERS_COUNT
        indices.append(shuffle_index)

    return indices
def process_shuffled_trackers(state: BeaconState, body: BeaconBlockBody) -> None:
    shuffle_epoch = get_current_epoch(state) % WHISK_EPOCHS_PER_SHUFFLING_PHASE
    if shuffle_epoch + WHISK_PROPOSER_SELECTION_GAP + 1 >= WHISK_EPOCHS_PER_SHUFFLING_PHASE:
        # Require trackers set to zero during cooldown
        assert body.whisk_post_shuffle_trackers == Vector[WhiskTracker, WHISK_VALIDATORS_PER_SHUFFLE]()
        assert body.whisk_shuffle_proof == WhiskShuffleProof()
    else:
        # Require shuffled trackers during shuffle
        shuffle_indices = get_shuffle_indices(body.randao_reveal)
        pre_shuffle_trackers = [state.whisk_candidate_trackers[i] for i in shuffle_indices]
        assert IsValidWhiskShuffleProof(
            pre_shuffle_trackers,
            body.whisk_post_shuffle_trackers,
            body.whisk_shuffle_proof,
        )
        # Shuffle candidate trackers
        for i, shuffle_index in enumerate(shuffle_indices):
            state.whisk_candidate_trackers[shuffle_index] = body.whisk_post_shuffle_trackers[i]
def is_k_commitment_unique(state: BeaconState, k_commitment: BLSG1Point) -> bool:
    return all([whisk_k_commitment != k_commitment for whisk_k_commitment in state.whisk_k_commitments])
def process_whisk_registration(state: BeaconState, body: BeaconBlockBody) -> None:
    proposer_index = get_beacon_proposer_index(state)
    if state.whisk_trackers[proposer_index].r_G == BLS_G1_GENERATOR:  # first Whisk proposal
        assert body.whisk_tracker.r_G != BLS_G1_GENERATOR
        assert is_k_commitment_unique(state, body.whisk_k_commitment)
        assert IsValidWhiskOpeningProof(
            body.whisk_tracker,
            body.whisk_k_commitment,
            body.whisk_registration_proof,
        )
        state.whisk_trackers[proposer_index] = body.whisk_tracker
        state.whisk_k_commitments[proposer_index] = body.whisk_k_commitment
    else:  # next Whisk proposals
        assert body.whisk_registration_proof == WhiskTrackerProof()
        assert body.whisk_tracker == WhiskTracker()
        assert body.whisk_k_commitment == BLSG1Point()
def process_block(state: BeaconState, block: BeaconBlock) -> None:
    process_block_header(state, block)
    process_withdrawals(state, block.body.execution_payload)
    process_execution_payload(state, block.body, EXECUTION_ENGINE)
    process_randao(state, block.body)
    process_eth1_data(state, block.body)
    process_operations(state, block.body)
    process_sync_aggregate(state, block.body.sync_aggregate)
    process_shuffled_trackers(state, block.body)  # [New in Whisk]
    process_whisk_registration(state, block.body)  # [New in Whisk]

Deposits

1
2
3
def get_initial_whisk_k(validator_index: ValidatorIndex, counter: int) -> BLSFieldElement:
    # hash `validator_index || counter`
    return BLSFieldElement(bytes_to_bls_field(hash(uint_to_bytes(validator_index) + uint_to_bytes(uint64(counter)))))
1
2
3
4
5
6
7
def get_unique_whisk_k(state: BeaconState, validator_index: ValidatorIndex) -> BLSFieldElement:
    counter = 0
    while True:
        k = get_initial_whisk_k(validator_index, counter)
        if is_k_commitment_unique(state, BLSG1ScalarMultiply(k, BLS_G1_GENERATOR)):
            return k  # unique by trial and error
        counter += 1
def get_k_commitment(k: BLSFieldElement) -> BLSG1Point:
    return BLSG1ScalarMultiply(k, BLS_G1_GENERATOR)
def get_initial_tracker(k: BLSFieldElement) -> WhiskTracker:
    return WhiskTracker(r_G=BLS_G1_GENERATOR, k_r_G=BLSG1ScalarMultiply(k, BLS_G1_GENERATOR))
def add_validator_to_registry(state: BeaconState,
                              pubkey: BLSPubkey,
                              withdrawal_credentials: Bytes32,
                              amount: uint64) -> None:
    index = get_index_for_new_validator(state)
    validator = get_validator_from_deposit(pubkey, withdrawal_credentials, amount)
    set_or_append_list(state.validators, index, validator)
    set_or_append_list(state.balances, index, amount)
    set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000))
    set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000))
    set_or_append_list(state.inactivity_scores, index, uint64(0))
    # [New in Whisk]
    k = get_unique_whisk_k(state, ValidatorIndex(len(state.validators) - 1))
    state.whisk_trackers.append(get_initial_tracker(k))
    state.whisk_k_commitments.append(get_k_commitment(k))

get_beacon_proposer_index

1
2
3
4
5
6
def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex:
    """
    Return the beacon proposer index at the current slot.
    """
    assert state.latest_block_header.slot == state.slot  # sanity check `process_block_header` has been called
    return state.latest_block_header.proposer_index