This proposal extends the EIP-7549 by introducing an on chain aggregate optimisation enabled by the EIP.
This proposal comes with a notion of network and on chain aggregate. Although, both type of aggregates are represented by the same Attestation
structure, their data complexity are different which is enforced by the additional gossipsub validation rule.
The changes are based on the consensus-specs#3559.
Name | Value |
---|---|
MAX_ATTESTER_SLASHINGS |
2**0 (= 1) |
MAX_ATTESTATIONS |
2**3 (= 8) |
class Attestation(Container):
aggregation_bits: List[Bitlist[MAX_VALIDATORS_PER_COMMITTEE], MAX_COMMITTEES_PER_SLOT] # [Modified in EIP7549]
data: AttestationData
committee_bits: Bitvector[MAX_COMMITTEES_PER_SLOT] # [New in EIP7549]
signature: BLSSignature
class IndexedAttestation(Container):
attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] # [Modified in EIP7549]
data: AttestationData
signature: BLSSignature
get_committee_indices
def get_committee_indices(commitee_bits: Bitvector) -> List[CommitteeIndex]:
return [CommitteeIndex(index) for bit, index in enumerate(commitee_bits) if bit]
get_attesting_indices
def get_attesting_indices(state: BeaconState, attestation: Attestation) -> Set[ValidatorIndex]:
"""
Return the set of attesting indices corresponding to ``aggregation_bits`` and ``committee_bits``.
"""
def get_committee_attesters(state: BeaconState, attesting_bits: Bitlist, index: CommitteeIndex) -> Set[ValidatorIndex]:
committee = get_beacon_committee(state, data.slot, index)
return set(index for i, index in enumerate(committee) if bits[i])
output = set()
committee_indices = get_committee_indices(attestation.committee_bits)
for index in committee_indices:
attesting_bits = attestation.attesting_bits[index]
committee_attesters = get_committee_attesters(state, attesting_bits, index)
output = output.union(committee_attesters)
return output
process_attestation
def process_attestation(state: BeaconState, attestation: Attestation) -> None:
data = attestation.data
index = get_attestation_index(attestation)
assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state))
assert data.target.epoch == compute_epoch_at_slot(data.slot)
assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot
assert index < get_committee_count_per_slot(state, data.target.epoch)
# [Modified in EIP7549]
committee_indices = get_committee_indices(attestation.committee_bits)
assert len(committee_indices) > 0
assert len(committee_indices) == len(attestation.aggregation_bits)
for index in committee_indices:
assert index < get_committee_count_per_slot(state, data.target.epoch)
committee = get_beacon_committee(state, data.slot, index)
assert len(attestation.aggregation_bits[index]) == len(committee)
# Participation flag indices
participation_flag_indices = get_attestation_participation_flag_indices(state, data, state.slot - data.slot)
# Verify signature
assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation))
# Update epoch participation flags
if data.target.epoch == get_current_epoch(state):
epoch_participation = state.current_epoch_participation
else:
epoch_participation = state.previous_epoch_participation
proposer_reward_numerator = 0
for index in get_attesting_indices(state, attestation):
for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS):
if flag_index in participation_flag_indices and not has_flag(epoch_participation[index], flag_index):
epoch_participation[index] = add_flag(epoch_participation[index], flag_index)
proposer_reward_numerator += get_base_reward(state, index) * weight
# Reward proposer
proposer_reward_denominator = (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT
proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator)
increase_balance(state, get_beacon_proposer_index(state), proposer_reward)
Run the following function to construct an on chain aggregate form a list of network aggregates with equal attestation.data
:
def compute_on_chain_aggregate(network_aggregates: List[Attestation]) -> Attestation:
aggregates = sorted(network_aggregates, key=lambda a: get_committee_indices(a)[0])
data = aggregates[0].data
aggregation_bits = [a.bits[0] for a in aggregates]
signature = bls.Aggregate([a.signature for a in aggregates])
committee_indices = [get_committee_indices(a)[0] for a in aggregates]
committee_flags = [(i in committee_indices) for i in range(0, MAX_COMMITTEES_PER_SLOT)]
committee_bits = Bitvector[MAX_COMMITTEES_PER_SLOT](committee_flags)
return Attestation(aggregation_bits=aggregation_bits,
data=data,
committee_bits=committee_bits,
signature=signature)
beacon_aggregate_and_proof
topicThe list of beacon_aggregate_and_proof
validations is extended with the following:
len(committee_indices) == len(aggregate.attestation_bits) == 1
, where committee_indices = get_committee_indices(aggregate)
.Data complexity of the network aggregtes does not increase as the new validity condition for the beacon_aggregate_and_proof
gossipsub topic ensures that every network aggregate comprises a single committee.
The above plot shows potential optimisation effect of the proposed change. Namely, it is roughly 18KB to 4KB reduction in the size of a 64-committee aggregate for a 1 million validator set. Note that these numbers assumes perfect aggregation.
MAX_ATTESTATIONS
MAX_ATTESTATIONS
parameter should be adjusted respectively to avoid increase of the block size.
The above plot suggests MAX_ATTESTATIONS = 8
as a reasonable limitation. Which means that assuming perfect aggregation the attestation capacity of the block increases from 128
committees to 8*64 = 512
committees, i.e. from 2
slots worth of attestations to 8
.
The plot below shows different variations of attester slashing data complexity. Numbers e.g. 64 vs 64 mean a number of committees which validator indices are respresented by the corresponding IndexedAttestation
structure. For instance, today every on chain aggregate comprises a single committee thus the worst case attester slashing produced from on chain aggregates will have two indexed attestations with 1_000_000 / 32 / 64 = 488
validator indices each, which is roughly 8 KB
of uncompressed data. With the proposed change an attester slashing obtained in the same way could require 488 KB
of uncompressed data because it could contain indices from up to 64 committees instead of just 1.
Note that in the case of attack entailing mass slashing the overall complexity of attester slashing data with the proposed change remains the same as it is with the status quo. However, the proposed changed increases the size of the beacon block as each slashing will contain up to 64 times more indices than with the status quo. The positive outcome here is that all slashable attestations can be processed during roughly one epoch (EIP-7549 makes this already possible).
The red line on the plot represents the case when one of the indexed attestations is obtained from an on chain aggregate and the other one from a network aggregate. Moreover, with the proposed change it is still possible to create a slashing from two network aggregates. Therefore, complexity of attester slashing data in case of a single slashing event depends on the slasher database schema, i.e. whether or not a slasher has access to network aggregates.
MAX_ATTESTER_SLASHINGS = 1
is suggested to alleviate the increase of the attester slashing complexity.
Intuitively validator indices should compress well as there is just 20
bits of useful information per each validator index in the case of 1_000_000
validator set size. So 488 KB
of attester slashing data (the worst case) can be reduced to 153 KB
by employing a trivial compression algorithm.
AttesterSlashing with 64x64 committees (1_000_000 validators) takes 320KB if compressed by Snappy (checked with PySpec SSZ test suite).
The proposed change reduces maximum number of aggregates per block from 128 to 8. This reduction can have adverse effects in the following cases:
Considering attesting committees shuffling, let's assume that validators of every attesting committee have roughly the same number of different views. With this assumption the proposed change is an improvement on the status quo as it can accommodate 8
different views vs 128 / 64 = 2
with the status quo.
In reality the distribution of a number of fork choice views can be more complicated. In the case of network asynchrony, a number of views per committee will highly depend on the network topology, i.e. how committee members are connected with each other and to members of other committees. Regardless of that, with the proposed change one block will be capable of accommodating 6
different aggregates more which should alleviate the complexity in the case of (1).
In the second case a proposer can opt in to include two on chain aggregates for the same slot, where the second aggregate will contain aggregates overlapping with the first one. The problem with that is every on chain aggregate can contain only one network aggregate from each committee. To overcome this limitation we can do the following changes:
class Attestation(Container):
aggregation_bits: List[Bitlist[MAX_VALIDATORS_PER_COMMITTEE], MAX_COMMITTEES_PER_SLOT] # [Modified in EIP7549]
data: AttestationData
committee_indexes: List[uint8, MAX_COMMITTEES_PER_SLOT] # [New in EIP7549]
signature: BLSSignature
def get_committee_indices(commitee_indices: List[uint8]) -> List[CommitteeIndex]:
return [CommitteeIndex(index) for index in committee_indices]
def get_attesting_indices_list(state: BeaconState, attestation: Attestation) -> Set[ValidatorIndex]:
"""
Return the set of attesting indices corresponding to ``aggregation_bits`` and ``committee_bits``.
"""
def get_committee_attesters(state: BeaconState, attesting_bits: Bitlist, index: CommitteeIndex) -> List[ValidatorIndex]:
committee = get_beacon_committee(state, data.slot, index)
return [index for i, index in enumerate(committee) if bits[i]]
output = []
committee_indices = get_committee_indices(attestation.committee_bits)
for index in committee_indices:
attesting_bits = attestation.attesting_bits[index]
committee_attesters = get_committee_attesters(state, attesting_bits, index)
output = output + committee_attesters
return output
def get_attesting_indices(state: BeaconState, attestation: Attestation) -> Set[ValidatorIndex]:
return set(get_attesting_indices_list(state, attestation))
def get_indexed_attestation(state: BeaconState, attestation: Attestation) -> IndexedAttestation:
"""
Return the indexed attestation corresponding to ``attestation``.
"""
attesting_indices = get_attesting_indices_list(state, attestation)
return IndexedAttestation(
attesting_indices=sorted(attesting_indices),
data=attestation.data,
signature=attestation.signature,
)
This change allows to pack overlapping network aggregates into a single on chain aggregate with high flexibility. Note that List[uint8]
can be replaced with List[CommitteeIndex]
if we assume that the latter is compressed to the size of the former.