[Altair] Sync committee pools (#2321)

Add pools supporting sync committees:
- naive sync aggregation pool
- observed sync contributions pool
- observed sync contributors pool
- observed sync aggregators pool

Add SSZ types and tests related to sync committee signatures.

Co-authored-by: Michael Sproul <michael@sigmaprime.io>
Co-authored-by: realbigsean <seananderson33@gmail.com>
This commit is contained in:
realbigsean
2021-07-15 00:52:02 +00:00
parent 8fa6e463ca
commit a3a7f39b0d
59 changed files with 5277 additions and 933 deletions

View File

@@ -1,14 +1,17 @@
use super::{
AggregateSignature, AttestationData, BitList, ChainSpec, Domain, EthSpec, Fork, SecretKey,
SignedRoot,
};
use crate::{test_utils::TestRandom, Hash256};
use safe_arith::ArithError;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
use crate::slot_data::SlotData;
use crate::{test_utils::TestRandom, Hash256, Slot};
use super::{
AggregateSignature, AttestationData, BitList, ChainSpec, Domain, EthSpec, Fork, SecretKey,
SignedRoot,
};
#[derive(Debug, PartialEq)]
pub enum Error {
SszTypesError(ssz_types::Error),
@@ -84,10 +87,17 @@ impl<T: EthSpec> Attestation<T> {
}
}
impl<T: EthSpec> SlotData for Attestation<T> {
fn get_slot(&self) -> Slot {
self.data.slot
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
use super::*;
ssz_and_tree_hash_tests!(Attestation<MainnetEthSpec>);
}

View File

@@ -1,6 +1,7 @@
use crate::test_utils::TestRandom;
use crate::{Checkpoint, Hash256, SignedRoot, Slot};
use crate::slot_data::SlotData;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
@@ -39,6 +40,12 @@ pub struct AttestationData {
impl SignedRoot for AttestationData {}
impl SlotData for AttestationData {
fn get_slot(&self) -> Slot {
self.slot
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -15,7 +15,7 @@ use ssz_derive::{Decode, Encode};
use ssz_types::{typenum::Unsigned, BitVector, FixedVector};
use std::collections::HashSet;
use std::convert::TryInto;
use std::{fmt, mem};
use std::{fmt, mem, sync::Arc};
use superstruct::superstruct;
use swap_or_not_shuffle::compute_shuffled_index;
use test_random_derive::TestRandom;
@@ -110,6 +110,10 @@ pub enum Error {
ArithError(ArithError),
MissingBeaconBlock(SignedBeaconBlockHash),
MissingBeaconState(BeaconStateHash),
SyncCommitteeNotKnown {
current_epoch: Epoch,
epoch: Epoch,
},
}
/// Control whether an epoch-indexed field can be indexed at the next epoch or not.
@@ -255,9 +259,9 @@ where
// Light-client sync committees
#[superstruct(only(Altair))]
pub current_sync_committee: SyncCommittee<T>,
pub current_sync_committee: Arc<SyncCommittee<T>>,
#[superstruct(only(Altair))]
pub next_sync_committee: SyncCommittee<T>,
pub next_sync_committee: Arc<SyncCommittee<T>>,
// Caching (not in the spec)
#[serde(skip_serializing, skip_deserializing)]
@@ -730,6 +734,28 @@ impl<T: EthSpec> BeaconState<T> {
Ok(hash(&preimage))
}
/// Get the already-built current or next sync committee from the state.
pub fn get_built_sync_committee(
&self,
epoch: Epoch,
spec: &ChainSpec,
) -> Result<&Arc<SyncCommittee<T>>, Error> {
let sync_committee_period = epoch.sync_committee_period(spec)?;
let current_sync_committee_period = self.current_epoch().sync_committee_period(spec)?;
let next_sync_committee_period = current_sync_committee_period.safe_add(1)?;
if sync_committee_period == current_sync_committee_period {
self.current_sync_committee()
} else if sync_committee_period == next_sync_committee_period {
self.next_sync_committee()
} else {
Err(Error::SyncCommitteeNotKnown {
current_epoch: self.current_epoch(),
epoch,
})
}
}
/// Get the validator indices of all validators from `sync_committee`.
pub fn get_sync_committee_indices(
&mut self,
@@ -1514,6 +1540,28 @@ impl<T: EthSpec> BeaconState<T> {
(self.previous_epoch() - self.finalized_checkpoint().epoch)
> spec.min_epochs_to_inactivity_penalty
}
/// Get the `SyncCommittee` associated with the next slot. Useful because sync committees
/// assigned to `slot` sign for `slot - 1`. This creates the exceptional logic below when
/// transitioning between sync committee periods.
pub fn get_sync_committee_for_next_slot(
&self,
spec: &ChainSpec,
) -> Result<Arc<SyncCommittee<T>>, Error> {
let next_slot_epoch = self
.slot()
.saturating_add(Slot::new(1))
.epoch(T::slots_per_epoch());
let sync_committee = if self.current_epoch().sync_committee_period(spec)
== next_slot_epoch.sync_committee_period(spec)
{
self.current_sync_committee()?.clone()
} else {
self.next_sync_committee()?.clone()
};
Ok(sync_committee)
}
}
impl From<RelativeEpochError> for Error {

View File

@@ -17,6 +17,8 @@ pub enum Domain {
SelectionProof,
AggregateAndProof,
SyncCommittee,
ContributionAndProof,
SyncCommitteeSelectionProof,
}
/// Lighthouse's internal configuration struct.
@@ -181,6 +183,37 @@ impl ChainSpec {
}
}
/// Returns the fork version for a named fork.
pub fn fork_version_for_name(&self, fork_name: ForkName) -> [u8; 4] {
match fork_name {
ForkName::Base => self.genesis_fork_version,
ForkName::Altair => self.altair_fork_version,
}
}
/// For a given fork name, return the epoch at which it activates.
pub fn fork_epoch(&self, fork_name: ForkName) -> Option<Epoch> {
match fork_name {
ForkName::Base => Some(Epoch::new(0)),
ForkName::Altair => self.altair_fork_epoch,
}
}
/// Returns a full `Fork` struct for a given epoch.
pub fn fork_at_epoch(&self, epoch: Epoch) -> Fork {
let current_fork_name = self.fork_name_at_epoch(epoch);
let previous_fork_name = current_fork_name.previous_fork().unwrap_or(ForkName::Base);
let epoch = self
.fork_epoch(current_fork_name)
.unwrap_or_else(|| Epoch::new(0));
Fork {
previous_version: self.fork_version_for_name(previous_fork_name),
current_version: self.fork_version_for_name(current_fork_name),
epoch,
}
}
/// Get the domain number, unmodified by the fork.
///
/// Spec v0.12.1
@@ -194,6 +227,8 @@ impl ChainSpec {
Domain::SelectionProof => self.domain_selection_proof,
Domain::AggregateAndProof => self.domain_aggregate_and_proof,
Domain::SyncCommittee => self.domain_sync_committee,
Domain::ContributionAndProof => self.domain_contribution_and_proof,
Domain::SyncCommitteeSelectionProof => self.domain_sync_committee_selection_proof,
}
}
@@ -675,6 +710,18 @@ mod tests {
);
test_domain(Domain::SyncCommittee, spec.domain_sync_committee, &spec);
}
// Test that `fork_name_at_epoch` and `fork_epoch` are consistent.
#[test]
fn fork_name_at_epoch_consistency() {
let spec = ChainSpec::mainnet();
for fork_name in ForkName::list_all() {
if let Some(fork_epoch) = spec.fork_epoch(fork_name) {
assert_eq!(spec.fork_name_at_epoch(fork_epoch), fork_name);
}
}
}
}
#[cfg(test)]

View File

@@ -8,6 +8,8 @@ pub mod altair {
pub const SYNC_REWARD_WEIGHT: u64 = 2;
pub const PROPOSER_WEIGHT: u64 = 8;
pub const WEIGHT_DENOMINATOR: u64 = 64;
pub const SYNC_COMMITTEE_SUBNET_COUNT: u64 = 4;
pub const TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: u64 = 4;
pub const PARTICIPATION_FLAG_WEIGHTS: [u64; NUM_FLAG_INDICES] = [
TIMELY_SOURCE_WEIGHT,

View File

@@ -0,0 +1,61 @@
use super::{
ChainSpec, EthSpec, Fork, Hash256, SecretKey, Signature, SignedRoot, SyncCommitteeContribution,
SyncSelectionProof,
};
use crate::test_utils::TestRandom;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// A Validators aggregate sync committee contribution and selection proof.
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom, TreeHash)]
#[serde(bound = "T: EthSpec")]
pub struct ContributionAndProof<T: EthSpec> {
/// The index of the validator that created the sync contribution.
#[serde(with = "serde_utils::quoted_u64")]
pub aggregator_index: u64,
/// The aggregate contribution.
pub contribution: SyncCommitteeContribution<T>,
/// A proof provided by the validator that permits them to publish on the
/// `sync_committee_contribution_and_proof` gossipsub topic.
pub selection_proof: Signature,
}
impl<T: EthSpec> ContributionAndProof<T> {
/// Produces a new `ContributionAndProof` with a `selection_proof` generated by signing
/// `SyncAggregatorSelectionData` with `secret_key`.
///
/// If `selection_proof.is_none()` it will be computed locally.
pub fn from_aggregate(
aggregator_index: u64,
contribution: SyncCommitteeContribution<T>,
selection_proof: Option<SyncSelectionProof>,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Self {
let selection_proof = selection_proof
.unwrap_or_else(|| {
SyncSelectionProof::new::<T>(
contribution.slot,
contribution.subcommittee_index,
secret_key,
fork,
genesis_validators_root,
spec,
)
})
.into();
Self {
aggregator_index,
contribution,
selection_proof,
}
}
}
impl<T: EthSpec> SignedRoot for ContributionAndProof<T> {}

View File

@@ -91,6 +91,10 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq +
///
/// Must be set to `EpochsPerEth1VotingPeriod * SlotsPerEpoch`
type SlotsPerEth1VotingPeriod: Unsigned + Clone + Sync + Send + Debug + PartialEq;
/// The size of `sync_subcommittees`.
///
/// Must be set to `SyncCommitteeSize / SyncCommitteeSubnetCount`.
type SyncSubcommitteeSize: Unsigned + Clone + Sync + Send + Debug + PartialEq;
fn default_spec() -> ChainSpec;
@@ -171,6 +175,16 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq +
fn slots_per_eth1_voting_period() -> usize {
Self::SlotsPerEth1VotingPeriod::to_usize()
}
/// Returns the `SYNC_COMMITTEE_SIZE` constant for this specification.
fn sync_committee_size() -> usize {
Self::SyncCommitteeSize::to_usize()
}
/// Returns the `SYNC_COMMITTEE_SIZE / SyncCommitteeSubnetCount`.
fn sync_subcommittee_size() -> usize {
Self::SyncSubcommitteeSize::to_usize()
}
}
/// Macro to inherit some type values from another EthSpec.
@@ -204,6 +218,7 @@ impl EthSpec for MainnetEthSpec {
type MaxDeposits = U16;
type MaxVoluntaryExits = U16;
type SyncCommitteeSize = U512;
type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count
type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch
type SlotsPerEth1VotingPeriod = U2048; // 64 epochs * 32 slots per epoch
@@ -228,6 +243,7 @@ impl EthSpec for MinimalEthSpec {
type EpochsPerHistoricalVector = U64;
type EpochsPerSlashingsVector = U64;
type SyncCommitteeSize = U32;
type SyncSubcommitteeSize = U8; // 32 committee size / 4 sync committee subnet count
type MaxPendingAttestations = U1024; // 128 max attestations * 8 slots per epoch
type SlotsPerEth1VotingPeriod = U32; // 4 epochs * 8 slots per epoch

View File

@@ -26,6 +26,26 @@ impl ForkName {
}
}
}
/// Return the name of the fork immediately prior to the current one.
///
/// If `self` is `ForkName::Base` then `Base` is returned.
pub fn previous_fork(self) -> Option<ForkName> {
match self {
ForkName::Base => None,
ForkName::Altair => Some(ForkName::Base),
}
}
/// Return the name of the fork immediately after the current one.
///
/// If `self` is the last known fork and has no successor, `None` is returned.
pub fn next_fork(self) -> Option<ForkName> {
match self {
ForkName::Base => Some(ForkName::Altair),
ForkName::Altair => None,
}
}
}
impl std::str::FromStr for ForkName {
@@ -45,3 +65,20 @@ pub struct InconsistentFork {
pub fork_at_slot: ForkName,
pub object_fork: ForkName,
}
#[cfg(test)]
mod test {
use super::*;
use itertools::Itertools;
#[test]
fn previous_and_next_fork_consistent() {
assert_eq!(ForkName::Altair.next_fork(), None);
assert_eq!(ForkName::Base.previous_fork(), None);
for (prev_fork, fork) in ForkName::list_all().into_iter().tuple_windows() {
assert_eq!(prev_fork.next_fork(), Some(fork));
assert_eq!(fork.previous_fork(), Some(prev_fork));
}
}
}

View File

@@ -30,6 +30,7 @@ pub mod beacon_state;
pub mod chain_spec;
pub mod checkpoint;
pub mod consts;
pub mod contribution_and_proof;
pub mod deposit;
pub mod deposit_data;
pub mod deposit_message;
@@ -51,6 +52,7 @@ pub mod shuffling_id;
pub mod signed_aggregate_and_proof;
pub mod signed_beacon_block;
pub mod signed_beacon_block_header;
pub mod signed_contribution_and_proof;
pub mod signed_voluntary_exit;
pub mod signing_data;
pub mod validator;
@@ -64,9 +66,15 @@ pub mod preset;
pub mod slot_epoch;
pub mod subnet_id;
pub mod sync_aggregate;
pub mod sync_aggregator_selection_data;
pub mod sync_committee;
pub mod sync_committee_contribution;
pub mod sync_committee_message;
pub mod sync_selection_proof;
pub mod sync_subnet_id;
mod tree_hash_impls;
pub mod slot_data;
#[cfg(feature = "sqlite")]
pub mod sqlite;
@@ -90,6 +98,7 @@ pub use crate::beacon_state::{BeaconTreeHashCache, Error as BeaconStateError, *}
pub use crate::chain_spec::{ChainSpec, Config, Domain};
pub use crate::checkpoint::Checkpoint;
pub use crate::config_and_preset::ConfigAndPreset;
pub use crate::contribution_and_proof::ContributionAndProof;
pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH};
pub use crate::deposit_data::DepositData;
pub use crate::deposit_message::DepositMessage;
@@ -115,12 +124,18 @@ pub use crate::signed_beacon_block::{
SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockHash,
};
pub use crate::signed_beacon_block_header::SignedBeaconBlockHeader;
pub use crate::signed_contribution_and_proof::SignedContributionAndProof;
pub use crate::signed_voluntary_exit::SignedVoluntaryExit;
pub use crate::signing_data::{SignedRoot, SigningData};
pub use crate::slot_epoch::{Epoch, Slot};
pub use crate::subnet_id::SubnetId;
pub use crate::sync_aggregate::SyncAggregate;
pub use crate::sync_aggregator_selection_data::SyncAggregatorSelectionData;
pub use crate::sync_committee::SyncCommittee;
pub use crate::sync_committee_contribution::SyncCommitteeContribution;
pub use crate::sync_committee_message::SyncCommitteeMessage;
pub use crate::sync_selection_proof::SyncSelectionProof;
pub use crate::sync_subnet_id::SyncSubnetId;
pub use crate::validator::Validator;
pub use crate::validator_subscription::ValidatorSubscription;
pub use crate::voluntary_exit::VoluntaryExit;

View File

@@ -1,6 +1,6 @@
use super::{
AggregateAndProof, Attestation, ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey,
SecretKey, SelectionProof, Signature, SignedRoot,
AggregateAndProof, Attestation, ChainSpec, Domain, EthSpec, Fork, Hash256, SecretKey,
SelectionProof, Signature, SignedRoot,
};
use crate::test_utils::TestRandom;
use serde_derive::{Deserialize, Serialize};
@@ -60,41 +60,4 @@ impl<T: EthSpec> SignedAggregateAndProof<T> {
signature: secret_key.sign(signing_message),
}
}
/// Verifies the signature of the `AggregateAndProof`
pub fn is_valid_signature(
&self,
validator_pubkey: &PublicKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> bool {
let target_epoch = self.message.aggregate.data.slot.epoch(T::slots_per_epoch());
let domain = spec.get_domain(
target_epoch,
Domain::AggregateAndProof,
fork,
genesis_validators_root,
);
let message = self.message.signing_root(domain);
self.signature.verify(validator_pubkey, message)
}
/// Verifies the signature of the `AggregateAndProof` as well the underlying selection_proof in
/// the contained `AggregateAndProof`.
pub fn is_valid(
&self,
validator_pubkey: &PublicKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> bool {
self.is_valid_signature(validator_pubkey, fork, genesis_validators_root, spec)
&& self.message.is_valid_selection_proof(
validator_pubkey,
fork,
genesis_validators_root,
spec,
)
}
}

View File

@@ -0,0 +1,61 @@
use super::{
ChainSpec, ContributionAndProof, Domain, EthSpec, Fork, Hash256, SecretKey, Signature,
SignedRoot, SyncCommitteeContribution, SyncSelectionProof,
};
use crate::test_utils::TestRandom;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// A Validators signed contribution proof to publish on the `sync_committee_contribution_and_proof`
/// gossipsub topic.
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom, TreeHash)]
#[serde(bound = "T: EthSpec")]
pub struct SignedContributionAndProof<T: EthSpec> {
/// The `ContributionAndProof` that was signed.
pub message: ContributionAndProof<T>,
/// The validator's signature of `message`.
pub signature: Signature,
}
impl<T: EthSpec> SignedContributionAndProof<T> {
/// Produces a new `SignedContributionAndProof` with a `selection_proof` generated by signing
/// `aggregate.data.slot` with `secret_key`.
///
/// If `selection_proof.is_none()` it will be computed locally.
pub fn from_aggregate(
aggregator_index: u64,
contribution: SyncCommitteeContribution<T>,
selection_proof: Option<SyncSelectionProof>,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Self {
let message = ContributionAndProof::from_aggregate(
aggregator_index,
contribution,
selection_proof,
secret_key,
fork,
genesis_validators_root,
spec,
);
let epoch = message.contribution.slot.epoch(T::slots_per_epoch());
let domain = spec.get_domain(
epoch,
Domain::ContributionAndProof,
fork,
genesis_validators_root,
);
let signing_message = message.signing_root(domain);
SignedContributionAndProof {
message,
signature: secret_key.sign(signing_message),
}
}
}

View File

@@ -0,0 +1,13 @@
use crate::Slot;
/// A trait providing a `Slot` getter for messages that are related to a single slot. Useful in
/// making parts of attestation and sync committee processing generic.
pub trait SlotData {
fn get_slot(&self) -> Slot;
}
impl SlotData for Slot {
fn get_slot(&self) -> Slot {
*self
}
}

View File

@@ -11,10 +11,10 @@
//! may lead to programming errors which are not detected by the compiler.
use crate::test_utils::TestRandom;
use crate::SignedRoot;
use crate::{ChainSpec, SignedRoot};
use rand::RngCore;
use safe_arith::SafeArith;
use safe_arith::{ArithError, SafeArith};
use serde_derive::{Deserialize, Serialize};
use ssz::{ssz_encode, Decode, DecodeError, Encode};
use std::fmt;
@@ -90,6 +90,13 @@ impl Epoch {
}
}
/// Compute the sync committee period for an epoch.
pub fn sync_committee_period(&self, spec: &ChainSpec) -> Result<u64, ArithError> {
Ok(self
.safe_div(spec.epochs_per_sync_committee_period)?
.as_u64())
}
pub fn slot_iter(&self, slots_per_epoch: u64) -> SlotIter {
SlotIter {
current_iteration: 0,

View File

@@ -1,10 +1,24 @@
use crate::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT;
use crate::test_utils::TestRandom;
use crate::{AggregateSignature, BitVector, EthSpec};
use crate::{AggregateSignature, BitVector, EthSpec, SyncCommitteeContribution};
use safe_arith::{ArithError, SafeArith};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
#[derive(Debug, PartialEq)]
pub enum Error {
SszTypesError(ssz_types::Error),
ArithError(ArithError),
}
impl From<ArithError> for Error {
fn from(e: ArithError) -> Error {
Error::ArithError(e)
}
}
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
@@ -23,6 +37,34 @@ impl<T: EthSpec> SyncAggregate<T> {
}
}
/// Create a `SyncAggregate` from a slice of `SyncCommitteeContribution`s.
///
/// Equivalent to `process_sync_committee_contributions` from the spec.
pub fn from_contributions(
contributions: &[SyncCommitteeContribution<T>],
) -> Result<SyncAggregate<T>, Error> {
let mut sync_aggregate = Self::new();
let sync_subcommittee_size =
T::sync_committee_size().safe_div(SYNC_COMMITTEE_SUBNET_COUNT as usize)?;
for contribution in contributions {
for (index, participated) in contribution.aggregation_bits.iter().enumerate() {
if participated {
let participant_index = sync_subcommittee_size
.safe_mul(contribution.subcommittee_index as usize)?
.safe_add(index)?;
sync_aggregate
.sync_committee_bits
.set(participant_index, true)
.map_err(Error::SszTypesError)?;
}
}
sync_aggregate
.sync_committee_signature
.add_assign_aggregate(&contribution.signature);
}
Ok(sync_aggregate)
}
/// Empty aggregate to be used at genesis.
///
/// Contains an empty signature and should *not* be used as the starting point for aggregation,

View File

@@ -0,0 +1,25 @@
use crate::test_utils::TestRandom;
use crate::{SignedRoot, Slot};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(
Debug, PartialEq, Clone, Serialize, Deserialize, Hash, Encode, Decode, TreeHash, TestRandom,
)]
pub struct SyncAggregatorSelectionData {
pub slot: Slot,
pub subcommittee_index: u64,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(SyncAggregatorSelectionData);
}
impl SignedRoot for SyncAggregatorSelectionData {}

View File

@@ -1,12 +1,30 @@
use crate::test_utils::TestRandom;
use crate::typenum::Unsigned;
use crate::{EthSpec, FixedVector};
use crate::{EthSpec, FixedVector, SyncSubnetId};
use bls::PublicKeyBytes;
use safe_arith::{ArithError, SafeArith};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use std::collections::HashMap;
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
#[derive(Debug, PartialEq)]
pub enum Error {
ArithError(ArithError),
InvalidSubcommitteeRange {
start_subcommittee_index: usize,
end_subcommittee_index: usize,
subcommittee_index: usize,
},
}
impl From<ArithError> for Error {
fn from(e: ArithError) -> Error {
Error::ArithError(e)
}
}
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
@@ -26,4 +44,44 @@ impl<T: EthSpec> SyncCommittee<T> {
aggregate_pubkey: PublicKeyBytes::empty(),
})
}
/// Return the pubkeys in this `SyncCommittee` for the given `subcommittee_index`.
pub fn get_subcommittee_pubkeys(
&self,
subcommittee_index: usize,
) -> Result<Vec<PublicKeyBytes>, Error> {
let start_subcommittee_index = subcommittee_index.safe_mul(T::sync_subcommittee_size())?;
let end_subcommittee_index =
start_subcommittee_index.safe_add(T::sync_subcommittee_size())?;
self.pubkeys
.get(start_subcommittee_index..end_subcommittee_index)
.ok_or(Error::InvalidSubcommitteeRange {
start_subcommittee_index,
end_subcommittee_index,
subcommittee_index,
})
.map(|s| s.to_vec())
}
/// For a given `pubkey`, finds all subcommittees that it is included in, and maps the
/// subcommittee index (typed as `SyncSubnetId`) to all positions this `pubkey` is associated
/// with within the subcommittee.
pub fn subcommittee_positions_for_public_key(
&self,
pubkey: &PublicKeyBytes,
) -> Result<HashMap<SyncSubnetId, Vec<usize>>, Error> {
let mut subnet_positions = HashMap::new();
for (committee_index, validator_pubkey) in self.pubkeys.iter().enumerate() {
if pubkey == validator_pubkey {
let subcommittee_index = committee_index.safe_div(T::sync_subcommittee_size())?;
let position_in_subcommittee =
committee_index.safe_rem(T::sync_subcommittee_size())?;
subnet_positions
.entry(SyncSubnetId::new(subcommittee_index as u64))
.or_insert_with(Vec::new)
.push(position_in_subcommittee);
}
}
Ok(subnet_positions)
}
}

View File

@@ -0,0 +1,113 @@
use super::{AggregateSignature, EthSpec, SignedRoot};
use crate::slot_data::SlotData;
use crate::{test_utils::TestRandom, BitVector, Hash256, Slot, SyncCommitteeMessage};
use safe_arith::ArithError;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
#[derive(Debug, PartialEq)]
pub enum Error {
SszTypesError(ssz_types::Error),
AlreadySigned(usize),
SubnetCountIsZero(ArithError),
}
/// An aggregation of `SyncCommitteeMessage`s, used in creating a `SignedContributionAndProof`.
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct SyncCommitteeContribution<T: EthSpec> {
pub slot: Slot,
pub beacon_block_root: Hash256,
pub subcommittee_index: u64,
pub aggregation_bits: BitVector<T::SyncSubcommitteeSize>,
pub signature: AggregateSignature,
}
impl<T: EthSpec> SyncCommitteeContribution<T> {
/// Create a `SyncCommitteeContribution` from:
///
/// - `message`: A single `SyncCommitteeMessage`.
/// - `subcommittee_index`: The subcommittee this contribution pertains to out of the broader
/// sync committee. This can be determined from the `SyncSubnetId` of the gossip subnet
/// this message was seen on.
/// - `validator_sync_committee_index`: The index of the validator **within** the subcommittee.
pub fn from_message(
message: &SyncCommitteeMessage,
subcommittee_index: u64,
validator_sync_committee_index: usize,
) -> Result<Self, Error> {
let mut bits = BitVector::new();
bits.set(validator_sync_committee_index, true)
.map_err(Error::SszTypesError)?;
Ok(Self {
slot: message.slot,
beacon_block_root: message.beacon_block_root,
subcommittee_index,
aggregation_bits: bits,
signature: AggregateSignature::from(&message.signature),
})
}
/// Are the aggregation bitfields of these sync contribution disjoint?
pub fn signers_disjoint_from(&self, other: &Self) -> bool {
self.aggregation_bits
.intersection(&other.aggregation_bits)
.is_zero()
}
/// Aggregate another `SyncCommitteeContribution` into this one.
///
/// The aggregation bitfields must be disjoint, and the data must be the same.
pub fn aggregate(&mut self, other: &Self) {
debug_assert_eq!(self.slot, other.slot);
debug_assert_eq!(self.beacon_block_root, other.beacon_block_root);
debug_assert_eq!(self.subcommittee_index, other.subcommittee_index);
debug_assert!(self.signers_disjoint_from(other));
self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits);
self.signature.add_assign_aggregate(&other.signature);
}
}
impl SignedRoot for Hash256 {}
/// This is not in the spec, but useful for determining uniqueness of sync committee contributions
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct SyncContributionData {
slot: Slot,
beacon_block_root: Hash256,
subcommittee_index: u64,
}
impl SyncContributionData {
pub fn from_contribution<T: EthSpec>(signing_data: &SyncCommitteeContribution<T>) -> Self {
Self {
slot: signing_data.slot,
beacon_block_root: signing_data.beacon_block_root,
subcommittee_index: signing_data.subcommittee_index,
}
}
}
impl<T: EthSpec> SlotData for SyncCommitteeContribution<T> {
fn get_slot(&self) -> Slot {
self.slot
}
}
impl SlotData for SyncContributionData {
fn get_slot(&self) -> Slot {
self.slot
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
ssz_and_tree_hash_tests!(SyncCommitteeContribution<MainnetEthSpec>);
}

View File

@@ -0,0 +1,57 @@
use crate::test_utils::TestRandom;
use crate::{ChainSpec, Domain, EthSpec, Fork, Hash256, SecretKey, Signature, SignedRoot, Slot};
use crate::slot_data::SlotData;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// The data upon which a `SyncCommitteeContribution` is based.
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct SyncCommitteeMessage {
pub slot: Slot,
pub beacon_block_root: Hash256,
#[serde(with = "serde_utils::quoted_u64")]
pub validator_index: u64,
// Signature by the validator over `beacon_block_root`.
pub signature: Signature,
}
impl SyncCommitteeMessage {
/// Equivalent to `get_sync_committee_message` from the spec.
pub fn new<E: EthSpec>(
slot: Slot,
beacon_block_root: Hash256,
validator_index: u64,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Self {
let epoch = slot.epoch(E::slots_per_epoch());
let domain = spec.get_domain(epoch, Domain::SyncCommittee, fork, genesis_validators_root);
let message = beacon_block_root.signing_root(domain);
let signature = secret_key.sign(message);
Self {
slot,
beacon_block_root,
validator_index,
signature,
}
}
}
impl SlotData for SyncCommitteeMessage {
fn get_slot(&self) -> Slot {
self.slot
}
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(SyncCommitteeMessage);
}

View File

@@ -0,0 +1,139 @@
use crate::consts::altair::{
SYNC_COMMITTEE_SUBNET_COUNT, TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE,
};
use crate::{
ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, SecretKey, Signature, SignedRoot, Slot,
SyncAggregatorSelectionData,
};
use eth2_hashing::hash;
use safe_arith::{ArithError, SafeArith};
use ssz::Encode;
use ssz_types::typenum::Unsigned;
use std::cmp;
use std::convert::TryInto;
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(PartialEq, Debug, Clone)]
pub struct SyncSelectionProof(Signature);
impl SyncSelectionProof {
pub fn new<T: EthSpec>(
slot: Slot,
subcommittee_index: u64,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Self {
let domain = spec.get_domain(
slot.epoch(T::slots_per_epoch()),
Domain::SyncCommitteeSelectionProof,
fork,
genesis_validators_root,
);
let message = SyncAggregatorSelectionData {
slot,
subcommittee_index,
}
.signing_root(domain);
Self(secret_key.sign(message))
}
/// Returns the "modulo" used for determining if a `SyncSelectionProof` elects an aggregator.
pub fn modulo<T: EthSpec>() -> Result<u64, ArithError> {
Ok(cmp::max(
1,
(T::SyncCommitteeSize::to_u64())
.safe_div(SYNC_COMMITTEE_SUBNET_COUNT)?
.safe_div(TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE)?,
))
}
pub fn is_aggregator<T: EthSpec>(&self) -> Result<bool, ArithError> {
self.is_aggregator_from_modulo(Self::modulo::<T>()?)
}
pub fn is_aggregator_from_modulo(&self, modulo: u64) -> Result<bool, ArithError> {
let signature_hash = hash(&self.0.as_ssz_bytes());
let signature_hash_int = u64::from_le_bytes(
signature_hash
.get(0..8)
.expect("hash is 32 bytes")
.try_into()
.expect("first 8 bytes of signature should always convert to fixed array"),
);
signature_hash_int.safe_rem(modulo).map(|rem| rem == 0)
}
pub fn verify<T: EthSpec>(
&self,
slot: Slot,
subcommittee_index: u64,
pubkey: &PublicKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> bool {
let domain = spec.get_domain(
slot.epoch(T::slots_per_epoch()),
Domain::SyncCommitteeSelectionProof,
fork,
genesis_validators_root,
);
let message = SyncAggregatorSelectionData {
slot,
subcommittee_index,
}
.signing_root(domain);
self.0.verify(pubkey, message)
}
}
impl Into<Signature> for SyncSelectionProof {
fn into(self) -> Signature {
self.0
}
}
impl From<Signature> for SyncSelectionProof {
fn from(sig: Signature) -> Self {
Self(sig)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::MainnetEthSpec;
use eth2_interop_keypairs::keypair;
#[test]
fn proof_sign_and_verify() {
let slot = Slot::new(1000);
let subcommittee_index = 12;
let key = keypair(1);
let fork = &Fork::default();
let genesis_validators_root = Hash256::zero();
let spec = &ChainSpec::mainnet();
let proof = SyncSelectionProof::new::<MainnetEthSpec>(
slot,
subcommittee_index,
&key.sk,
fork,
genesis_validators_root,
spec,
);
assert!(proof.verify::<MainnetEthSpec>(
slot,
subcommittee_index,
&key.pk,
fork,
genesis_validators_root,
spec
));
}
}

View File

@@ -0,0 +1,74 @@
//! Identifies each sync committee subnet by an integer identifier.
use crate::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT;
use serde_derive::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};
lazy_static! {
static ref SYNC_SUBNET_ID_TO_STRING: Vec<String> = {
let mut v = Vec::with_capacity(SYNC_COMMITTEE_SUBNET_COUNT as usize);
for i in 0..SYNC_COMMITTEE_SUBNET_COUNT {
v.push(i.to_string());
}
v
};
}
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SyncSubnetId(#[serde(with = "serde_utils::quoted_u64")] u64);
pub fn sync_subnet_id_to_string(i: u64) -> &'static str {
if i < SYNC_COMMITTEE_SUBNET_COUNT {
&SYNC_SUBNET_ID_TO_STRING
.get(i as usize)
.expect("index below SYNC_COMMITTEE_SUBNET_COUNT")
} else {
"sync subnet id out of range"
}
}
impl SyncSubnetId {
pub fn new(id: u64) -> Self {
id.into()
}
}
impl Deref for SyncSubnetId {
type Target = u64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SyncSubnetId {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<u64> for SyncSubnetId {
fn from(x: u64) -> Self {
Self(x)
}
}
impl Into<u64> for SyncSubnetId {
fn into(self) -> u64 {
self.0
}
}
impl Into<u64> for &SyncSubnetId {
fn into(self) -> u64 {
self.0
}
}
impl AsRef<str> for SyncSubnetId {
fn as_ref(&self) -> &str {
sync_subnet_id_to_string(self.0)
}
}

View File

@@ -3,6 +3,7 @@ use rand::RngCore;
use rand::SeedableRng;
use rand_xorshift::XorShiftRng;
use ssz_types::typenum::Unsigned;
use std::sync::Arc;
mod address;
mod aggregate_signature;
@@ -68,6 +69,15 @@ where
}
}
impl<U> TestRandom for Arc<U>
where
U: TestRandom,
{
fn random_for_test(rng: &mut impl RngCore) -> Self {
Arc::new(U::random_for_test(rng))
}
}
impl<T, N: Unsigned> TestRandom for FixedVector<T, N>
where
T: TestRandom,