# Eth2 gossip-based attestation aggregation model Attestation nets: - Global net, aimed for "the selected validators" - Topic name: `beacon_aggregate_and_proof` - Subnets, aimed for aggregating attestations within your committee, and possibly shared with a few other committees. - Topic name: `committee_index{subnet_id}_beacon_attestation` ## Model Params / setup Model Params: - `SLOTS_PER_EPOCH` - `RANDOM_SUBNETS_PER_VALIDATOR` - `ATTESTATION_PROPAGATION_SLOT_RANGE` - `ACTIVE_VALIDATOR_COUNT` - `COMMITTEE_COUNT` - `ATTESTATION_SUBNET_COUNT` - `PARTICIPATING_NETWORK_NODES` Derived Params: - committee size: `ACTIVE_VALIDATOR_COUNT / COMMITTEE_COUNT` - subnet size (committees): `COMMITTEE_COUNT / ATTESTATION_SUBNET_COUNT` TODO topology/randomness params: - chain forking - network topology - latency, bandwidth TODO: distribute `ACTIVE_VALIDATOR_COUNT` over `PARTICIPATING_NETWORK_NODES`, with random shuffling every epoch ---- # Simulation Model ## Split into roles Create network of N nodes with some topology function. Select M out of N nodes to be validating. Distribute P "good" validators to the M validating nodes. Distribute M-P "bad" validators to the remaining validating nodes. For the N-M non-validating nodes, select Q "good" nodes: just do node work without any validator. The remainder (N-M-Q) "bad" nodes will be misbehaving with their gossip. ## Per role ### For each validating node: Subscribe to some subnets, as backbone for the network. For each validator, subscribe randomly to some subnets: - Switch between every `EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION` and `2 * EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION` epochs Subscribe to the subnets for the committees of its validators in time for the epoch. TODO: see specs issue \#2523, small chance of shuffling problems Propagate subnet attestations if `get_strategy_state(attestation.data.slot, attestation.data.index)` can find a `strategy_state` and they are valid: `if process_attestation(strategy_state, attestation)` and follow the strategy. I.e. `if propagation_ok(strategy_state, attestation)` For each validator, when it needs to attest: ask it to sign an attestation, and put it on the correct subnet After receiving a sufficient amount of attestations, for each `strategy_state` (i.e. each managed committee): - aggregate attestations for it `make_candidates(strategy_state, time_elapsed)` (just one in the standard case) - filter out already propagated attestations - if any, make the validator sign these - Then repeat this process whenever a new attestation is received. Additionally, run the above routine on a few `time_elapsed` in the round, to cover the case when no other attestations trigger making new candidates. For each `strategy_state` (i.e. each managed committee), near the end of the round, if one of its validators is selected: - get the aggregate: `make_aggregate(strategy_state)`, and put it on the global topic. Propagate global attestations, if: - attestation has not been seen - TODO: global net honest nodes will have to track seen messages. - attested block is valid - TODO: model invalid blocks - slot within propagation range - selected validator is valid: - part of attestation participants - selected for the slot - selection proof signature is valid - The aggregate attestation itself is valid *insert strategy*, executed per committee with validators of the node, interface: - `new_round(strategy_state: AttestationStrategy, slot: Slot, index: CommitteeIndex)` - `process_attestation(strategy_state: AttestationStrategy, incoming: Attestation) -> bool`: returns true if valid, and memorizes it if so. - `make_candidates(strategy_state: AttestationStrategy, time_elapsed: int) -> []Attestation` - `make_aggregate(strategy_state: AttestationStrategy) -> Attestation` - `propagation_ok(strategy_state: AttestationStrategy, incoming: Attestation) -> bool`: returns true if it should be propagated #### For each "good" validator: - Sign attestations when the node asks for it #### For each "bad" validator: TODO: possible mistakes/bad behavior: - Do not participate, i.e. no signing - Participate naive, i.e. sign, even though it results in bad aggregation - Participate incorrectly, i.e. wrong signing ### For each non-validating node: #### For each "good" node Subscribe to some subnets more persistently, similar to what to-be-activated validators would do. - Switch between every `EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION` and `2 * EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION` epochs Produce an attestation for the node head every epoch TODO: just mock a set of random chain head roots every slot? #### For each "bad" node TODO: possible mistakes/bad behavior: - Subscribe to some subnets, and quickly leave them again for others - Drop valid messages - Propagate/create bad messages ## Possible aggregation strategies Helper, checks consensus validity: ```python def is_valid_attestation(att: Attestation) -> bool: ... ``` ### Naive ```python @dataclass def AttestationStrategy(): seen: Dict[Root, Attestation] = field(default_factory=dict) ``` ```python def process_attestation(strategy_state: AttestationStrategy, incoming: Attestation) -> bool: if is_valid_attestation(incoming): strategy_state.seen[hash_tree_root(incoming)] = incoming return True return False ``` ```python def make_candidates(strategy_state: AttestationStrategy, time_elapsed: int) -> []Attestation: # do not aggregate anything, except that of local validators local_atts = strategy_state.seen.values().filter(lambda att: any((loc in att.participants()) for loc in strategy_state.local_committee)) candidates = [aggregate(local_atts)] candidates.extend(strategy_state.seen.values() - local_atts) return candidates ``` ```python def make_aggregate(strategy_state: AttestationStrategy) -> Attestation: # TODO: just combine seen attestations into some aggregate, even if unorganized/sparse. return aggregate ``` ```python def propagation_ok(strategy_state: AttestationStrategy, incoming: Attestation, time_elapsed: int) -> bool: return hash_tree_root(incoming) not in strategy_state.seen ``` ### Partition and performance based Based on this idea: https://notes.ethereum.org/9Ijj2RkuRiqQYB9NOfY_9Q ```python @dataclass def AttestationStrategy(): seen: Dict[Root, Attestation] = field(default_factory=dict) # TODO: performance state for each bit ``` ```python def process_attestation(strategy_state: AttestationStrategy, incoming: Attestation) -> bool: if is_valid_attestation(incoming): strategy_state.seen[hash_tree_root(incoming)] = incoming # TODO update performance states return True return False ``` ```python def make_candidates(strategy_state: AttestationStrategy, time_elapsed: int) -> []Attestation: # based on the time closing in to some target, prefer a full attestation over a well-organized one. # TODO: of seen attestations, group them based on what is acceptable to aggregate given the elapsed time. # (adjusted based on previous aggregate performance, tracked in AttestationStrategy). # TODO: aggregate each group, this will be the candidates # TODO: store new unseen aggregates # TODO: update performance of bits based on aggregates. return candidates ``` ```python def make_aggregate(strategy_state: AttestationStrategy) -> Attestation: # TODO: just combine seen attestations into some aggregate, even if unorganized/sparse. # Start with the best-scoring ones. Should be fast, even with many half-aggregated inputs return aggregate ``` ```python def propagation_ok(strategy_state: AttestationStrategy, incoming: Attestation, time_elapsed: int) -> bool: # TODO: score the attestation based on: # - partitioning of bits. Try make non-overlapping attestations # - the bits it has, and if they have been seen already or not, and each of their previous performance. threshold = # for the time elapsed, determine how eagerly we want to propagate it. if score > current: # - increase performance score of affected bits return True return False ```