Try   HackMD

PubkeyCache Analysis

Introduction

This is an analysis on the consensus, p2p and beacon api spec on the usage of pubkeyCache - a collection of pubkey2Index and index2Pubkey, and whether they require the knowledge of validators whose initial deposit is not yet finalized (See the rationale of unfinalized cache in background). These are the places that require validator's index or pubkey to perform signature verification where bls.Verify() or bls.FastAggregateVerify() is used.

Background

The use of pubkeyCache (pubkey2Index and index2Pubkey) cache is critical for CL clients to allow efficient lookup of public keys and indices of validators in many places particularly during state transition. The populating of said cache happen during deposit processing which assign indices to new validators.

In the current deposit flow, the indices assigned to new validators are uniform across all branches in the block tree due to the large follow distance of Eth1Data poll. Therefore, a single global fork-independent pubkeyCache is sufficient.

EIP-6110 proposes a refrom to the deposit flow that 1) Shifts the deposit inclusion and validation to EL 2) Deprecates the Eth1Data voting mechanism, CL no longer has a large follow distance and hence a validator index becomes fork dependent ie. a validator with the same pubkey can have different indices in different block tree branches.

For the case of active validator, the single global pubkeyCache approach is sufficient since the beacon state that first assigns index (by adding it to state.validators) to it is already finalized. For the case of a validator that is assigned an index from a block that hasn't been finalized, its index might be different in different fork. Hence we need to have another pubkeyCache that is fork-dependent to specifically stores the validators whose initial deposit is unfinalized. This two-cache approach is proposed in Lodestar, different client may have different approach, but a fork-dependent cache is necessary to embrace the change from EIP-6110. Regardless, there is a need to go through the specifications systematically, point out the usage of pubkeyCache and whether it needs to look up for only finalized, or both finalized and unfinalized validators.

tl;dr

tl;dr: index2Pubkey is used in a lot more scenarios than pubkey2Index. The use cases for index2Pubkey do not require unfinalized information. process_deposit() is the only place in consensus spec that needs unfinalized information and it utilizespubkey2Index.
In terms of the two-cache approach, unfinalizedIndex2Pubkey is not needed since there is not a place that utilizes it. unfinalizedPubkey2Index, however, is needed for process_deposit().

unfinalized pubkey2Index? unfinalized index2Pubkey?
onBlock - state_transition - verify_block_signature N/A No
onBlock - state_transition - process_block - process_randao N/A No
onBlock - state_transition - process_block - process_operations - process_proposer_slashing N/A No
onBlock - state_transition - process_block - process_operations - process_attester_slashing - is_valid_indexed_attestation N/A No
onBlock - state_transition - process_block - process_operations - process_attestation - is_valid_indexed_attestation N/A No
onBlock - state_transition - process_block - process_operations - process_deposit - apply_deposit Yes N/A
onBlock - state_transition - process_block - process_operations - process_sync_aggregate - eth_fast_aggregate_verify No No
onBlock - state_transition - process_block - process_bls_to_execution_change N/A N/A
onBlock - state_transition - process_block - process_voluntary_exit N/A No
p2p - beacon_block onBlock N/A No
p2p - beacon_aggregate_and_proof onAttestation N/A No
p2p - voluntary_exit - process_voluntary_exit N/A No
p2p - proposer_slashing - process_proposer_slashing N/A No
p2p - attester_slashing - process_attester_slashing N/A No
p2p - beacon_attestation_{subnet_id} onAttestation N/A No
p2p - sync_committee_contribution_and_proof N/A No
p2p - sync_committee_{subnet_id} N/A No
p2p - bls_to_execution_change - process_bls_to_execution_change N/A N/A
p2p - blob_sidecar_{subnet_id} - verify_blob_sidecar_signature N/A No

Phase 0

verify_block_signature

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

During the state_transition(), we need to verify the block signature by checking on SignedBeaconBlock.message against proposer's public key.
The proposer's public key is obtained via index2Pubkey with SignedBeaconBlock.message.proposer_index.

From the spec of verify_block_signature(), it does not enforce the proposer to be active or not, it is possible to a perform index2Pubkey lookup with a proposer_index belongs to a validator whose deposit is unfinalized.
However, with the current spec, verify_block_signature() is only called from state_transition(), which will call process_block_header() after verify_block_signature(). process_block_header() enforces the proposer_index must match the result from get_beacon_proposer_index() which requires it to be in the current shuffling and henceforth, belongs to an active validator. We do not need to have an unfinalized index2Pubkey in this scenario.

process_randao

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

process_randao() verifies the signing root of the current epoch against the pubkey of the current slot proposer which is calculated by get_beacon_proposer_index().
get_beacon_proposer_index() picks the proposer from the list of active validators returned from get_active_validator_indices() according to a seed.
Hence, the lookup of index2Pubkey for proposer's pubkey must be finalized, because proposer index must be finalized.

process_proposer_slashing

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

process_proposer_slashing() checks the validity of signed_header_1 and signed_header_2 of ProposerSlashing by verifying them against the public key of the offending proposer.
The proposer's public key is obtained by looking up index2Pubkey with proposer index supplied in ProposerSlashing.
A is_slashable_validator() check happens before bls.Verify() which enforces the offending validator must be active. Therefore unfinalized pubkeyCache is not needed.

process_attester_slashing - is_valid_indexed_attestation

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

is_valid_indexed_attestation() is called twice in process_attester_slashing(). One for each potential slashable attestations to verify the signature against the attesting_indices.
Although is_valid_indexed_attestation(), the caller of index2Pubkey, does not check whether the validator indices in offending attestations are active, the subsequent call in process_attester_slashing() checks the offending validator if it is slashable is_slashable_validator() and it requires the validator to be active.

process_attestation - is_valid_indexed_attestation

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

In process_attestation, it first converts Attestation into IndexedAttestation by calling get_indexed_attestation(). There are nested functions that run several levels deep in get_indexed_attestation(), but ultimately it returns an indexed_attesation for which its attesting_indices are from active validators.
The said indexed_attesation is then fed into is_valid_indexed_attestation that extracts public keys from every validators in attesting_indices by calling index2Pubkey and finally verify the signature of the attestation with bls.FastAggregateVerify.
Since the indices in indexed_attestation must belong to active validators, unfinalized cache is not needed.

apply_deposit

pubkey2Index or index2Pubkey? pubkey2Index
Require unfinalized pubkeyCache? Yes

Given a BLSPubkey, apply_deposit() initializes a validator or top-up existed validator depending on whether the supplied public key exists in the BeaconState.validators.
The pubkey2Index lookup here happens during the top-up operation in order to find the index for increase_balance(state, index, amount).
Since apply_deposit() accepts public keys from active/inactive validators whose deposit may or may not be finalized, this function needs pubkey2Index to contain unfinalized pubkeys.

Altair

process_sync_aggregate - eth_fast_aggregate_verify

pubkey2Index or index2Pubkey? pubkey2Index and index2Pubkey
Require unfinalized pubkeyCache? No

In process_sync_aggregate(), it fetches a list of public keys of sync committee participants ie. validators that are in state.current_sync_committee.pubkeys and its corresponding sync_aggregate.sync_committee_bits is active. The public keys are then used to verify the signature of sync_aggregate.
Although process_sync_aggregate() does not lookup any sort of pubkeyCache, it does rely on the pulic keys of current_sync_committee in the Beacon state, which is populated by process_sync_committee_updates() and get_next_sync_committee(). They require sync committee to be active validators and use index2Pubkey for looking up the public keys.
In Lodestar, pubkey2Index is also being used to construct syncCommitteeCache which stores both public keys and indices in EpochCache.

Capella

process_bls_to_execution_change

pubkey2Index or index2Pubkey? N/A
Require unfinalized pubkeyCache? N/A

Both validator index and public key during the withdrawal credential change is supplied from BeaconBlockBody.bls_to_execution_changes, hence no look up to PubkeyCache is necessary.

process_voluntary_exit

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

process_voluntary_exit() first looks up index2Pubkey with SignedVoluntaryExit.validator_index for public key of the validator that is exiting.
Before verifying the signature, is_active_validator() check is processed which requires the exiting validator must be active.
process_voluntary_exit does not need to look up unfinalized pubkeys.

P2P

The following section covers the signature verifications happened upon receiving objects through each gossipsub topic

beacon_block

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

The proposer's signature ie. signed_beacon_block.signature is checked against the proposer's pubkey, which is obtained via index2Pubkey with proposer_index. proposer_index needs to be in the current shuffling - a subset of currently active validators.
This is only for verifying gossip blocks. After which onBlock() is called for further processing and verification on beacon block.

beacon_aggregate_and_proof

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

There are 3 signatures need to be checked here upon receiving signed_aggregate_and_proof from the network before proceeding to on_attestation():

  • Selection proof signed_aggregate_and_proof.message.selection_proof
  • Signature of SignedAggregateAndProof signed_aggregate_and_proof.signature
  • Signature of aggregate attestation signed_aggregate_and_proof.message.aggregate.signature

For both selection proof, and signed_aggregate_and_proof.signature, it compares them against aggregator's public key which is looked up via index2Pubkey with aggregator_index.

For the signature of aggregate attestation, it first converts the aggregation_bits into indices of attesting validators (no cache is used here), then look up corresponding public key (index2Pubkey) for each index. Then compare the keys against signature of aggregate attestation (signed_aggregate_and_proof.message.aggregate.signature).
Attesters' indices are derived from aggregations_bits intersecting with get_beacon_committee() so they must be active. Aggregator's index needs to be in get_beacon_committee() as well hence active.

voluntary_exit

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

A signature verification is performed that is identical to process_voluntary_exit() in process_block().

proposer_slashing

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

A signature verification is performed that is identical to process_proposer_slashing() in process_block().

attester_slashing

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

A signature verification is performed that is identical to process_attester_slashing() in process_block().

beacon_attestation_{subnet_id}

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

Similar to verifying the signature of aggregate attestation in beacon_aggregate_and_proof, we first need to convert the aggregation_bits into validator indices. Difference is the bit list here should only contain a single bit and thus only one validator index. We then obtain the public key of said validator through index2Pubkey to verify the signature.
The attesting indices are derived from aggregation_bits and get_beacon_committee() hence they must be active.

sync_committee_contribution_and_proof

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

There are 3 signatures need to be checked here upon receiving signed_aggregate_and_proof from the network before proceeding to on_attestation():

  • Selection proof signed_aggregate_and_proof.message.selection_proof
  • Signature of SignedAggregateAndProof signed_aggregate_and_proof.signature
  • Aggregate signature signed_aggregate_and_proof.message.aggregate.signature

For both selection proof, and signed_aggregate_and_proof.signature, it compares them against aggregator's public key which is looked up via index2Pubkey with aggregator_index.

For the signature of aggregate signature, it first converts the aggregation_bits into indices of sync committee participants (syncCommitteeCache is used), then look up corresponding public key (index2Pubkey) for each index. Then compare the keys against signature of aggregate attestation (signed_aggregate_and_proof.message.aggregate.signature)

Attesters and aggregator must be part of the sync committee (get_sync_subcommittee_pubkeys()) which is pre-computed and embedded in the beacon state. The pre-computation happens in process_sync_committee_updates() therefore sync committee must be a subset of active validators.

sync_committee_{subnet_id}

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

For a single sync_committee_message, we need to verify its signature against both the validator pubkey and beacon_block_root. index2Pubkey is used to look up the public key using validator_index.
Note that subnet_id is also being verified against compute_subnets_for_sync_committee() and it would fail if the validator's index is not in state.current_sync_committee. Therefore the message will be rejected will the validator is not active.

bls_to_execution_change

pubkey2Index or index2Pubkey? N/A
Require unfinalized pubkeyCache? N/A

A signature verification is performed that is identical to process_bls_to_execution_change() in process_block().

blob_sidecar_{subnet_id}

pubkey2Index or index2Pubkey? index2Pubkey
Require unfinalized pubkeyCache? No

blob_sidecar_{subnet_id} checks on signed_blob_sidecar.signature by calling verify_blob_sidecar_signature which requires public key of the proposer. index2Pubkey is looked up with signed_blob_sidecar.message.proposer_index.
Proposer must be in current shuffling which by definition is an active validator, hence unfinalized pubkeyCache is not needed.

Beacon API

The following is a list of pubkeyCache use in beacon and builder api.

unfinalized pubkey2Index? unfinalized index2Pubkey?
/eth/v1/beacon/states/{state_id}/validators/{validator_id} Yes N/A
/eth/v1/beacon/states/{state_id}/validators Yes N/A
/eth/v1/beacon/states/{state_id}/validator_balances Yes N/A
/eth/v1/builder/validators Yes N/A
/eth/v1/validator/register_validator Yes N/A

The apis above use pubkey2Index is to convert public key fed by users to validator index. The said validator index will then be used to look up the validator object to either return related info or process the validator registration.

Summary

We have listed out all the places in the current specifications where the pubkeyCache is used. By closely inspecting them one by one, we can conclude

  1. pubkey2Index is mainly used in process_deposit() and beacon apis. It is necessary for it to contain validators whose deposits are not finalized and thus, it is necessary to have a fork-dependent unfinalized cache.
  2. index2Pubkey is used extensively in variety of places. However, only active indices can ever make use of the cache which are guaranteed to come from finalized deposits. Thus an unfinalized index2Pubkey is not necessary.

pubkey2Index and index2Pubkey for active validators are needed and can continue to exist in a global singleton form. pubkey2Index for validators whose deposits are not yet finalized is also needed but should be fork-dependent and is recommended to have an instance attached to the state. unfinalizedIndex2Pubkey does not need to exist.