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.
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: 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 |
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.
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.
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.
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.
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.
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.
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
.
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.
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.
The following section covers the signature verifications happened upon receiving objects through each gossipsub topic
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.
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()
:
signed_aggregate_and_proof.message.selection_proof
signed_aggregate_and_proof.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 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.
pubkey2Index or index2Pubkey ? |
index2Pubkey |
Require unfinalized pubkeyCache? | No |
A signature verification is performed that is identical to process_voluntary_exit()
in process_block()
.
pubkey2Index or index2Pubkey ? |
index2Pubkey |
Require unfinalized pubkeyCache? | No |
A signature verification is performed that is identical to process_proposer_slashing()
in process_block()
.
pubkey2Index or index2Pubkey ? |
index2Pubkey |
Require unfinalized pubkeyCache? | No |
A signature verification is performed that is identical to process_attester_slashing()
in process_block()
.
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.
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():
signed_aggregate_and_proof.message.selection_proof
signed_aggregate_and_proof.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.
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.
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()
.
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.
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.
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
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.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.