Directory Restructure (#1163)

* Move tests -> testing

* Directory restructure

* Update Cargo.toml during restructure

* Update Makefile during restructure

* Fix arbitrary path
This commit is contained in:
Paul Hauner
2020-05-18 21:24:23 +10:00
committed by GitHub
parent c571afb8d8
commit 4331834003
358 changed files with 217 additions and 229 deletions

View File

@@ -0,0 +1,81 @@
use super::{
Attestation, ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, SecretKey, SelectionProof,
Signature, SignedRoot,
};
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 attestation and selection proof.
///
/// Spec v0.10.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom, TreeHash)]
#[serde(bound = "T: EthSpec")]
pub struct AggregateAndProof<T: EthSpec> {
/// The index of the validator that created the attestation.
pub aggregator_index: u64,
/// The aggregate attestation.
pub aggregate: Attestation<T>,
/// A proof provided by the validator that permits them to publish on the
/// `beacon_aggregate_and_proof` gossipsub topic.
pub selection_proof: Signature,
}
impl<T: EthSpec> AggregateAndProof<T> {
/// Produces a new `AggregateAndProof` 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,
aggregate: Attestation<T>,
selection_proof: Option<SelectionProof>,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Self {
let selection_proof = selection_proof
.unwrap_or_else(|| {
SelectionProof::new::<T>(
aggregate.data.slot,
secret_key,
fork,
genesis_validators_root,
spec,
)
})
.into();
Self {
aggregator_index,
aggregate,
selection_proof,
}
}
/// Returns `true` if `validator_pubkey` signed over `self.aggregate.data.slot`.
pub fn is_valid_selection_proof(
&self,
validator_pubkey: &PublicKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> bool {
let target_epoch = self.aggregate.data.slot.epoch(T::slots_per_epoch());
let domain = spec.get_domain(
target_epoch,
Domain::SelectionProof,
fork,
genesis_validators_root,
);
let message = self.aggregate.data.slot.signing_root(domain);
self.selection_proof
.verify(message.as_bytes(), validator_pubkey)
}
}
impl<T: EthSpec> SignedRoot for AggregateAndProof<T> {}

View File

@@ -0,0 +1,107 @@
use super::{
AggregateSignature, AttestationData, BitList, ChainSpec, Domain, EthSpec, Fork, SecretKey,
Signature, SignedRoot, SubnetId,
};
use crate::{test_utils::TestRandom, Hash256};
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),
AlreadySigned(usize),
SubnetCountIsZero(ArithError),
}
/// Details an attestation that can be slashable.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct Attestation<T: EthSpec> {
pub aggregation_bits: BitList<T::MaxValidatorsPerCommittee>,
pub data: AttestationData,
pub signature: AggregateSignature,
}
impl<T: EthSpec> Attestation<T> {
/// Are the aggregation bitfields of these attestations disjoint?
pub fn signers_disjoint_from(&self, other: &Self) -> bool {
self.aggregation_bits
.intersection(&other.aggregation_bits)
.is_zero()
}
/// Aggregate another Attestation 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.data, other.data);
debug_assert!(self.signers_disjoint_from(other));
self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits);
self.signature.add_aggregate(&other.signature);
}
/// Signs `self`, setting the `committee_position`'th bit of `aggregation_bits` to `true`.
///
/// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`.
pub fn sign(
&mut self,
secret_key: &SecretKey,
committee_position: usize,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Result<(), Error> {
if self
.aggregation_bits
.get(committee_position)
.map_err(Error::SszTypesError)?
{
Err(Error::AlreadySigned(committee_position))
} else {
self.aggregation_bits
.set(committee_position, true)
.map_err(Error::SszTypesError)?;
let domain = spec.get_domain(
self.data.target.epoch,
Domain::BeaconAttester,
fork,
genesis_validators_root,
);
let message = self.data.signing_root(domain);
self.signature
.add(&Signature::new(message.as_bytes(), secret_key));
Ok(())
}
}
/// Returns the subnet id associated with the attestation.
///
/// Note, this will return the subnet id for an aggregated attestation. This is done
/// to avoid checking aggregate bits every time we wish to get an id.
pub fn subnet_id(&self, spec: &ChainSpec) -> Result<SubnetId, Error> {
self.data
.index
.safe_rem(spec.attestation_subnet_count)
.map(SubnetId::new)
.map_err(Error::SubnetCountIsZero)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
ssz_and_tree_hash_tests!(Attestation<MainnetEthSpec>);
}

View File

@@ -0,0 +1,46 @@
use crate::test_utils::TestRandom;
use crate::{Checkpoint, Hash256, SignedRoot, Slot};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// The data upon which an attestation is based.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
Hash,
Encode,
Decode,
TreeHash,
TestRandom,
Default,
)]
pub struct AttestationData {
pub slot: Slot,
pub index: u64,
// LMD GHOST vote
pub beacon_block_root: Hash256,
// FFG Vote
pub source: Checkpoint,
pub target: Checkpoint,
}
impl SignedRoot for AttestationData {}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(AttestationData);
}

View File

@@ -0,0 +1,15 @@
use crate::*;
use serde_derive::{Deserialize, Serialize};
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize)]
pub struct AttestationDuty {
/// The slot during which the attester must attest.
pub slot: Slot,
/// The index of this committee within the committees in `slot`.
pub index: CommitteeIndex,
/// The position of the attester within the committee.
pub committee_position: usize,
/// The total number of attesters in the committee.
pub committee_len: usize,
}

View File

@@ -0,0 +1,25 @@
use crate::{test_utils::TestRandom, EthSpec, IndexedAttestation};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// Two conflicting attestations.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct AttesterSlashing<T: EthSpec> {
pub attestation_1: IndexedAttestation<T>,
pub attestation_2: IndexedAttestation<T>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
ssz_and_tree_hash_tests!(AttesterSlashing<MainnetEthSpec>);
}

View File

@@ -0,0 +1,216 @@
use crate::test_utils::TestRandom;
use crate::*;
use bls::Signature;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash::TreeHash;
use tree_hash_derive::TreeHash;
/// A block of the `BeaconChain`.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct BeaconBlock<T: EthSpec> {
pub slot: Slot,
pub proposer_index: u64,
pub parent_root: Hash256,
pub state_root: Hash256,
pub body: BeaconBlockBody<T>,
}
impl<T: EthSpec> SignedRoot for BeaconBlock<T> {}
impl<T: EthSpec> BeaconBlock<T> {
/// Returns an empty block to be used during genesis.
///
/// Spec v0.11.1
pub fn empty(spec: &ChainSpec) -> Self {
BeaconBlock {
slot: spec.genesis_slot,
proposer_index: 0,
parent_root: Hash256::zero(),
state_root: Hash256::zero(),
body: BeaconBlockBody {
randao_reveal: Signature::empty_signature(),
eth1_data: Eth1Data {
deposit_root: Hash256::zero(),
block_hash: Hash256::zero(),
deposit_count: 0,
},
graffiti: [0; 32],
proposer_slashings: VariableList::empty(),
attester_slashings: VariableList::empty(),
attestations: VariableList::empty(),
deposits: VariableList::empty(),
voluntary_exits: VariableList::empty(),
},
}
}
/// Return a block where the block has the max possible operations.
pub fn full(spec: &ChainSpec) -> BeaconBlock<T> {
let header = BeaconBlockHeader {
slot: Slot::new(1),
proposer_index: 0,
parent_root: Hash256::zero(),
state_root: Hash256::zero(),
body_root: Hash256::zero(),
};
let signed_header = SignedBeaconBlockHeader {
message: header,
signature: Signature::empty_signature(),
};
let indexed_attestation: IndexedAttestation<T> = IndexedAttestation {
attesting_indices: VariableList::new(vec![
0 as u64;
T::MaxValidatorsPerCommittee::to_usize()
])
.unwrap(),
data: AttestationData::default(),
signature: AggregateSignature::new(),
};
let deposit_data = DepositData {
pubkey: PublicKeyBytes::empty(),
withdrawal_credentials: Hash256::zero(),
amount: 0,
signature: SignatureBytes::empty(),
};
let proposer_slashing = ProposerSlashing {
signed_header_1: signed_header.clone(),
signed_header_2: signed_header.clone(),
};
let attester_slashing = AttesterSlashing {
attestation_1: indexed_attestation.clone(),
attestation_2: indexed_attestation.clone(),
};
let attestation: Attestation<T> = Attestation {
aggregation_bits: BitList::with_capacity(T::MaxValidatorsPerCommittee::to_usize())
.unwrap(),
data: AttestationData::default(),
signature: AggregateSignature::new(),
};
let deposit = Deposit {
proof: FixedVector::from_elem(Hash256::zero()),
data: deposit_data,
};
let voluntary_exit = VoluntaryExit {
epoch: Epoch::new(1),
validator_index: 1,
};
let signed_voluntary_exit = SignedVoluntaryExit {
message: voluntary_exit,
signature: Signature::empty_signature(),
};
let mut block: BeaconBlock<T> = BeaconBlock::empty(spec);
for _ in 0..T::MaxProposerSlashings::to_usize() {
block
.body
.proposer_slashings
.push(proposer_slashing.clone())
.unwrap();
}
for _ in 0..T::MaxDeposits::to_usize() {
block.body.deposits.push(deposit.clone()).unwrap();
}
for _ in 0..T::MaxVoluntaryExits::to_usize() {
block
.body
.voluntary_exits
.push(signed_voluntary_exit.clone())
.unwrap();
}
for _ in 0..T::MaxAttesterSlashings::to_usize() {
block
.body
.attester_slashings
.push(attester_slashing.clone())
.unwrap();
}
for _ in 0..T::MaxAttestations::to_usize() {
block.body.attestations.push(attestation.clone()).unwrap();
}
block
}
/// Returns the epoch corresponding to `self.slot`.
pub fn epoch(&self) -> Epoch {
self.slot.epoch(T::slots_per_epoch())
}
/// Returns the `tree_hash_root` of the block.
///
/// Spec v0.11.1
pub fn canonical_root(&self) -> Hash256 {
Hash256::from_slice(&self.tree_hash_root()[..])
}
/// Returns a full `BeaconBlockHeader` of this block.
///
/// Note: This method is used instead of an `Into` impl to avoid a `Clone` of an entire block
/// when you want to have the block _and_ the header.
///
/// Note: performs a full tree-hash of `self.body`.
///
/// Spec v0.11.1
pub fn block_header(&self) -> BeaconBlockHeader {
BeaconBlockHeader {
slot: self.slot,
proposer_index: self.proposer_index,
parent_root: self.parent_root,
state_root: self.state_root,
body_root: Hash256::from_slice(&self.body.tree_hash_root()[..]),
}
}
/// Returns a "temporary" header, where the `state_root` is `Hash256::zero()`.
///
/// Spec v0.11.1
pub fn temporary_block_header(&self) -> BeaconBlockHeader {
BeaconBlockHeader {
state_root: Hash256::zero(),
..self.block_header()
}
}
/// Signs `self`, producing a `SignedBeaconBlock`.
pub fn sign(
self,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> SignedBeaconBlock<T> {
let domain = spec.get_domain(
self.epoch(),
Domain::BeaconProposer,
fork,
genesis_validators_root,
);
let message = self.signing_root(domain);
let signature = Signature::new(message.as_bytes(), secret_key);
SignedBeaconBlock {
message: self,
signature,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(BeaconBlock<MainnetEthSpec>);
}

View File

@@ -0,0 +1,37 @@
use crate::test_utils::TestRandom;
use crate::utils::{graffiti_from_hex_str, graffiti_to_hex_str};
use crate::*;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use ssz_types::VariableList;
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// The body of a `BeaconChain` block, containing operations.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct BeaconBlockBody<T: EthSpec> {
pub randao_reveal: Signature,
pub eth1_data: Eth1Data,
#[serde(
serialize_with = "graffiti_to_hex_str",
deserialize_with = "graffiti_from_hex_str"
)]
pub graffiti: [u8; 32],
pub proposer_slashings: VariableList<ProposerSlashing, T::MaxProposerSlashings>,
pub attester_slashings: VariableList<AttesterSlashing<T>, T::MaxAttesterSlashings>,
pub attestations: VariableList<Attestation<T>, T::MaxAttestations>,
pub deposits: VariableList<Deposit, T::MaxDeposits>,
pub voluntary_exits: VariableList<SignedVoluntaryExit, T::MaxVoluntaryExits>,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(BeaconBlockBody<MainnetEthSpec>);
}

View File

@@ -0,0 +1,70 @@
use crate::test_utils::TestRandom;
use crate::*;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash::TreeHash;
use tree_hash_derive::TreeHash;
/// A header of a `BeaconBlock`.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct BeaconBlockHeader {
pub slot: Slot,
pub proposer_index: u64,
pub parent_root: Hash256,
pub state_root: Hash256,
pub body_root: Hash256,
}
impl SignedRoot for BeaconBlockHeader {}
impl BeaconBlockHeader {
/// Returns the `tree_hash_root` of the header.
///
/// Spec v0.11.1
pub fn canonical_root(&self) -> Hash256 {
Hash256::from_slice(&self.tree_hash_root()[..])
}
/// Given a `body`, consumes `self` and returns a complete `BeaconBlock`.
///
/// Spec v0.11.1
pub fn into_block<T: EthSpec>(self, body: BeaconBlockBody<T>) -> BeaconBlock<T> {
BeaconBlock {
slot: self.slot,
proposer_index: self.proposer_index,
parent_root: self.parent_root,
state_root: self.state_root,
body,
}
}
/// Signs `self`, producing a `SignedBeaconBlockHeader`.
pub fn sign<E: EthSpec>(
self,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> SignedBeaconBlockHeader {
let epoch = self.slot.epoch(E::slots_per_epoch());
let domain = spec.get_domain(epoch, Domain::BeaconProposer, fork, genesis_validators_root);
let message = self.signing_root(domain);
let signature = Signature::new(message.as_bytes(), secret_key);
SignedBeaconBlockHeader {
message: self,
signature,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(BeaconBlockHeader);
}

View File

@@ -0,0 +1,26 @@
use crate::*;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct BeaconCommittee<'a> {
pub slot: Slot,
pub index: CommitteeIndex,
pub committee: &'a [usize],
}
impl<'a> BeaconCommittee<'a> {
pub fn into_owned(self) -> OwnedBeaconCommittee {
OwnedBeaconCommittee {
slot: self.slot,
index: self.index,
committee: self.committee.to_vec(),
}
}
}
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Default, Clone, Debug, PartialEq)]
pub struct OwnedBeaconCommittee {
pub slot: Slot,
pub index: CommitteeIndex,
pub committee: Vec<usize>,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
/// Configuration struct for controlling which caches of a `BeaconState` should be cloned.
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub struct CloneConfig {
pub committee_caches: bool,
pub pubkey_cache: bool,
pub exit_cache: bool,
pub tree_hash_cache: bool,
}
impl CloneConfig {
pub fn all() -> Self {
Self {
committee_caches: true,
pubkey_cache: true,
exit_cache: true,
tree_hash_cache: true,
}
}
pub fn none() -> Self {
Self::default()
}
pub fn committee_caches_only() -> Self {
Self {
committee_caches: true,
..Self::none()
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn sanity() {
assert!(CloneConfig::all().pubkey_cache);
assert!(!CloneConfig::none().tree_hash_cache);
assert!(CloneConfig::committee_caches_only().committee_caches);
assert!(!CloneConfig::committee_caches_only().exit_cache);
}
}

View File

@@ -0,0 +1,286 @@
#![allow(clippy::integer_arithmetic)]
use super::BeaconState;
use crate::*;
use core::num::NonZeroUsize;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use std::ops::Range;
use swap_or_not_shuffle::shuffle_list;
mod tests;
/// Computes and stores the shuffling for an epoch. Provides various getters to allow callers to
/// read the committees for the given epoch.
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct CommitteeCache {
initialized_epoch: Option<Epoch>,
shuffling: Vec<usize>,
shuffling_positions: Vec<Option<NonZeroUsize>>,
committees_per_slot: u64,
slots_per_epoch: u64,
}
impl CommitteeCache {
/// Return a new, fully initialized cache.
///
/// Spec v0.11.1
pub fn initialized<T: EthSpec>(
state: &BeaconState<T>,
epoch: Epoch,
spec: &ChainSpec,
) -> Result<CommitteeCache, Error> {
RelativeEpoch::from_epoch(state.current_epoch(), epoch)
.map_err(|_| Error::EpochOutOfBounds)?;
// May cause divide-by-zero errors.
if T::slots_per_epoch() == 0 {
return Err(Error::ZeroSlotsPerEpoch);
}
let active_validator_indices = get_active_validator_indices(&state.validators, epoch);
if active_validator_indices.is_empty() {
return Err(Error::InsufficientValidators);
}
let committees_per_slot =
T::get_committee_count_per_slot(active_validator_indices.len(), spec)? as u64;
let seed = state.get_seed(epoch, Domain::BeaconAttester, spec)?;
let shuffling = shuffle_list(
active_validator_indices,
spec.shuffle_round_count,
&seed[..],
false,
)
.ok_or_else(|| Error::UnableToShuffle)?;
// The use of `NonZeroUsize` reduces the maximum number of possible validators by one.
if state.validators.len() == usize::max_value() {
return Err(Error::TooManyValidators);
}
let mut shuffling_positions = vec![None; state.validators.len()];
for (i, v) in shuffling.iter().enumerate() {
shuffling_positions[*v] = NonZeroUsize::new(i + 1);
}
Ok(CommitteeCache {
initialized_epoch: Some(epoch),
shuffling,
shuffling_positions,
committees_per_slot,
slots_per_epoch: T::slots_per_epoch(),
})
}
/// Returns `true` if the cache has been initialized at the supplied `epoch`.
///
/// An non-initialized cache does not provide any useful information.
pub fn is_initialized_at(&self, epoch: Epoch) -> bool {
Some(epoch) == self.initialized_epoch
}
/// Returns the **shuffled** list of active validator indices for the initialized epoch.
///
/// These indices are not in ascending order.
///
/// Always returns `&[]` for a non-initialized epoch.
///
/// Spec v0.11.1
pub fn active_validator_indices(&self) -> &[usize] {
&self.shuffling
}
/// Returns the shuffled list of active validator indices for the initialized epoch.
///
/// Always returns `&[]` for a non-initialized epoch.
///
/// Spec v0.11.1
pub fn shuffling(&self) -> &[usize] {
&self.shuffling
}
/// Get the Beacon committee for the given `slot` and `index`.
///
/// Return `None` if the cache is uninitialized, or the `slot` or `index` is out of range.
pub fn get_beacon_committee(
&self,
slot: Slot,
index: CommitteeIndex,
) -> Option<BeaconCommittee> {
if self.initialized_epoch.is_none()
|| !self.is_initialized_at(slot.epoch(self.slots_per_epoch))
|| index >= self.committees_per_slot
{
return None;
}
let committee_index =
(slot.as_u64() % self.slots_per_epoch) * self.committees_per_slot + index;
let committee = self.compute_committee(committee_index as usize)?;
Some(BeaconCommittee {
slot,
index,
committee,
})
}
/// Get all the Beacon committees at a given `slot`.
pub fn get_beacon_committees_at_slot(&self, slot: Slot) -> Result<Vec<BeaconCommittee>, Error> {
if self.initialized_epoch.is_none() {
return Err(Error::CommitteeCacheUninitialized(None));
}
(0..self.committees_per_slot())
.map(|index| {
self.get_beacon_committee(slot, index)
.ok_or(Error::NoCommittee { slot, index })
})
.collect()
}
/// Returns all committees for `self.initialized_epoch`.
pub fn get_all_beacon_committees(&self) -> Result<Vec<BeaconCommittee>, Error> {
let initialized_epoch = self
.initialized_epoch
.ok_or_else(|| Error::CommitteeCacheUninitialized(None))?;
initialized_epoch.slot_iter(self.slots_per_epoch).try_fold(
Vec::with_capacity(self.slots_per_epoch as usize),
|mut vec, slot| {
vec.append(&mut self.get_beacon_committees_at_slot(slot)?);
Ok(vec)
},
)
}
/// Returns the `AttestationDuty` for the given `validator_index`.
///
/// Returns `None` if the `validator_index` does not exist, does not have duties or `Self` is
/// non-initialized.
pub fn get_attestation_duties(&self, validator_index: usize) -> Option<AttestationDuty> {
let i = self.shuffled_position(validator_index)?;
(0..self.epoch_committee_count())
.map(|nth_committee| (nth_committee, self.compute_committee_range(nth_committee)))
.find(|(_, range)| {
if let Some(range) = range {
range.start <= i && range.end > i
} else {
false
}
})
.and_then(|(nth_committee, range)| {
let (slot, index) = self.convert_to_slot_and_index(nth_committee as u64)?;
let range = range?;
let committee_position = i - range.start;
let committee_len = range.end - range.start;
Some(AttestationDuty {
slot,
index,
committee_position,
committee_len,
})
})
}
/// Convert an index addressing the list of all epoch committees into a slot and per-slot index.
fn convert_to_slot_and_index(
&self,
global_committee_index: u64,
) -> Option<(Slot, CommitteeIndex)> {
let epoch_start_slot = self.initialized_epoch?.start_slot(self.slots_per_epoch);
let slot_offset = global_committee_index / self.committees_per_slot;
let index = global_committee_index % self.committees_per_slot;
Some((epoch_start_slot + slot_offset, index))
}
/// Returns the number of active validators in the initialized epoch.
///
/// Always returns `usize::default()` for a non-initialized epoch.
///
/// Spec v0.11.1
pub fn active_validator_count(&self) -> usize {
self.shuffling.len()
}
/// Returns the total number of committees in the initialized epoch.
///
/// Always returns `usize::default()` for a non-initialized epoch.
///
/// Spec v0.11.1
pub fn epoch_committee_count(&self) -> usize {
self.committees_per_slot as usize * self.slots_per_epoch as usize
}
/// Returns the number of committees per slot for this cache's epoch.
pub fn committees_per_slot(&self) -> u64 {
self.committees_per_slot
}
/// Returns a slice of `self.shuffling` that represents the `index`'th committee in the epoch.
///
/// Spec v0.11.1
fn compute_committee(&self, index: usize) -> Option<&[usize]> {
Some(&self.shuffling[self.compute_committee_range(index)?])
}
/// Returns a range of `self.shuffling` that represents the `index`'th committee in the epoch.
///
/// To avoid a divide-by-zero, returns `None` if `self.committee_count` is zero.
///
/// Will also return `None` if the index is out of bounds.
///
/// Spec v0.11.1
fn compute_committee_range(&self, index: usize) -> Option<Range<usize>> {
let count = self.epoch_committee_count();
if count == 0 || index >= count {
return None;
}
let num_validators = self.shuffling.len();
let start = (num_validators * index) / count;
let end = (num_validators * (index + 1)) / count;
Some(start..end)
}
/// Returns the index of some validator in `self.shuffling`.
///
/// Always returns `None` for a non-initialized epoch.
fn shuffled_position(&self, validator_index: usize) -> Option<usize> {
self.shuffling_positions
.get(validator_index)?
.and_then(|p| Some(p.get() - 1))
}
}
/// Returns a list of all `validators` indices where the validator is active at the given
/// `epoch`.
///
/// Spec v0.11.1
pub fn get_active_validator_indices(validators: &[Validator], epoch: Epoch) -> Vec<usize> {
let mut active = Vec::with_capacity(validators.len());
for (index, validator) in validators.iter().enumerate() {
if validator.is_active_at(epoch) {
active.push(index)
}
}
active.shrink_to_fit();
active
}
#[cfg(feature = "arbitrary-fuzz")]
impl arbitrary::Arbitrary for CommitteeCache {
fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self::default())
}
}

View File

@@ -0,0 +1,120 @@
#![cfg(test)]
use super::*;
use crate::{test_utils::*, *};
#[test]
fn default_values() {
let cache = CommitteeCache::default();
assert_eq!(cache.is_initialized_at(Epoch::new(0)), false);
assert!(&cache.active_validator_indices().is_empty());
assert_eq!(cache.get_beacon_committee(Slot::new(0), 0), None);
assert_eq!(cache.get_attestation_duties(0), None);
assert_eq!(cache.active_validator_count(), 0);
assert_eq!(cache.epoch_committee_count(), 0);
assert!(cache.get_beacon_committees_at_slot(Slot::new(0)).is_err());
}
fn new_state<T: EthSpec>(validator_count: usize, slot: Slot) -> BeaconState<T> {
let spec = &T::default_spec();
let mut builder =
TestingBeaconStateBuilder::from_single_keypair(validator_count, &Keypair::random(), spec);
builder.teleport_to_slot(slot);
let (state, _keypairs) = builder.build();
state
}
#[test]
fn fails_without_validators() {
let state = new_state::<MinimalEthSpec>(0, Slot::new(0));
let spec = &MinimalEthSpec::default_spec();
assert_eq!(
CommitteeCache::initialized(&state, state.current_epoch(), &spec),
Err(BeaconStateError::InsufficientValidators)
);
}
#[test]
fn initializes_with_the_right_epoch() {
let state = new_state::<MinimalEthSpec>(16, Slot::new(0));
let spec = &MinimalEthSpec::default_spec();
let cache = CommitteeCache::default();
assert_eq!(cache.initialized_epoch, None);
let cache = CommitteeCache::initialized(&state, state.current_epoch(), &spec).unwrap();
assert_eq!(cache.initialized_epoch, Some(state.current_epoch()));
let cache = CommitteeCache::initialized(&state, state.previous_epoch(), &spec).unwrap();
assert_eq!(cache.initialized_epoch, Some(state.previous_epoch()));
let cache = CommitteeCache::initialized(&state, state.next_epoch(), &spec).unwrap();
assert_eq!(cache.initialized_epoch, Some(state.next_epoch()));
}
#[test]
fn shuffles_for_the_right_epoch() {
use crate::EthSpec;
let num_validators = MinimalEthSpec::minimum_validator_count() * 2;
let epoch = Epoch::new(100_000_000);
let slot = epoch.start_slot(MinimalEthSpec::slots_per_epoch());
let mut state = new_state::<MinimalEthSpec>(num_validators, slot);
let spec = &MinimalEthSpec::default_spec();
let distinct_hashes: Vec<Hash256> = (0..MinimalEthSpec::epochs_per_historical_vector())
.map(|i| Hash256::from_low_u64_be(i as u64))
.collect();
state.randao_mixes = FixedVector::from(distinct_hashes);
let previous_seed = state
.get_seed(state.previous_epoch(), Domain::BeaconAttester, spec)
.unwrap();
let current_seed = state
.get_seed(state.current_epoch(), Domain::BeaconAttester, spec)
.unwrap();
let next_seed = state
.get_seed(state.next_epoch(), Domain::BeaconAttester, spec)
.unwrap();
assert!((previous_seed != current_seed) && (current_seed != next_seed));
let shuffling_with_seed = |seed: Hash256| {
shuffle_list(
(0..num_validators).collect(),
spec.shuffle_round_count,
&seed[..],
false,
)
.unwrap()
};
let assert_shuffling_positions_accurate = |cache: &CommitteeCache| {
for (i, v) in cache.shuffling.iter().enumerate() {
assert_eq!(
cache.shuffling_positions[*v].unwrap().get() - 1,
i,
"Shuffling position inaccurate"
);
}
};
let cache = CommitteeCache::initialized(&state, state.current_epoch(), spec).unwrap();
assert_eq!(cache.shuffling, shuffling_with_seed(current_seed));
assert_shuffling_positions_accurate(&cache);
let cache = CommitteeCache::initialized(&state, state.previous_epoch(), spec).unwrap();
assert_eq!(cache.shuffling, shuffling_with_seed(previous_seed));
assert_shuffling_positions_accurate(&cache);
let cache = CommitteeCache::initialized(&state, state.next_epoch(), spec).unwrap();
assert_eq!(cache.shuffling, shuffling_with_seed(next_seed));
assert_shuffling_positions_accurate(&cache);
}

View File

@@ -0,0 +1,73 @@
use super::{BeaconStateError, ChainSpec, Epoch, Validator};
use safe_arith::SafeArith;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
/// Map from exit epoch to the number of validators with that exit epoch.
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExitCache {
initialized: bool,
exit_epoch_counts: HashMap<Epoch, u64>,
}
impl ExitCache {
/// Build the cache if not initialized.
pub fn build(
&mut self,
validators: &[Validator],
spec: &ChainSpec,
) -> Result<(), BeaconStateError> {
if self.initialized {
return Ok(());
}
self.initialized = true;
// Add all validators with a non-default exit epoch to the cache.
validators
.iter()
.filter(|validator| validator.exit_epoch != spec.far_future_epoch)
.try_for_each(|validator| self.record_validator_exit(validator.exit_epoch))
}
/// Check that the cache is initialized and return an error if it is not.
pub fn check_initialized(&self) -> Result<(), BeaconStateError> {
if self.initialized {
Ok(())
} else {
Err(BeaconStateError::ExitCacheUninitialized)
}
}
/// Record the exit epoch of a validator. Must be called only once per exiting validator.
pub fn record_validator_exit(&mut self, exit_epoch: Epoch) -> Result<(), BeaconStateError> {
self.check_initialized()?;
self.exit_epoch_counts
.entry(exit_epoch)
.or_insert(0)
.increment()?;
Ok(())
}
/// Get the largest exit epoch with a non-zero exit epoch count.
pub fn max_epoch(&self) -> Result<Option<Epoch>, BeaconStateError> {
self.check_initialized()?;
Ok(self.exit_epoch_counts.keys().max().cloned())
}
/// Get number of validators with the given exit epoch. (Return 0 for the default exit epoch.)
pub fn get_churn_at(&self, exit_epoch: Epoch) -> Result<u64, BeaconStateError> {
self.check_initialized()?;
Ok(self
.exit_epoch_counts
.get(&exit_epoch)
.cloned()
.unwrap_or(0))
}
}
#[cfg(feature = "arbitrary-fuzz")]
impl arbitrary::Arbitrary for ExitCache {
fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self::default())
}
}

View File

@@ -0,0 +1,48 @@
use crate::*;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
type ValidatorIndex = usize;
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
pub struct PubkeyCache {
/// Maintain the number of keys added to the map. It is not sufficient to just use the HashMap
/// len, as it does not increase when duplicate keys are added. Duplicate keys are used during
/// testing.
len: usize,
map: HashMap<PublicKeyBytes, ValidatorIndex>,
}
impl PubkeyCache {
/// Returns the number of validator indices added to the map so far.
pub fn len(&self) -> ValidatorIndex {
self.len
}
/// Inserts a validator index into the map.
///
/// The added index must equal the number of validators already added to the map. This ensures
/// that an index is never skipped.
#[allow(clippy::integer_arithmetic)]
pub fn insert(&mut self, pubkey: PublicKeyBytes, index: ValidatorIndex) -> bool {
if index == self.len {
self.map.insert(pubkey, index);
self.len += 1;
true
} else {
false
}
}
/// Looks up a validator index's by their public key.
pub fn get(&self, pubkey: &PublicKeyBytes) -> Option<ValidatorIndex> {
self.map.get(pubkey).copied()
}
}
#[cfg(feature = "arbitrary-fuzz")]
impl arbitrary::Arbitrary for PubkeyCache {
fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self::default())
}
}

View File

@@ -0,0 +1,408 @@
#![cfg(test)]
use super::*;
use crate::test_utils::*;
ssz_and_tree_hash_tests!(FoundationBeaconState);
fn test_beacon_proposer_index<T: EthSpec>() {
let spec = T::default_spec();
let relative_epoch = RelativeEpoch::Current;
// Build a state for testing.
let build_state = |validator_count: usize| -> BeaconState<T> {
let builder: TestingBeaconStateBuilder<T> =
TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(validator_count, &spec);
let (mut state, _keypairs) = builder.build();
state.build_committee_cache(relative_epoch, &spec).unwrap();
state
};
// Get the i'th candidate proposer for the given state and slot
let ith_candidate = |state: &BeaconState<T>, slot: Slot, i: usize| {
let epoch = slot.epoch(T::slots_per_epoch());
let seed = state.get_beacon_proposer_seed(slot, &spec).unwrap();
let active_validators = state.get_active_validator_indices(epoch);
active_validators[compute_shuffled_index(
i,
active_validators.len(),
&seed,
spec.shuffle_round_count,
)
.unwrap()]
};
// Run a test on the state.
let test = |state: &BeaconState<T>, slot: Slot, candidate_index: usize| {
assert_eq!(
state.get_beacon_proposer_index(slot, &spec),
Ok(ith_candidate(state, slot, candidate_index))
);
};
// Test where we have one validator per slot.
// 0th candidate should be chosen every time.
let state = build_state(T::slots_per_epoch() as usize);
for i in 0..T::slots_per_epoch() {
test(&state, Slot::from(i), 0);
}
// Test where we have two validators per slot.
// 0th candidate should be chosen every time.
let state = build_state(T::slots_per_epoch() as usize * 2);
for i in 0..T::slots_per_epoch() {
test(&state, Slot::from(i), 0);
}
// Test with two validators per slot, first validator has zero balance.
let mut state = build_state(T::slots_per_epoch() as usize * 2);
let slot0_candidate0 = ith_candidate(&state, Slot::new(0), 0);
state.validators[slot0_candidate0].effective_balance = 0;
test(&state, Slot::new(0), 1);
for i in 1..T::slots_per_epoch() {
test(&state, Slot::from(i), 0);
}
}
#[test]
fn beacon_proposer_index() {
test_beacon_proposer_index::<MinimalEthSpec>();
}
/// Test that
///
/// 1. Using the cache before it's built fails.
/// 2. Using the cache after it's build passes.
/// 3. Using the cache after it's dropped fails.
fn test_cache_initialization<'a, T: EthSpec>(
state: &'a mut BeaconState<T>,
relative_epoch: RelativeEpoch,
spec: &ChainSpec,
) {
let slot = relative_epoch
.into_epoch(state.slot.epoch(T::slots_per_epoch()))
.start_slot(T::slots_per_epoch());
// Assuming the cache isn't already built, assert that a call to a cache-using function fails.
assert_eq!(
state.get_attestation_duties(0, relative_epoch),
Err(BeaconStateError::CommitteeCacheUninitialized(Some(
relative_epoch
)))
);
// Build the cache.
state.build_committee_cache(relative_epoch, spec).unwrap();
// Assert a call to a cache-using function passes.
let _ = state.get_beacon_proposer_index(slot, spec).unwrap();
// Drop the cache.
state.drop_committee_cache(relative_epoch);
// Assert a call to a cache-using function fail.
assert_eq!(
state.get_beacon_committee(slot, 0),
Err(BeaconStateError::CommitteeCacheUninitialized(Some(
relative_epoch
)))
);
}
#[test]
fn cache_initialization() {
let spec = MinimalEthSpec::default_spec();
let builder: TestingBeaconStateBuilder<MinimalEthSpec> =
TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(16, &spec);
let (mut state, _keypairs) = builder.build();
state.slot =
(MinimalEthSpec::genesis_epoch() + 1).start_slot(MinimalEthSpec::slots_per_epoch());
test_cache_initialization(&mut state, RelativeEpoch::Previous, &spec);
test_cache_initialization(&mut state, RelativeEpoch::Current, &spec);
test_cache_initialization(&mut state, RelativeEpoch::Next, &spec);
}
fn test_clone_config<E: EthSpec>(base_state: &BeaconState<E>, clone_config: CloneConfig) {
let state = base_state.clone_with(clone_config.clone());
if clone_config.committee_caches {
state
.committee_cache(RelativeEpoch::Previous)
.expect("committee cache exists");
state
.committee_cache(RelativeEpoch::Current)
.expect("committee cache exists");
state
.committee_cache(RelativeEpoch::Next)
.expect("committee cache exists");
} else {
state
.committee_cache(RelativeEpoch::Previous)
.expect_err("shouldn't exist");
state
.committee_cache(RelativeEpoch::Current)
.expect_err("shouldn't exist");
state
.committee_cache(RelativeEpoch::Next)
.expect_err("shouldn't exist");
}
if clone_config.pubkey_cache {
assert_ne!(state.pubkey_cache.len(), 0);
} else {
assert_eq!(state.pubkey_cache.len(), 0);
}
if clone_config.exit_cache {
state
.exit_cache
.check_initialized()
.expect("exit cache exists");
} else {
state
.exit_cache
.check_initialized()
.expect_err("exit cache doesn't exist");
}
if clone_config.tree_hash_cache {
assert!(state.tree_hash_cache.is_some());
} else {
assert!(state.tree_hash_cache.is_none(), "{:?}", clone_config);
}
}
#[test]
fn clone_config() {
let spec = MinimalEthSpec::default_spec();
let builder: TestingBeaconStateBuilder<MinimalEthSpec> =
TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(16, &spec);
let (mut state, _keypairs) = builder.build();
state.build_all_caches(&spec).unwrap();
let num_caches = 4;
let all_configs = (0..2u8.pow(num_caches)).map(|i| CloneConfig {
committee_caches: (i & 1) != 0,
pubkey_cache: ((i >> 1) & 1) != 0,
exit_cache: ((i >> 2) & 1) != 0,
tree_hash_cache: ((i >> 3) & 1) != 0,
});
for config in all_configs {
test_clone_config(&state, config);
}
}
#[test]
fn tree_hash_cache() {
use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
use tree_hash::TreeHash;
let mut rng = XorShiftRng::from_seed([42; 16]);
let mut state: FoundationBeaconState = BeaconState::random_for_test(&mut rng);
let root = state.update_tree_hash_cache().unwrap();
assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]);
state.slot += 1;
let root = state.update_tree_hash_cache().unwrap();
assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]);
}
/// Tests committee-specific components
#[cfg(test)]
mod committees {
use super::*;
use crate::beacon_state::MinimalEthSpec;
use swap_or_not_shuffle::shuffle_list;
fn execute_committee_consistency_test<T: EthSpec>(
state: BeaconState<T>,
epoch: Epoch,
validator_count: usize,
spec: &ChainSpec,
) {
let active_indices: Vec<usize> = (0..validator_count).collect();
let seed = state.get_seed(epoch, Domain::BeaconAttester, spec).unwrap();
let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), epoch).unwrap();
let mut ordered_indices = state
.get_cached_active_validator_indices(relative_epoch)
.unwrap()
.to_vec();
ordered_indices.sort_unstable();
assert_eq!(
active_indices, ordered_indices,
"Validator indices mismatch"
);
let shuffling =
shuffle_list(active_indices, spec.shuffle_round_count, &seed[..], false).unwrap();
let mut expected_indices_iter = shuffling.iter();
// Loop through all slots in the epoch being tested.
for slot in epoch.slot_iter(T::slots_per_epoch()) {
let beacon_committees = state.get_beacon_committees_at_slot(slot).unwrap();
// Assert that the number of committees in this slot is consistent with the reported number
// of committees in an epoch.
assert_eq!(
beacon_committees.len() as u64,
state.get_epoch_committee_count(relative_epoch).unwrap() / T::slots_per_epoch()
);
for (committee_index, bc) in beacon_committees.iter().enumerate() {
// Assert that indices are assigned sequentially across committees.
assert_eq!(committee_index as u64, bc.index);
// Assert that a committee lookup via slot is identical to a committee lookup via
// index.
assert_eq!(state.get_beacon_committee(bc.slot, bc.index).unwrap(), *bc);
// Loop through each validator in the committee.
for (committee_i, validator_i) in bc.committee.iter().enumerate() {
// Assert the validators are assigned contiguously across committees.
assert_eq!(
*validator_i,
*expected_indices_iter.next().unwrap(),
"Non-sequential validators."
);
// Assert a call to `get_attestation_duties` is consistent with a call to
// `get_beacon_committees_at_slot`
let attestation_duty = state
.get_attestation_duties(*validator_i, relative_epoch)
.unwrap()
.unwrap();
assert_eq!(attestation_duty.slot, slot);
assert_eq!(attestation_duty.index, bc.index);
assert_eq!(attestation_duty.committee_position, committee_i);
assert_eq!(attestation_duty.committee_len, bc.committee.len());
}
}
}
// Assert that all validators were assigned to a committee.
assert!(expected_indices_iter.next().is_none());
}
fn committee_consistency_test<T: EthSpec>(
validator_count: usize,
state_epoch: Epoch,
cache_epoch: RelativeEpoch,
) {
let spec = &T::default_spec();
let mut builder = TestingBeaconStateBuilder::from_single_keypair(
validator_count,
&Keypair::random(),
spec,
);
let slot = state_epoch.start_slot(T::slots_per_epoch());
builder.teleport_to_slot(slot);
let (mut state, _keypairs): (BeaconState<T>, _) = builder.build();
let distinct_hashes: Vec<Hash256> = (0..T::epochs_per_historical_vector())
.map(|i| Hash256::from_low_u64_be(i as u64))
.collect();
state.randao_mixes = FixedVector::from(distinct_hashes);
state
.build_committee_cache(RelativeEpoch::Previous, spec)
.unwrap();
state
.build_committee_cache(RelativeEpoch::Current, spec)
.unwrap();
state
.build_committee_cache(RelativeEpoch::Next, spec)
.unwrap();
let cache_epoch = cache_epoch.into_epoch(state_epoch);
execute_committee_consistency_test(state, cache_epoch, validator_count as usize, &spec);
}
fn committee_consistency_test_suite<T: EthSpec>(cached_epoch: RelativeEpoch) {
let spec = T::default_spec();
let validator_count = spec.max_committees_per_slot
* T::slots_per_epoch() as usize
* spec.target_committee_size
+ 1;
committee_consistency_test::<T>(validator_count as usize, Epoch::new(0), cached_epoch);
committee_consistency_test::<T>(
validator_count as usize,
T::genesis_epoch() + 4,
cached_epoch,
);
committee_consistency_test::<T>(
validator_count as usize,
T::genesis_epoch() + T::slots_per_historical_root() as u64 * T::slots_per_epoch() * 4,
cached_epoch,
);
}
#[test]
fn current_epoch_committee_consistency() {
committee_consistency_test_suite::<MinimalEthSpec>(RelativeEpoch::Current);
}
#[test]
fn previous_epoch_committee_consistency() {
committee_consistency_test_suite::<MinimalEthSpec>(RelativeEpoch::Previous);
}
#[test]
fn next_epoch_committee_consistency() {
committee_consistency_test_suite::<MinimalEthSpec>(RelativeEpoch::Next);
}
}
mod get_outstanding_deposit_len {
use super::*;
use crate::test_utils::TestingBeaconStateBuilder;
use crate::MinimalEthSpec;
fn state() -> BeaconState<MinimalEthSpec> {
let spec = MinimalEthSpec::default_spec();
let builder: TestingBeaconStateBuilder<MinimalEthSpec> =
TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(16, &spec);
let (state, _keypairs) = builder.build();
state
}
#[test]
fn returns_ok() {
let mut state = state();
assert_eq!(state.get_outstanding_deposit_len(), Ok(0));
state.eth1_data.deposit_count = 17;
state.eth1_deposit_index = 16;
assert_eq!(state.get_outstanding_deposit_len(), Ok(1));
}
#[test]
fn returns_err_if_the_state_is_invalid() {
let mut state = state();
// The state is invalid, deposit count is lower than deposit index.
state.eth1_data.deposit_count = 16;
state.eth1_deposit_index = 17;
assert_eq!(
state.get_outstanding_deposit_len(),
Err(BeaconStateError::InvalidDepositState {
deposit_count: 16,
deposit_index: 17,
})
);
}
}

View File

@@ -0,0 +1,330 @@
#![allow(clippy::integer_arithmetic)]
use super::Error;
use crate::{BeaconState, EthSpec, Hash256, Unsigned, Validator};
use cached_tree_hash::{int_log, CacheArena, CachedTreeHash, TreeHashCache};
use rayon::prelude::*;
use ssz_derive::{Decode, Encode};
use tree_hash::{mix_in_length, MerkleHasher, TreeHash};
/// The number of fields on a beacon state.
const NUM_BEACON_STATE_HASHING_FIELDS: usize = 20;
/// The number of nodes in the Merkle tree of a validator record.
const NODES_PER_VALIDATOR: usize = 15;
/// The number of validator record tree hash caches stored in each arena.
///
/// This is primarily used for concurrency; if we have 16 validators and set `VALIDATORS_PER_ARENA
/// == 8` then it is possible to do a 2-core concurrent hash.
///
/// Do not set to 0.
const VALIDATORS_PER_ARENA: usize = 4_096;
/// A cache that performs a caching tree hash of the entire `BeaconState` struct.
#[derive(Debug, PartialEq, Clone, Default, Encode, Decode)]
pub struct BeaconTreeHashCache {
// Validators cache
validators: ValidatorsListTreeHashCache,
// Arenas
fixed_arena: CacheArena,
balances_arena: CacheArena,
slashings_arena: CacheArena,
// Caches
block_roots: TreeHashCache,
state_roots: TreeHashCache,
historical_roots: TreeHashCache,
balances: TreeHashCache,
randao_mixes: TreeHashCache,
slashings: TreeHashCache,
}
impl BeaconTreeHashCache {
/// Instantiates a new cache.
///
/// Allocates the necessary memory to store all of the cached Merkle trees but does perform any
/// hashing.
pub fn new<T: EthSpec>(state: &BeaconState<T>) -> Self {
let mut fixed_arena = CacheArena::default();
let block_roots = state.block_roots.new_tree_hash_cache(&mut fixed_arena);
let state_roots = state.state_roots.new_tree_hash_cache(&mut fixed_arena);
let historical_roots = state.historical_roots.new_tree_hash_cache(&mut fixed_arena);
let randao_mixes = state.randao_mixes.new_tree_hash_cache(&mut fixed_arena);
let validators = ValidatorsListTreeHashCache::new::<T>(&state.validators[..]);
let mut balances_arena = CacheArena::default();
let balances = state.balances.new_tree_hash_cache(&mut balances_arena);
let mut slashings_arena = CacheArena::default();
let slashings = state.slashings.new_tree_hash_cache(&mut slashings_arena);
Self {
validators,
fixed_arena,
balances_arena,
slashings_arena,
block_roots,
state_roots,
historical_roots,
balances,
randao_mixes,
slashings,
}
}
/// Updates the cache and returns the tree hash root for the given `state`.
///
/// The provided `state` should be a descendant of the last `state` given to this function, or
/// the `Self::new` function.
pub fn recalculate_tree_hash_root<T: EthSpec>(
&mut self,
state: &BeaconState<T>,
) -> Result<Hash256, Error> {
let mut hasher = MerkleHasher::with_leaves(NUM_BEACON_STATE_HASHING_FIELDS);
hasher.write(state.genesis_time.tree_hash_root().as_bytes())?;
hasher.write(state.genesis_validators_root.tree_hash_root().as_bytes())?;
hasher.write(state.slot.tree_hash_root().as_bytes())?;
hasher.write(state.fork.tree_hash_root().as_bytes())?;
hasher.write(state.latest_block_header.tree_hash_root().as_bytes())?;
hasher.write(
state
.block_roots
.recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.block_roots)?
.as_bytes(),
)?;
hasher.write(
state
.state_roots
.recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.state_roots)?
.as_bytes(),
)?;
hasher.write(
state
.historical_roots
.recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.historical_roots)?
.as_bytes(),
)?;
hasher.write(state.eth1_data.tree_hash_root().as_bytes())?;
hasher.write(state.eth1_data_votes.tree_hash_root().as_bytes())?;
hasher.write(state.eth1_deposit_index.tree_hash_root().as_bytes())?;
hasher.write(
self.validators
.recalculate_tree_hash_root(&state.validators[..])?
.as_bytes(),
)?;
hasher.write(
state
.balances
.recalculate_tree_hash_root(&mut self.balances_arena, &mut self.balances)?
.as_bytes(),
)?;
hasher.write(
state
.randao_mixes
.recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.randao_mixes)?
.as_bytes(),
)?;
hasher.write(
state
.slashings
.recalculate_tree_hash_root(&mut self.slashings_arena, &mut self.slashings)?
.as_bytes(),
)?;
hasher.write(
state
.previous_epoch_attestations
.tree_hash_root()
.as_bytes(),
)?;
hasher.write(state.current_epoch_attestations.tree_hash_root().as_bytes())?;
hasher.write(state.justification_bits.tree_hash_root().as_bytes())?;
hasher.write(
state
.previous_justified_checkpoint
.tree_hash_root()
.as_bytes(),
)?;
hasher.write(
state
.current_justified_checkpoint
.tree_hash_root()
.as_bytes(),
)?;
hasher.write(state.finalized_checkpoint.tree_hash_root().as_bytes())?;
hasher.finish().map_err(Into::into)
}
/// Updates the cache and provides the root of the given `validators`.
pub fn recalculate_validators_tree_hash_root(
&mut self,
validators: &[Validator],
) -> Result<Hash256, Error> {
self.validators.recalculate_tree_hash_root(validators)
}
}
/// A specialized cache for computing the tree hash root of `state.validators`.
#[derive(Debug, PartialEq, Clone, Default, Encode, Decode)]
struct ValidatorsListTreeHashCache {
list_arena: CacheArena,
list_cache: TreeHashCache,
values: ParallelValidatorTreeHash,
}
impl ValidatorsListTreeHashCache {
/// Instantiates a new cache.
///
/// Allocates the necessary memory to store all of the cached Merkle trees but does perform any
/// hashing.
fn new<E: EthSpec>(validators: &[Validator]) -> Self {
let mut list_arena = CacheArena::default();
Self {
list_cache: TreeHashCache::new(
&mut list_arena,
int_log(E::ValidatorRegistryLimit::to_usize()),
validators.len(),
),
list_arena,
values: ParallelValidatorTreeHash::new::<E>(validators),
}
}
/// Updates the cache and returns the tree hash root for the given `state`.
///
/// This function makes assumptions that the `validators` list will only change in accordance
/// with valid per-block/per-slot state transitions.
fn recalculate_tree_hash_root(&mut self, validators: &[Validator]) -> Result<Hash256, Error> {
let mut list_arena = std::mem::take(&mut self.list_arena);
let leaves = self
.values
.leaves(validators)?
.into_iter()
.flatten()
.map(|h| h.to_fixed_bytes())
.collect::<Vec<_>>();
let list_root = self
.list_cache
.recalculate_merkle_root(&mut list_arena, leaves.into_iter())?;
std::mem::replace(&mut self.list_arena, list_arena);
Ok(mix_in_length(&list_root, validators.len()))
}
}
/// Provides a cache for each of the `Validator` objects in `state.validators` and computes the
/// roots of these using Rayon parallelization.
#[derive(Debug, PartialEq, Clone, Default, Encode, Decode)]
pub struct ParallelValidatorTreeHash {
/// Each arena and its associated sub-trees.
arenas: Vec<(CacheArena, Vec<TreeHashCache>)>,
}
impl ParallelValidatorTreeHash {
/// Instantiates a new cache.
///
/// Allocates the necessary memory to store all of the cached Merkle trees but does perform any
/// hashing.
fn new<E: EthSpec>(validators: &[Validator]) -> Self {
let num_arenas = std::cmp::max(
1,
(validators.len() + VALIDATORS_PER_ARENA - 1) / VALIDATORS_PER_ARENA,
);
let mut arenas = (1..=num_arenas)
.map(|i| {
let num_validators = if i == num_arenas {
validators.len() % VALIDATORS_PER_ARENA
} else {
VALIDATORS_PER_ARENA
};
NODES_PER_VALIDATOR * num_validators
})
.map(|capacity| (CacheArena::with_capacity(capacity), vec![]))
.collect::<Vec<_>>();
validators.iter().enumerate().for_each(|(i, v)| {
let (arena, caches) = &mut arenas[i / VALIDATORS_PER_ARENA];
caches.push(v.new_tree_hash_cache(arena))
});
Self { arenas }
}
/// Returns the number of validators stored in self.
fn len(&self) -> usize {
self.arenas.last().map_or(0, |last| {
// Subtraction cannot underflow because `.last()` ensures the `.len() > 0`.
(self.arenas.len() - 1) * VALIDATORS_PER_ARENA + last.1.len()
})
}
/// Updates the caches for each `Validator` in `validators` and returns a list that maps 1:1
/// with `validators` to the hash of each validator.
///
/// This function makes assumptions that the `validators` list will only change in accordance
/// with valid per-block/per-slot state transitions.
fn leaves(&mut self, validators: &[Validator]) -> Result<Vec<Vec<Hash256>>, Error> {
if self.len() < validators.len() {
validators.iter().skip(self.len()).for_each(|v| {
if self
.arenas
.last()
.map_or(true, |last| last.1.len() >= VALIDATORS_PER_ARENA)
{
let mut arena = CacheArena::default();
let cache = v.new_tree_hash_cache(&mut arena);
self.arenas.push((arena, vec![cache]))
} else {
let (arena, caches) = &mut self
.arenas
.last_mut()
.expect("Cannot reach this block if arenas is empty.");
caches.push(v.new_tree_hash_cache(arena))
}
})
} else if validators.len() < self.len() {
return Err(Error::ValidatorRegistryShrunk);
}
self.arenas
.par_iter_mut()
.enumerate()
.map(|(arena_index, (arena, caches))| {
caches
.iter_mut()
.enumerate()
.map(move |(cache_index, cache)| {
let val_index = (arena_index * VALIDATORS_PER_ARENA) + cache_index;
let validator = validators
.get(val_index)
.ok_or_else(|| Error::TreeHashCacheInconsistent)?;
validator
.recalculate_tree_hash_root(arena, cache)
.map_err(Error::CachedTreeHashError)
})
.collect()
})
.collect()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn validator_node_count() {
let mut arena = CacheArena::default();
let v = Validator::default();
let _cache = v.new_tree_hash_cache(&mut arena);
assert_eq!(arena.backing_len(), NODES_PER_VALIDATOR);
}
}

View File

@@ -0,0 +1,782 @@
use crate::*;
use int_to_bytes::int_to_bytes4;
use serde_derive::{Deserialize, Serialize};
use std::fs::File;
use std::path::Path;
use tree_hash::TreeHash;
use utils::{
fork_from_hex_str, fork_to_hex_str, u32_from_hex_str, u32_to_hex_str, u8_from_hex_str,
u8_to_hex_str,
};
/// Each of the BLS signature domains.
///
/// Spec v0.11.1
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Domain {
BeaconProposer,
BeaconAttester,
Randao,
Deposit,
VoluntaryExit,
SelectionProof,
AggregateAndProof,
}
/// Holds all the "constants" for a BeaconChain.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ChainSpec {
/*
* Constants
*/
pub genesis_slot: Slot,
#[serde(skip_serializing)] // skipped because Serde TOML has trouble with u64::max
pub far_future_epoch: Epoch,
pub base_rewards_per_epoch: u64,
pub deposit_contract_tree_depth: u64,
/*
* Misc
*/
pub max_committees_per_slot: usize,
pub target_committee_size: usize,
pub min_per_epoch_churn_limit: u64,
pub churn_limit_quotient: u64,
pub shuffle_round_count: u8,
pub min_genesis_active_validator_count: u64,
pub min_genesis_time: u64,
pub hysteresis_quotient: u64,
pub hysteresis_downward_multiplier: u64,
pub hysteresis_upward_multiplier: u64,
/*
* Gwei values
*/
pub min_deposit_amount: u64,
pub max_effective_balance: u64,
pub ejection_balance: u64,
pub effective_balance_increment: u64,
/*
* Initial Values
*/
#[serde(
serialize_with = "fork_to_hex_str",
deserialize_with = "fork_from_hex_str"
)]
pub genesis_fork_version: [u8; 4],
#[serde(deserialize_with = "u8_from_hex_str", serialize_with = "u8_to_hex_str")]
pub bls_withdrawal_prefix_byte: u8,
/*
* Time parameters
*/
pub min_genesis_delay: u64,
pub milliseconds_per_slot: u64,
pub min_attestation_inclusion_delay: u64,
pub min_seed_lookahead: Epoch,
pub max_seed_lookahead: Epoch,
pub min_epochs_to_inactivity_penalty: u64,
pub min_validator_withdrawability_delay: Epoch,
pub persistent_committee_period: u64,
/*
* Reward and penalty quotients
*/
pub base_reward_factor: u64,
pub whistleblower_reward_quotient: u64,
pub proposer_reward_quotient: u64,
pub inactivity_penalty_quotient: u64,
pub min_slashing_penalty_quotient: u64,
/*
* Signature domains
*/
domain_beacon_proposer: u32,
domain_beacon_attester: u32,
domain_randao: u32,
domain_deposit: u32,
domain_voluntary_exit: u32,
domain_selection_proof: u32,
domain_aggregate_and_proof: u32,
/*
* Fork choice
*/
pub safe_slots_to_update_justified: u64,
/*
* Eth1
*/
pub eth1_follow_distance: u64,
pub seconds_per_eth1_block: u64,
/*
* Networking
*/
pub boot_nodes: Vec<String>,
pub network_id: u8,
pub attestation_propagation_slot_range: u64,
pub maximum_gossip_clock_disparity_millis: u64,
pub target_aggregators_per_committee: u64,
pub attestation_subnet_count: u64,
pub random_subnets_per_validator: u64,
pub epochs_per_random_subnet_subscription: u64,
}
impl ChainSpec {
/// Returns an `EnrForkId` for the given `slot`.
///
/// Presently, we don't have any forks so we just ignore the slot. In the future this function
/// may return something different based upon the slot.
pub fn enr_fork_id(&self, _slot: Slot, genesis_validators_root: Hash256) -> EnrForkId {
EnrForkId {
fork_digest: Self::compute_fork_digest(
self.genesis_fork_version,
genesis_validators_root,
),
next_fork_version: self.genesis_fork_version,
next_fork_epoch: self.far_future_epoch,
}
}
/// Returns the epoch of the next scheduled change in the `fork.current_version`.
///
/// There are no future forks scheduled so this function always returns `None`. This may not
/// always be the case in the future, though.
pub fn next_fork_epoch(&self) -> Option<Epoch> {
None
}
/// Get the domain number, unmodified by the fork.
///
/// Spec v0.11.1
pub fn get_domain_constant(&self, domain: Domain) -> u32 {
match domain {
Domain::BeaconProposer => self.domain_beacon_proposer,
Domain::BeaconAttester => self.domain_beacon_attester,
Domain::Randao => self.domain_randao,
Domain::Deposit => self.domain_deposit,
Domain::VoluntaryExit => self.domain_voluntary_exit,
Domain::SelectionProof => self.domain_selection_proof,
Domain::AggregateAndProof => self.domain_aggregate_and_proof,
}
}
/// Get the domain that represents the fork meta and signature domain.
///
/// Spec v0.11.1
pub fn get_domain(
&self,
epoch: Epoch,
domain: Domain,
fork: &Fork,
genesis_validators_root: Hash256,
) -> Hash256 {
let fork_version = fork.get_fork_version(epoch);
self.compute_domain(domain, fork_version, genesis_validators_root)
}
/// Get the domain for a deposit signature.
///
/// Deposits are valid across forks, thus the deposit domain is computed
/// with the genesis fork version.
///
/// Spec v0.11.1
pub fn get_deposit_domain(&self) -> Hash256 {
self.compute_domain(Domain::Deposit, self.genesis_fork_version, Hash256::zero())
}
/// Return the 32-byte fork data root for the `current_version` and `genesis_validators_root`.
///
/// This is used primarily in signature domains to avoid collisions across forks/chains.
///
/// Spec v0.11.1
pub fn compute_fork_data_root(
current_version: [u8; 4],
genesis_validators_root: Hash256,
) -> Hash256 {
ForkData {
current_version,
genesis_validators_root,
}
.tree_hash_root()
}
/// Return the 4-byte fork digest for the `current_version` and `genesis_validators_root`.
///
/// This is a digest primarily used for domain separation on the p2p layer.
/// 4-bytes suffices for practical separation of forks/chains.
pub fn compute_fork_digest(
current_version: [u8; 4],
genesis_validators_root: Hash256,
) -> [u8; 4] {
let mut result = [0; 4];
let root = Self::compute_fork_data_root(current_version, genesis_validators_root);
result.copy_from_slice(&root.as_bytes()[0..4]);
result
}
/// Compute a domain by applying the given `fork_version`.
///
/// Spec v0.11.1
pub fn compute_domain(
&self,
domain: Domain,
fork_version: [u8; 4],
genesis_validators_root: Hash256,
) -> Hash256 {
let domain_constant = self.get_domain_constant(domain);
let mut domain = [0; 32];
domain[0..4].copy_from_slice(&int_to_bytes4(domain_constant));
domain[4..].copy_from_slice(
&Self::compute_fork_data_root(fork_version, genesis_validators_root)[..28],
);
Hash256::from(domain)
}
/// Returns a `ChainSpec` compatible with the Ethereum Foundation specification.
///
/// Spec v0.11.1
pub fn mainnet() -> Self {
Self {
/*
* Constants
*/
genesis_slot: Slot::new(0),
far_future_epoch: Epoch::new(u64::max_value()),
base_rewards_per_epoch: 4,
deposit_contract_tree_depth: 32,
/*
* Misc
*/
max_committees_per_slot: 64,
target_committee_size: 128,
min_per_epoch_churn_limit: 4,
churn_limit_quotient: 65_536,
shuffle_round_count: 90,
min_genesis_active_validator_count: 16_384,
min_genesis_time: 1_578_009_600, // Jan 3, 2020
hysteresis_quotient: 4,
hysteresis_downward_multiplier: 1,
hysteresis_upward_multiplier: 5,
/*
* Gwei values
*/
min_deposit_amount: u64::pow(2, 0).saturating_mul(u64::pow(10, 9)),
max_effective_balance: u64::pow(2, 5).saturating_mul(u64::pow(10, 9)),
ejection_balance: u64::pow(2, 4).saturating_mul(u64::pow(10, 9)),
effective_balance_increment: u64::pow(2, 0).saturating_mul(u64::pow(10, 9)),
/*
* Initial Values
*/
genesis_fork_version: [0; 4],
bls_withdrawal_prefix_byte: 0,
/*
* Time parameters
*/
min_genesis_delay: 86400, // 1 day
milliseconds_per_slot: 12_000,
min_attestation_inclusion_delay: 1,
min_seed_lookahead: Epoch::new(1),
max_seed_lookahead: Epoch::new(4),
min_epochs_to_inactivity_penalty: 4,
min_validator_withdrawability_delay: Epoch::new(256),
persistent_committee_period: 2_048,
/*
* Reward and penalty quotients
*/
base_reward_factor: 64,
whistleblower_reward_quotient: 512,
proposer_reward_quotient: 8,
inactivity_penalty_quotient: 33_554_432,
min_slashing_penalty_quotient: 32,
/*
* Signature domains
*/
domain_beacon_proposer: 0,
domain_beacon_attester: 1,
domain_randao: 2,
domain_deposit: 3,
domain_voluntary_exit: 4,
domain_selection_proof: 5,
domain_aggregate_and_proof: 6,
/*
* Fork choice
*/
safe_slots_to_update_justified: 8,
/*
* Eth1
*/
eth1_follow_distance: 1_024,
seconds_per_eth1_block: 14,
/*
* Network specific
*/
boot_nodes: vec![],
network_id: 1, // mainnet network id
attestation_propagation_slot_range: 32,
attestation_subnet_count: 64,
random_subnets_per_validator: 1,
maximum_gossip_clock_disparity_millis: 500,
target_aggregators_per_committee: 16,
epochs_per_random_subnet_subscription: 256,
}
}
/// Ethereum Foundation minimal spec, as defined in the eth2.0-specs repo.
///
/// Spec v0.11.1
pub fn minimal() -> Self {
// Note: bootnodes to be updated when static nodes exist.
let boot_nodes = vec![];
Self {
max_committees_per_slot: 4,
target_committee_size: 4,
shuffle_round_count: 10,
min_genesis_active_validator_count: 64,
eth1_follow_distance: 16,
genesis_fork_version: [0x00, 0x00, 0x00, 0x01],
persistent_committee_period: 128,
min_genesis_delay: 300,
milliseconds_per_slot: 6_000,
safe_slots_to_update_justified: 2,
network_id: 2, // lighthouse testnet network id
boot_nodes,
..ChainSpec::mainnet()
}
}
/// Interop testing spec
///
/// This allows us to customize a chain spec for interop testing.
pub fn interop() -> Self {
let boot_nodes = vec![];
Self {
milliseconds_per_slot: 12_000,
target_committee_size: 4,
shuffle_round_count: 10,
network_id: 13,
boot_nodes,
..ChainSpec::mainnet()
}
}
}
impl Default for ChainSpec {
fn default() -> Self {
Self::mainnet()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mainnet_spec_can_be_constructed() {
let _ = ChainSpec::mainnet();
}
fn test_domain(domain_type: Domain, raw_domain: u32, spec: &ChainSpec) {
let previous_version = [0, 0, 0, 1];
let current_version = [0, 0, 0, 2];
let genesis_validators_root = Hash256::from_low_u64_le(77);
let fork_epoch = Epoch::new(1024);
let fork = Fork {
previous_version,
current_version,
epoch: fork_epoch,
};
for (epoch, version) in vec![
(fork_epoch - 1, previous_version),
(fork_epoch, current_version),
(fork_epoch + 1, current_version),
] {
let domain1 = spec.get_domain(epoch, domain_type, &fork, genesis_validators_root);
let domain2 = spec.compute_domain(domain_type, version, genesis_validators_root);
assert_eq!(domain1, domain2);
assert_eq!(&domain1.as_bytes()[0..4], &int_to_bytes4(raw_domain)[..]);
}
}
#[test]
fn test_get_domain() {
let spec = ChainSpec::mainnet();
test_domain(Domain::BeaconProposer, spec.domain_beacon_proposer, &spec);
test_domain(Domain::BeaconAttester, spec.domain_beacon_attester, &spec);
test_domain(Domain::Randao, spec.domain_randao, &spec);
test_domain(Domain::Deposit, spec.domain_deposit, &spec);
test_domain(Domain::VoluntaryExit, spec.domain_voluntary_exit, &spec);
test_domain(Domain::SelectionProof, spec.domain_selection_proof, &spec);
test_domain(
Domain::AggregateAndProof,
spec.domain_aggregate_and_proof,
&spec,
);
}
}
/// Union of a ChainSpec struct and an EthSpec struct that holds constants used for the configs
/// from the Ethereum 2 specs repo (https://github.com/ethereum/eth2.0-specs/tree/dev/configs)
///
/// Doesn't include fields of the YAML that we don't need yet (e.g. Phase 1 stuff).
///
/// Spec v0.11.1
// Yaml Config is declared here in order to access domain fields of ChainSpec which are private.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "UPPERCASE")]
#[serde(default)]
pub struct YamlConfig {
// ChainSpec
far_future_epoch: u64,
base_rewards_per_epoch: u64,
deposit_contract_tree_depth: u64,
max_committees_per_slot: usize,
target_committee_size: usize,
min_per_epoch_churn_limit: u64,
churn_limit_quotient: u64,
shuffle_round_count: u8,
min_genesis_active_validator_count: u64,
min_genesis_time: u64,
min_genesis_delay: u64,
min_deposit_amount: u64,
max_effective_balance: u64,
ejection_balance: u64,
effective_balance_increment: u64,
hysteresis_quotient: u64,
hysteresis_downward_multiplier: u64,
hysteresis_upward_multiplier: u64,
genesis_slot: u64,
#[serde(
serialize_with = "fork_to_hex_str",
deserialize_with = "fork_from_hex_str"
)]
genesis_fork_version: [u8; 4],
#[serde(deserialize_with = "u8_from_hex_str", serialize_with = "u8_to_hex_str")]
bls_withdrawal_prefix: u8,
seconds_per_slot: u64,
min_attestation_inclusion_delay: u64,
min_seed_lookahead: u64,
max_seed_lookahead: u64,
min_epochs_to_inactivity_penalty: u64,
min_validator_withdrawability_delay: u64,
persistent_committee_period: u64,
base_reward_factor: u64,
whistleblower_reward_quotient: u64,
proposer_reward_quotient: u64,
inactivity_penalty_quotient: u64,
min_slashing_penalty_quotient: u64,
safe_slots_to_update_justified: u64,
#[serde(
deserialize_with = "u32_from_hex_str",
serialize_with = "u32_to_hex_str"
)]
domain_beacon_proposer: u32,
#[serde(
deserialize_with = "u32_from_hex_str",
serialize_with = "u32_to_hex_str"
)]
domain_beacon_attester: u32,
#[serde(
deserialize_with = "u32_from_hex_str",
serialize_with = "u32_to_hex_str"
)]
domain_randao: u32,
#[serde(
deserialize_with = "u32_from_hex_str",
serialize_with = "u32_to_hex_str"
)]
domain_deposit: u32,
#[serde(
deserialize_with = "u32_from_hex_str",
serialize_with = "u32_to_hex_str"
)]
domain_voluntary_exit: u32,
#[serde(
deserialize_with = "u32_from_hex_str",
serialize_with = "u32_to_hex_str"
)]
domain_selection_proof: u32,
#[serde(
deserialize_with = "u32_from_hex_str",
serialize_with = "u32_to_hex_str"
)]
domain_aggregate_and_proof: u32,
#[serde(
deserialize_with = "u32_from_hex_str",
serialize_with = "u32_to_hex_str"
)]
// EthSpec
justification_bits_length: u32,
max_validators_per_committee: u32,
genesis_epoch: Epoch,
slots_per_epoch: u64,
epochs_per_eth1_voting_period: u64,
slots_per_historical_root: usize,
epochs_per_historical_vector: usize,
epochs_per_slashings_vector: usize,
historical_roots_limit: u64,
validator_registry_limit: u64,
max_proposer_slashings: u32,
max_attester_slashings: u32,
max_attestations: u32,
max_deposits: u32,
max_voluntary_exits: u32,
// Validator
eth1_follow_distance: u64,
target_aggregators_per_committee: u64,
random_subnets_per_validator: u64,
epochs_per_random_subnet_subscription: u64,
seconds_per_eth1_block: u64,
}
impl Default for YamlConfig {
fn default() -> Self {
let chain_spec = MainnetEthSpec::default_spec();
YamlConfig::from_spec::<MainnetEthSpec>(&chain_spec)
}
}
/// Spec v0.11.1
impl YamlConfig {
#[allow(clippy::integer_arithmetic)]
pub fn from_spec<T: EthSpec>(spec: &ChainSpec) -> Self {
Self {
// ChainSpec
far_future_epoch: spec.far_future_epoch.into(),
base_rewards_per_epoch: spec.base_rewards_per_epoch,
deposit_contract_tree_depth: spec.deposit_contract_tree_depth,
max_committees_per_slot: spec.max_committees_per_slot,
target_committee_size: spec.target_committee_size,
min_per_epoch_churn_limit: spec.min_per_epoch_churn_limit,
churn_limit_quotient: spec.churn_limit_quotient,
shuffle_round_count: spec.shuffle_round_count,
min_genesis_active_validator_count: spec.min_genesis_active_validator_count,
min_genesis_time: spec.min_genesis_time,
min_genesis_delay: spec.min_genesis_delay,
min_deposit_amount: spec.min_deposit_amount,
max_effective_balance: spec.max_effective_balance,
ejection_balance: spec.ejection_balance,
effective_balance_increment: spec.effective_balance_increment,
hysteresis_quotient: spec.hysteresis_quotient,
hysteresis_downward_multiplier: spec.hysteresis_downward_multiplier,
hysteresis_upward_multiplier: spec.hysteresis_upward_multiplier,
genesis_slot: spec.genesis_slot.into(),
bls_withdrawal_prefix: spec.bls_withdrawal_prefix_byte,
seconds_per_slot: spec.milliseconds_per_slot / 1000,
min_attestation_inclusion_delay: spec.min_attestation_inclusion_delay,
min_seed_lookahead: spec.min_seed_lookahead.into(),
max_seed_lookahead: spec.max_seed_lookahead.into(),
min_validator_withdrawability_delay: spec.min_validator_withdrawability_delay.into(),
persistent_committee_period: spec.persistent_committee_period,
min_epochs_to_inactivity_penalty: spec.min_epochs_to_inactivity_penalty,
base_reward_factor: spec.base_reward_factor,
whistleblower_reward_quotient: spec.whistleblower_reward_quotient,
proposer_reward_quotient: spec.proposer_reward_quotient,
inactivity_penalty_quotient: spec.inactivity_penalty_quotient,
min_slashing_penalty_quotient: spec.min_slashing_penalty_quotient,
genesis_fork_version: spec.genesis_fork_version,
safe_slots_to_update_justified: spec.safe_slots_to_update_justified,
domain_beacon_proposer: spec.domain_beacon_proposer,
domain_beacon_attester: spec.domain_beacon_attester,
domain_randao: spec.domain_randao,
domain_deposit: spec.domain_deposit,
domain_voluntary_exit: spec.domain_voluntary_exit,
domain_selection_proof: spec.domain_selection_proof,
domain_aggregate_and_proof: spec.domain_aggregate_and_proof,
// EthSpec
justification_bits_length: T::JustificationBitsLength::to_u32(),
max_validators_per_committee: T::MaxValidatorsPerCommittee::to_u32(),
genesis_epoch: T::genesis_epoch(),
slots_per_epoch: T::slots_per_epoch(),
epochs_per_eth1_voting_period: T::EpochsPerEth1VotingPeriod::to_u64(),
slots_per_historical_root: T::slots_per_historical_root(),
epochs_per_historical_vector: T::epochs_per_historical_vector(),
epochs_per_slashings_vector: T::EpochsPerSlashingsVector::to_usize(),
historical_roots_limit: T::HistoricalRootsLimit::to_u64(),
validator_registry_limit: T::ValidatorRegistryLimit::to_u64(),
max_proposer_slashings: T::MaxProposerSlashings::to_u32(),
max_attester_slashings: T::MaxAttesterSlashings::to_u32(),
max_attestations: T::MaxAttestations::to_u32(),
max_deposits: T::MaxDeposits::to_u32(),
max_voluntary_exits: T::MaxVoluntaryExits::to_u32(),
// Validator
eth1_follow_distance: spec.eth1_follow_distance,
target_aggregators_per_committee: spec.target_aggregators_per_committee,
random_subnets_per_validator: spec.random_subnets_per_validator,
epochs_per_random_subnet_subscription: spec.epochs_per_random_subnet_subscription,
seconds_per_eth1_block: spec.seconds_per_eth1_block,
}
}
pub fn from_file(filename: &Path) -> Result<Self, String> {
let f = File::open(filename)
.map_err(|e| format!("Error opening spec at {}: {:?}", filename.display(), e))?;
serde_yaml::from_reader(f)
.map_err(|e| format!("Error parsing spec at {}: {:?}", filename.display(), e))
}
pub fn apply_to_chain_spec<T: EthSpec>(&self, chain_spec: &ChainSpec) -> Option<ChainSpec> {
// Checking for EthSpec constants
if self.justification_bits_length != T::JustificationBitsLength::to_u32()
|| self.max_validators_per_committee != T::MaxValidatorsPerCommittee::to_u32()
|| self.genesis_epoch != T::genesis_epoch()
|| self.slots_per_epoch != T::slots_per_epoch()
|| self.epochs_per_eth1_voting_period != T::EpochsPerEth1VotingPeriod::to_u64()
|| self.slots_per_historical_root != T::slots_per_historical_root()
|| self.epochs_per_historical_vector != T::epochs_per_historical_vector()
|| self.epochs_per_slashings_vector != T::EpochsPerSlashingsVector::to_usize()
|| self.historical_roots_limit != T::HistoricalRootsLimit::to_u64()
|| self.validator_registry_limit != T::ValidatorRegistryLimit::to_u64()
|| self.max_proposer_slashings != T::MaxProposerSlashings::to_u32()
|| self.max_attester_slashings != T::MaxAttesterSlashings::to_u32()
|| self.max_attestations != T::MaxAttestations::to_u32()
|| self.max_deposits != T::MaxDeposits::to_u32()
|| self.max_voluntary_exits != T::MaxVoluntaryExits::to_u32()
{
return None;
}
// Create a ChainSpec from the yaml config
Some(ChainSpec {
far_future_epoch: Epoch::from(self.far_future_epoch),
base_rewards_per_epoch: self.base_rewards_per_epoch,
deposit_contract_tree_depth: self.deposit_contract_tree_depth,
target_committee_size: self.target_committee_size,
min_per_epoch_churn_limit: self.min_per_epoch_churn_limit,
churn_limit_quotient: self.churn_limit_quotient,
shuffle_round_count: self.shuffle_round_count,
min_genesis_active_validator_count: self.min_genesis_active_validator_count,
min_genesis_time: self.min_genesis_time,
min_deposit_amount: self.min_deposit_amount,
min_genesis_delay: self.min_genesis_delay,
max_effective_balance: self.max_effective_balance,
hysteresis_quotient: self.hysteresis_quotient,
hysteresis_downward_multiplier: self.hysteresis_downward_multiplier,
hysteresis_upward_multiplier: self.hysteresis_upward_multiplier,
ejection_balance: self.ejection_balance,
effective_balance_increment: self.effective_balance_increment,
genesis_slot: Slot::from(self.genesis_slot),
bls_withdrawal_prefix_byte: self.bls_withdrawal_prefix,
milliseconds_per_slot: self.seconds_per_slot.saturating_mul(1000),
min_attestation_inclusion_delay: self.min_attestation_inclusion_delay,
min_seed_lookahead: Epoch::from(self.min_seed_lookahead),
max_seed_lookahead: Epoch::from(self.max_seed_lookahead),
min_validator_withdrawability_delay: Epoch::from(
self.min_validator_withdrawability_delay,
),
persistent_committee_period: self.persistent_committee_period,
min_epochs_to_inactivity_penalty: self.min_epochs_to_inactivity_penalty,
base_reward_factor: self.base_reward_factor,
whistleblower_reward_quotient: self.whistleblower_reward_quotient,
proposer_reward_quotient: self.proposer_reward_quotient,
inactivity_penalty_quotient: self.inactivity_penalty_quotient,
min_slashing_penalty_quotient: self.min_slashing_penalty_quotient,
domain_beacon_proposer: self.domain_beacon_proposer,
domain_beacon_attester: self.domain_beacon_attester,
domain_randao: self.domain_randao,
domain_deposit: self.domain_deposit,
domain_voluntary_exit: self.domain_voluntary_exit,
boot_nodes: chain_spec.boot_nodes.clone(),
genesis_fork_version: self.genesis_fork_version,
eth1_follow_distance: self.eth1_follow_distance,
..*chain_spec
})
}
}
#[cfg(test)]
mod yaml_tests {
use super::*;
use std::fs::OpenOptions;
use tempfile::NamedTempFile;
#[test]
fn minimal_round_trip() {
// create temp file
let tmp_file = NamedTempFile::new().expect("failed to create temp file");
let writer = OpenOptions::new()
.read(false)
.write(true)
.open(tmp_file.as_ref())
.expect("error opening file");
let minimal_spec = ChainSpec::minimal();
let yamlconfig = YamlConfig::from_spec::<MinimalEthSpec>(&minimal_spec);
// write fresh minimal config to file
serde_yaml::to_writer(writer, &yamlconfig).expect("failed to write or serialize");
let reader = OpenOptions::new()
.read(true)
.write(false)
.open(tmp_file.as_ref())
.expect("error while opening the file");
// deserialize minimal config from file
let from: YamlConfig = serde_yaml::from_reader(reader).expect("error while deserializing");
assert_eq!(from, yamlconfig);
}
#[test]
fn mainnet_round_trip() {
let tmp_file = NamedTempFile::new().expect("failed to create temp file");
let writer = OpenOptions::new()
.read(false)
.write(true)
.open(tmp_file.as_ref())
.expect("error opening file");
let mainnet_spec = ChainSpec::mainnet();
let yamlconfig = YamlConfig::from_spec::<MainnetEthSpec>(&mainnet_spec);
serde_yaml::to_writer(writer, &yamlconfig).expect("failed to write or serialize");
let reader = OpenOptions::new()
.read(true)
.write(false)
.open(tmp_file.as_ref())
.expect("error while opening the file");
let from: YamlConfig = serde_yaml::from_reader(reader).expect("error while deserializing");
assert_eq!(from, yamlconfig);
}
#[test]
fn apply_to_spec() {
let mut spec = ChainSpec::minimal();
let yamlconfig = YamlConfig::from_spec::<MinimalEthSpec>(&spec);
// modifying the original spec
spec.deposit_contract_tree_depth += 1;
// Applying a yaml config with incorrect EthSpec should fail
let res = yamlconfig.apply_to_chain_spec::<MainnetEthSpec>(&spec);
assert_eq!(res, None);
// Applying a yaml config with correct EthSpec should NOT fail
let new_spec = yamlconfig
.apply_to_chain_spec::<MinimalEthSpec>(&spec)
.expect("should have applied spec");
assert_eq!(new_spec, ChainSpec::minimal());
}
}

View File

@@ -0,0 +1,36 @@
use crate::test_utils::TestRandom;
use crate::{Epoch, Hash256};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// Casper FFG checkpoint, used in attestations.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(
Debug,
Clone,
PartialEq,
Eq,
Default,
Hash,
Serialize,
Deserialize,
Encode,
Decode,
TreeHash,
TestRandom,
)]
pub struct Checkpoint {
pub epoch: Epoch,
pub root: Hash256,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(Checkpoint);
}

View File

@@ -0,0 +1,26 @@
use crate::test_utils::TestRandom;
use crate::*;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use ssz_types::typenum::U33;
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
pub const DEPOSIT_TREE_DEPTH: usize = 32;
/// A deposit to potentially become a beacon chain validator.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct Deposit {
pub proof: FixedVector<Hash256, U33>,
pub data: DepositData,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(Deposit);
}

View File

@@ -0,0 +1,50 @@
use crate::test_utils::TestRandom;
use crate::*;
use bls::{PublicKeyBytes, SignatureBytes};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// The data supplied by the user to the deposit contract.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct DepositData {
pub pubkey: PublicKeyBytes,
pub withdrawal_credentials: Hash256,
pub amount: u64,
pub signature: SignatureBytes,
}
impl DepositData {
/// Create a `DepositMessage` corresponding to this `DepositData`, for signature verification.
///
/// Spec v0.11.1
pub fn as_deposit_message(&self) -> DepositMessage {
DepositMessage {
pubkey: self.pubkey.clone(),
withdrawal_credentials: self.withdrawal_credentials,
amount: self.amount,
}
}
/// Generate the signature for a given DepositData details.
///
/// Spec v0.11.1
pub fn create_signature(&self, secret_key: &SecretKey, spec: &ChainSpec) -> SignatureBytes {
let domain = spec.get_deposit_domain();
let msg = self.as_deposit_message().signing_root(domain);
SignatureBytes::from(Signature::new(msg.as_bytes(), secret_key))
}
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(DepositData);
}

View File

@@ -0,0 +1,28 @@
use crate::test_utils::TestRandom;
use crate::*;
use bls::PublicKeyBytes;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// The data supplied by the user to the deposit contract.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct DepositMessage {
pub pubkey: PublicKeyBytes,
pub withdrawal_credentials: Hash256,
pub amount: u64,
}
impl SignedRoot for DepositMessage {}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(DepositMessage);
}

View File

@@ -0,0 +1,37 @@
use crate::test_utils::TestRandom;
use crate::utils::{fork_from_hex_str, fork_to_hex_str};
use crate::Epoch;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// Specifies a fork which allows nodes to identify each other on the network. This fork is used in
/// a nodes local ENR.
///
/// Spec v0.11
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(
Debug, Clone, PartialEq, Default, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom,
)]
pub struct EnrForkId {
#[serde(
serialize_with = "fork_to_hex_str",
deserialize_with = "fork_from_hex_str"
)]
pub fork_digest: [u8; 4],
#[serde(
serialize_with = "fork_to_hex_str",
deserialize_with = "fork_from_hex_str"
)]
pub next_fork_version: [u8; 4],
pub next_fork_epoch: Epoch,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(EnrForkId);
}

View File

@@ -0,0 +1,38 @@
use super::Hash256;
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;
/// Contains data obtained from the Eth1 chain.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(
Debug,
PartialEq,
Clone,
Default,
Eq,
Hash,
Serialize,
Deserialize,
Encode,
Decode,
TreeHash,
TestRandom,
)]
pub struct Eth1Data {
pub deposit_root: Hash256,
pub deposit_count: u64,
pub block_hash: Hash256,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(Eth1Data);
}

View File

@@ -0,0 +1,236 @@
use crate::*;
use safe_arith::SafeArith;
use serde_derive::{Deserialize, Serialize};
use ssz_types::typenum::{
Unsigned, U0, U1, U1024, U1099511627776, U128, U16, U16777216, U2, U2048, U32, U4, U4096, U64,
U65536, U8, U8192,
};
use std::fmt::Debug;
pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq {
/*
* Constants
*/
type GenesisEpoch: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type JustificationBitsLength: Unsigned + Clone + Sync + Send + Debug + PartialEq + Default;
type SubnetBitfieldLength: Unsigned + Clone + Sync + Send + Debug + PartialEq + Default;
/*
* Misc
*/
type MaxValidatorsPerCommittee: Unsigned + Clone + Sync + Send + Debug + PartialEq;
/*
* Time parameters
*/
type SlotsPerEpoch: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type EpochsPerEth1VotingPeriod: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type SlotsPerHistoricalRoot: Unsigned + Clone + Sync + Send + Debug + PartialEq;
/*
* State list lengths
*/
type EpochsPerHistoricalVector: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type EpochsPerSlashingsVector: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type HistoricalRootsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type ValidatorRegistryLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq;
/*
* Max operations per block
*/
type MaxProposerSlashings: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type MaxAttesterSlashings: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type MaxAttestations: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type MaxDeposits: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type MaxVoluntaryExits: Unsigned + Clone + Sync + Send + Debug + PartialEq;
/*
* Derived values (set these CAREFULLY)
*/
/// The length of the `{previous,current}_epoch_attestations` lists.
///
/// Must be set to `MaxAttestations * SlotsPerEpoch`
// NOTE: we could safely instantiate these by using type-level arithmetic, but doing
// so adds ~25s to the time required to type-check this crate
type MaxPendingAttestations: Unsigned + Clone + Sync + Send + Debug + PartialEq;
/// The length of `eth1_data_votes`.
///
/// Must be set to `EpochsPerEth1VotingPeriod * SlotsPerEpoch`
type SlotsPerEth1VotingPeriod: Unsigned + Clone + Sync + Send + Debug + PartialEq;
fn default_spec() -> ChainSpec;
fn genesis_epoch() -> Epoch {
Epoch::new(Self::GenesisEpoch::to_u64())
}
/// Return the number of committees per slot.
///
/// Note: the number of committees per slot is constant in each epoch, and depends only on
/// the `active_validator_count` during the slot's epoch.
///
/// Spec v0.11.1
fn get_committee_count_per_slot(
active_validator_count: usize,
spec: &ChainSpec,
) -> Result<usize, Error> {
let slots_per_epoch = Self::SlotsPerEpoch::to_usize();
Ok(std::cmp::max(
1,
std::cmp::min(
spec.max_committees_per_slot,
active_validator_count
.safe_div(slots_per_epoch)?
.safe_div(spec.target_committee_size)?,
),
))
}
/// Returns the minimum number of validators required for this spec.
///
/// This is the _absolute_ minimum, the number required to make the chain operate in the most
/// basic sense. This count is not required to provide any security guarantees regarding
/// decentralization, entropy, etc.
fn minimum_validator_count() -> usize {
Self::SlotsPerEpoch::to_usize()
}
/// Returns the `SLOTS_PER_EPOCH` constant for this specification.
///
/// Spec v0.11.1
fn slots_per_epoch() -> u64 {
Self::SlotsPerEpoch::to_u64()
}
/// Returns the `SLOTS_PER_HISTORICAL_ROOT` constant for this specification.
///
/// Spec v0.11.1
fn slots_per_historical_root() -> usize {
Self::SlotsPerHistoricalRoot::to_usize()
}
/// Returns the `EPOCHS_PER_HISTORICAL_VECTOR` constant for this specification.
///
/// Spec v0.11.1
fn epochs_per_historical_vector() -> usize {
Self::EpochsPerHistoricalVector::to_usize()
}
/// Returns the `SLOTS_PER_ETH1_VOTING_PERIOD` constant for this specification.
///
/// Spec v0.11.1
fn slots_per_eth1_voting_period() -> usize {
Self::SlotsPerEth1VotingPeriod::to_usize()
}
}
/// Macro to inherit some type values from another EthSpec.
#[macro_export]
macro_rules! params_from_eth_spec {
($spec_ty:ty { $($ty_name:ident),+ }) => {
$(type $ty_name = <$spec_ty as EthSpec>::$ty_name;)+
}
}
/// Ethereum Foundation specifications.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct MainnetEthSpec;
impl EthSpec for MainnetEthSpec {
type JustificationBitsLength = U4;
type SubnetBitfieldLength = U64;
type MaxValidatorsPerCommittee = U2048;
type GenesisEpoch = U0;
type SlotsPerEpoch = U32;
type EpochsPerEth1VotingPeriod = U32;
type SlotsPerHistoricalRoot = U8192;
type EpochsPerHistoricalVector = U65536;
type EpochsPerSlashingsVector = U8192;
type HistoricalRootsLimit = U16777216;
type ValidatorRegistryLimit = U1099511627776;
type MaxProposerSlashings = U16;
type MaxAttesterSlashings = U1;
type MaxAttestations = U128;
type MaxDeposits = U16;
type MaxVoluntaryExits = U16;
type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch
type SlotsPerEth1VotingPeriod = U1024; // 32 epochs * 32 slots per epoch
fn default_spec() -> ChainSpec {
ChainSpec::mainnet()
}
}
pub type FoundationBeaconState = BeaconState<MainnetEthSpec>;
/// Ethereum Foundation minimal spec, as defined in the eth2.0-specs repo.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct MinimalEthSpec;
impl EthSpec for MinimalEthSpec {
type SlotsPerEpoch = U8;
type EpochsPerEth1VotingPeriod = U2;
type SlotsPerHistoricalRoot = U64;
type EpochsPerHistoricalVector = U64;
type EpochsPerSlashingsVector = U64;
type MaxPendingAttestations = U1024; // 128 max attestations * 8 slots per epoch
type SlotsPerEth1VotingPeriod = U16; // 2 epochs * 8 slots per epoch
params_from_eth_spec!(MainnetEthSpec {
JustificationBitsLength,
SubnetBitfieldLength,
MaxValidatorsPerCommittee,
GenesisEpoch,
HistoricalRootsLimit,
ValidatorRegistryLimit,
MaxProposerSlashings,
MaxAttesterSlashings,
MaxAttestations,
MaxDeposits,
MaxVoluntaryExits
});
fn default_spec() -> ChainSpec {
ChainSpec::minimal()
}
}
pub type MinimalBeaconState = BeaconState<MinimalEthSpec>;
/// Interop testnet spec
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct InteropEthSpec;
impl EthSpec for InteropEthSpec {
type SlotsPerEpoch = U8;
type EpochsPerEth1VotingPeriod = U2;
type SlotsPerHistoricalRoot = U64;
type EpochsPerHistoricalVector = U64;
type EpochsPerSlashingsVector = U64;
type MaxPendingAttestations = U1024; // 128 max attestations * 8 slots per epoch
type SlotsPerEth1VotingPeriod = U16; // 2 epochs * 8 slots per epoch
params_from_eth_spec!(MainnetEthSpec {
JustificationBitsLength,
SubnetBitfieldLength,
MaxValidatorsPerCommittee,
GenesisEpoch,
HistoricalRootsLimit,
ValidatorRegistryLimit,
MaxProposerSlashings,
MaxAttesterSlashings,
MaxAttestations,
MaxDeposits,
MaxVoluntaryExits
});
fn default_spec() -> ChainSpec {
ChainSpec::interop()
}
}
pub type InteropBeaconState = BeaconState<InteropEthSpec>;

View File

@@ -0,0 +1,65 @@
use crate::test_utils::TestRandom;
use crate::utils::{fork_from_hex_str, fork_to_hex_str};
use crate::Epoch;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// Specifies a fork of the `BeaconChain`, to prevent replay attacks.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(
Debug, Clone, PartialEq, Default, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom,
)]
pub struct Fork {
#[serde(
serialize_with = "fork_to_hex_str",
deserialize_with = "fork_from_hex_str"
)]
pub previous_version: [u8; 4],
#[serde(
serialize_with = "fork_to_hex_str",
deserialize_with = "fork_from_hex_str"
)]
pub current_version: [u8; 4],
pub epoch: Epoch,
}
impl Fork {
/// Return the fork version of the given ``epoch``.
///
/// Spec v0.11.1
pub fn get_fork_version(&self, epoch: Epoch) -> [u8; 4] {
if epoch < self.epoch {
return self.previous_version;
}
self.current_version
}
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(Fork);
#[test]
fn get_fork_version() {
let previous_version = [1; 4];
let current_version = [2; 4];
let epoch = Epoch::new(10);
let fork = Fork {
previous_version,
current_version,
epoch,
};
assert_eq!(fork.get_fork_version(epoch - 1), previous_version);
assert_eq!(fork.get_fork_version(epoch), current_version);
assert_eq!(fork.get_fork_version(epoch + 1), current_version);
}
}

View File

@@ -0,0 +1,33 @@
use crate::test_utils::TestRandom;
use crate::utils::{fork_from_hex_str, fork_to_hex_str};
use crate::{Hash256, SignedRoot};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// Specifies a fork of the `BeaconChain`, to prevent replay attacks.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(
Debug, Clone, PartialEq, Default, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom,
)]
pub struct ForkData {
#[serde(
serialize_with = "fork_to_hex_str",
deserialize_with = "fork_from_hex_str"
)]
pub current_version: [u8; 4],
pub genesis_validators_root: Hash256,
}
impl SignedRoot for ForkData {}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(ForkData);
}

View File

@@ -0,0 +1,13 @@
/// Note: this object does not actually exist in the spec.
///
/// We use it for managing attestations that have not been aggregated.
use super::{AttestationData, Signature};
use serde_derive::Serialize;
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct FreeAttestation {
pub data: AttestationData,
pub signature: Signature,
pub validator_index: u64,
}

View File

@@ -0,0 +1,27 @@
use crate::test_utils::TestRandom;
use crate::*;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use ssz_types::FixedVector;
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// Historical block and state roots.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct HistoricalBatch<T: EthSpec> {
pub block_roots: FixedVector<Hash256, T::SlotsPerHistoricalRoot>,
pub state_roots: FixedVector<Hash256, T::SlotsPerHistoricalRoot>,
}
#[cfg(test)]
mod tests {
use super::*;
pub type FoundationHistoricalBatch = HistoricalBatch<MainnetEthSpec>;
ssz_and_tree_hash_tests!(FoundationHistoricalBatch);
}

View File

@@ -0,0 +1,126 @@
use crate::{test_utils::TestRandom, AggregateSignature, AttestationData, EthSpec, VariableList};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// Details an attestation that can be slashable.
///
/// To be included in an `AttesterSlashing`.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct IndexedAttestation<T: EthSpec> {
/// Lists validator registry indices, not committee indices.
pub attesting_indices: VariableList<u64, T::MaxValidatorsPerCommittee>,
pub data: AttestationData,
pub signature: AggregateSignature,
}
impl<T: EthSpec> IndexedAttestation<T> {
/// Check if ``attestation_data_1`` and ``attestation_data_2`` have the same target.
///
/// Spec v0.11.1
pub fn is_double_vote(&self, other: &Self) -> bool {
self.data.target.epoch == other.data.target.epoch && self.data != other.data
}
/// Check if ``attestation_data_1`` surrounds ``attestation_data_2``.
///
/// Spec v0.11.1
pub fn is_surround_vote(&self, other: &Self) -> bool {
self.data.source.epoch < other.data.source.epoch
&& other.data.target.epoch < self.data.target.epoch
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::slot_epoch::Epoch;
use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
use crate::MainnetEthSpec;
#[test]
pub fn test_is_double_vote_true() {
let indexed_vote_first = create_indexed_attestation(3, 1);
let indexed_vote_second = create_indexed_attestation(3, 2);
assert_eq!(
indexed_vote_first.is_double_vote(&indexed_vote_second),
true
)
}
#[test]
pub fn test_is_double_vote_false() {
let indexed_vote_first = create_indexed_attestation(1, 1);
let indexed_vote_second = create_indexed_attestation(2, 1);
assert_eq!(
indexed_vote_first.is_double_vote(&indexed_vote_second),
false
);
}
#[test]
pub fn test_is_surround_vote_true() {
let indexed_vote_first = create_indexed_attestation(2, 1);
let indexed_vote_second = create_indexed_attestation(1, 2);
assert_eq!(
indexed_vote_first.is_surround_vote(&indexed_vote_second),
true
);
}
#[test]
pub fn test_is_surround_vote_true_realistic() {
let indexed_vote_first = create_indexed_attestation(4, 1);
let indexed_vote_second = create_indexed_attestation(3, 2);
assert_eq!(
indexed_vote_first.is_surround_vote(&indexed_vote_second),
true
);
}
#[test]
pub fn test_is_surround_vote_false_source_epoch_fails() {
let indexed_vote_first = create_indexed_attestation(2, 2);
let indexed_vote_second = create_indexed_attestation(1, 1);
assert_eq!(
indexed_vote_first.is_surround_vote(&indexed_vote_second),
false
);
}
#[test]
pub fn test_is_surround_vote_false_target_epoch_fails() {
let indexed_vote_first = create_indexed_attestation(1, 1);
let indexed_vote_second = create_indexed_attestation(2, 2);
assert_eq!(
indexed_vote_first.is_surround_vote(&indexed_vote_second),
false
);
}
ssz_and_tree_hash_tests!(IndexedAttestation<MainnetEthSpec>);
fn create_indexed_attestation(
target_epoch: u64,
source_epoch: u64,
) -> IndexedAttestation<MainnetEthSpec> {
let mut rng = XorShiftRng::from_seed([42; 16]);
let mut indexed_vote = IndexedAttestation::random_for_test(&mut rng);
indexed_vote.data.source.epoch = Epoch::new(source_epoch);
indexed_vote.data.target.epoch = Epoch::new(target_epoch);
indexed_vote
}
}

101
consensus/types/src/lib.rs Normal file
View File

@@ -0,0 +1,101 @@
//! Ethereum 2.0 types
// Required for big type-level numbers
#![recursion_limit = "128"]
// Clippy lint set up
#![deny(clippy::integer_arithmetic)]
#[macro_use]
pub mod test_utils;
pub mod aggregate_and_proof;
pub mod attestation;
pub mod attestation_data;
pub mod attestation_duty;
pub mod attester_slashing;
pub mod beacon_block;
pub mod beacon_block_body;
pub mod beacon_block_header;
pub mod beacon_committee;
pub mod beacon_state;
pub mod chain_spec;
pub mod checkpoint;
pub mod deposit;
pub mod deposit_data;
pub mod deposit_message;
pub mod enr_fork_id;
pub mod eth1_data;
pub mod eth_spec;
pub mod fork;
pub mod fork_data;
pub mod free_attestation;
pub mod historical_batch;
pub mod indexed_attestation;
pub mod pending_attestation;
pub mod proposer_slashing;
pub mod relative_epoch;
pub mod selection_proof;
pub mod signed_aggregate_and_proof;
pub mod signed_beacon_block;
pub mod signed_beacon_block_header;
pub mod signed_voluntary_exit;
pub mod signing_root;
pub mod utils;
pub mod validator;
pub mod voluntary_exit;
#[macro_use]
pub mod slot_epoch_macros;
pub mod slot_epoch;
pub mod subnet_id;
mod tree_hash_impls;
#[cfg(feature = "sqlite")]
pub mod sqlite;
use ethereum_types::{H160, H256};
pub use crate::aggregate_and_proof::AggregateAndProof;
pub use crate::attestation::{Attestation, Error as AttestationError};
pub use crate::attestation_data::AttestationData;
pub use crate::attestation_duty::AttestationDuty;
pub use crate::attester_slashing::AttesterSlashing;
pub use crate::beacon_block::BeaconBlock;
pub use crate::beacon_block_body::BeaconBlockBody;
pub use crate::beacon_block_header::BeaconBlockHeader;
pub use crate::beacon_committee::{BeaconCommittee, OwnedBeaconCommittee};
pub use crate::beacon_state::{BeaconTreeHashCache, Error as BeaconStateError, *};
pub use crate::chain_spec::{ChainSpec, Domain, YamlConfig};
pub use crate::checkpoint::Checkpoint;
pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH};
pub use crate::deposit_data::DepositData;
pub use crate::deposit_message::DepositMessage;
pub use crate::enr_fork_id::EnrForkId;
pub use crate::eth1_data::Eth1Data;
pub use crate::fork::Fork;
pub use crate::fork_data::ForkData;
pub use crate::free_attestation::FreeAttestation;
pub use crate::historical_batch::HistoricalBatch;
pub use crate::indexed_attestation::IndexedAttestation;
pub use crate::pending_attestation::PendingAttestation;
pub use crate::proposer_slashing::ProposerSlashing;
pub use crate::relative_epoch::{Error as RelativeEpochError, RelativeEpoch};
pub use crate::selection_proof::SelectionProof;
pub use crate::signed_aggregate_and_proof::SignedAggregateAndProof;
pub use crate::signed_beacon_block::{SignedBeaconBlock, SignedBeaconBlockHash};
pub use crate::signed_beacon_block_header::SignedBeaconBlockHeader;
pub use crate::signed_voluntary_exit::SignedVoluntaryExit;
pub use crate::signing_root::{SignedRoot, SigningRoot};
pub use crate::slot_epoch::{Epoch, Slot};
pub use crate::subnet_id::SubnetId;
pub use crate::validator::Validator;
pub use crate::voluntary_exit::VoluntaryExit;
pub type CommitteeIndex = u64;
pub type Hash256 = H256;
pub type Address = H160;
pub use bls::{
AggregatePublicKey, AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey,
Signature, SignatureBytes,
};
pub use ssz_types::{typenum, typenum::Unsigned, BitList, BitVector, FixedVector, VariableList};

View File

@@ -0,0 +1,38 @@
use crate::test_utils::TestRandom;
use crate::{AttestationData, BitList, EthSpec};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// An attestation that has been included in the state but not yet fully processed.
///
/// Spec v0.11.1
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct PendingAttestation<T: EthSpec> {
pub aggregation_bits: BitList<T::MaxValidatorsPerCommittee>,
pub data: AttestationData,
pub inclusion_delay: u64,
pub proposer_index: u64,
}
#[cfg(feature = "arbitrary-fuzz")]
impl<T: EthSpec> arbitrary::Arbitrary for PendingAttestation<T> {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self {
aggregation_bits: <BitList<T::MaxValidatorsPerCommittee>>::arbitrary(u)?,
data: AttestationData::arbitrary(u)?,
inclusion_delay: u64::arbitrary(u)?,
proposer_index: u64::arbitrary(u)?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
ssz_and_tree_hash_tests!(PendingAttestation<MainnetEthSpec>);
}

View File

@@ -0,0 +1,24 @@
use crate::test_utils::TestRandom;
use crate::SignedBeaconBlockHeader;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// Two conflicting proposals from the same proposer (validator).
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct ProposerSlashing {
pub signed_header_1: SignedBeaconBlockHeader,
pub signed_header_2: SignedBeaconBlockHeader,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(ProposerSlashing);
}

View File

@@ -0,0 +1,118 @@
use crate::*;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Error {
EpochTooLow { base: Epoch, other: Epoch },
EpochTooHigh { base: Epoch, other: Epoch },
}
#[cfg(feature = "arbitrary-fuzz")]
use arbitrary::Arbitrary;
/// Defines the epochs relative to some epoch. Most useful when referring to the committees prior
/// to and following some epoch.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum RelativeEpoch {
/// The prior epoch.
Previous,
/// The current epoch.
Current,
/// The next epoch.
Next,
}
impl RelativeEpoch {
/// Returns the `epoch` that `self` refers to, with respect to the `base` epoch.
///
/// Spec v0.11.1
pub fn into_epoch(self, base: Epoch) -> Epoch {
match self {
// Due to saturating nature of epoch, check for current first.
RelativeEpoch::Current => base,
RelativeEpoch::Previous => base - 1,
RelativeEpoch::Next => base + 1,
}
}
/// Converts the `other` epoch into a `RelativeEpoch`, with respect to `base`
///
/// ## Errors
/// Returns an error when:
/// - `EpochTooLow` when `other` is more than 1 prior to `base`.
/// - `EpochTooHigh` when `other` is more than 1 after `base`.
///
/// Spec v0.11.1
pub fn from_epoch(base: Epoch, other: Epoch) -> Result<Self, Error> {
// Due to saturating nature of epoch, check for current first.
if other == base {
Ok(RelativeEpoch::Current)
} else if other == base - 1 {
Ok(RelativeEpoch::Previous)
} else if other == base + 1 {
Ok(RelativeEpoch::Next)
} else if other < base {
Err(Error::EpochTooLow { base, other })
} else {
Err(Error::EpochTooHigh { base, other })
}
}
/// Convenience function for `Self::from_epoch` where both slots are converted into epochs.
pub fn from_slot(base: Slot, other: Slot, slots_per_epoch: u64) -> Result<Self, Error> {
Self::from_epoch(base.epoch(slots_per_epoch), other.epoch(slots_per_epoch))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_into_epoch() {
let base = Epoch::new(10);
assert_eq!(RelativeEpoch::Current.into_epoch(base), base);
assert_eq!(RelativeEpoch::Previous.into_epoch(base), base - 1);
assert_eq!(RelativeEpoch::Next.into_epoch(base), base + 1);
}
#[test]
fn from_epoch() {
let base = Epoch::new(10);
assert_eq!(
RelativeEpoch::from_epoch(base, base - 1),
Ok(RelativeEpoch::Previous)
);
assert_eq!(
RelativeEpoch::from_epoch(base, base),
Ok(RelativeEpoch::Current)
);
assert_eq!(
RelativeEpoch::from_epoch(base, base + 1),
Ok(RelativeEpoch::Next)
);
}
#[test]
fn from_slot() {
let slots_per_epoch: u64 = 64;
let base = Slot::new(10 * slots_per_epoch);
assert_eq!(
RelativeEpoch::from_slot(base, base - 1, slots_per_epoch),
Ok(RelativeEpoch::Previous)
);
assert_eq!(
RelativeEpoch::from_slot(base, base, slots_per_epoch),
Ok(RelativeEpoch::Current)
);
assert_eq!(
RelativeEpoch::from_slot(base, base + slots_per_epoch, slots_per_epoch),
Ok(RelativeEpoch::Next)
);
}
}

View File

@@ -0,0 +1,90 @@
use crate::{
ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, SecretKey, Signature, SignedRoot, Slot,
};
use safe_arith::{ArithError, SafeArith};
use std::cmp;
use std::convert::TryInto;
use tree_hash::TreeHash;
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(PartialEq, Debug, Clone)]
pub struct SelectionProof(Signature);
impl SelectionProof {
pub fn new<T: EthSpec>(
slot: Slot,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Self {
let domain = spec.get_domain(
slot.epoch(T::slots_per_epoch()),
Domain::SelectionProof,
fork,
genesis_validators_root,
);
let message = slot.signing_root(domain);
Self(Signature::new(message.as_bytes(), secret_key))
}
/// Returns the "modulo" used for determining if a `SelectionProof` elects an aggregator.
pub fn modulo(committee_len: usize, spec: &ChainSpec) -> Result<u64, ArithError> {
Ok(cmp::max(
1,
(committee_len as u64).safe_div(spec.target_aggregators_per_committee)?,
))
}
pub fn is_aggregator(
&self,
committee_len: usize,
spec: &ChainSpec,
) -> Result<bool, ArithError> {
self.is_aggregator_from_modulo(Self::modulo(committee_len, spec)?)
}
pub fn is_aggregator_from_modulo(&self, modulo: u64) -> Result<bool, ArithError> {
let signature_hash = self.0.tree_hash_root();
let signature_hash_int = u64::from_le_bytes(
signature_hash[0..8]
.as_ref()
.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,
pubkey: &PublicKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> bool {
let domain = spec.get_domain(
slot.epoch(T::slots_per_epoch()),
Domain::SelectionProof,
fork,
genesis_validators_root,
);
let message = slot.signing_root(domain);
self.0.verify(message.as_bytes(), pubkey)
}
}
impl Into<Signature> for SelectionProof {
fn into(self) -> Signature {
self.0
}
}
impl From<Signature> for SelectionProof {
fn from(sig: Signature) -> Self {
Self(sig)
}
}

View File

@@ -0,0 +1,100 @@
use super::{
AggregateAndProof, Attestation, ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey,
SecretKey, SelectionProof, Signature, SignedRoot,
};
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 aggregate proof to publish on the `beacon_aggregate_and_proof`
/// gossipsub topic.
///
/// Spec v0.10.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom, TreeHash)]
#[serde(bound = "T: EthSpec")]
pub struct SignedAggregateAndProof<T: EthSpec> {
/// The `AggregateAndProof` that was signed.
pub message: AggregateAndProof<T>,
/// The aggregate attestation.
pub signature: Signature,
}
impl<T: EthSpec> SignedAggregateAndProof<T> {
/// Produces a new `SignedAggregateAndProof` 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,
aggregate: Attestation<T>,
selection_proof: Option<SelectionProof>,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Self {
let message = AggregateAndProof::from_aggregate(
aggregator_index,
aggregate,
selection_proof,
secret_key,
fork,
genesis_validators_root,
spec,
);
let target_epoch = message.aggregate.data.slot.epoch(T::slots_per_epoch());
let domain = spec.get_domain(
target_epoch,
Domain::AggregateAndProof,
fork,
genesis_validators_root,
);
let signing_message = message.signing_root(domain);
SignedAggregateAndProof {
message,
signature: Signature::new(signing_message.as_bytes(), &secret_key),
}
}
/// 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(message.as_bytes(), validator_pubkey)
}
/// 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,113 @@
use crate::{
test_utils::TestRandom, BeaconBlock, ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey,
SignedRoot, SigningRoot, Slot,
};
use bls::Signature;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use std::fmt;
use test_random_derive::TestRandom;
use tree_hash::TreeHash;
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
pub struct SignedBeaconBlockHash(Hash256);
impl fmt::Debug for SignedBeaconBlockHash {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "SignedBeaconBlockHash({:?})", self.0)
}
}
impl fmt::Display for SignedBeaconBlockHash {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<Hash256> for SignedBeaconBlockHash {
fn from(hash: Hash256) -> SignedBeaconBlockHash {
SignedBeaconBlockHash(hash)
}
}
impl From<SignedBeaconBlockHash> for Hash256 {
fn from(signed_beacon_block_hash: SignedBeaconBlockHash) -> Hash256 {
signed_beacon_block_hash.0
}
}
/// A `BeaconBlock` and a signature from its proposer.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TestRandom)]
#[serde(bound = "E: EthSpec")]
pub struct SignedBeaconBlock<E: EthSpec> {
pub message: BeaconBlock<E>,
pub signature: Signature,
}
impl<E: EthSpec> SignedBeaconBlock<E> {
/// Verify `self.signature`.
///
/// If the root of `block.message` is already known it can be passed in via `object_root_opt`.
/// Otherwise, it will be computed locally.
pub fn verify_signature(
&self,
object_root_opt: Option<Hash256>,
pubkey: &PublicKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> bool {
let domain = spec.get_domain(
self.message.slot.epoch(E::slots_per_epoch()),
Domain::BeaconProposer,
fork,
genesis_validators_root,
);
let message = if let Some(object_root) = object_root_opt {
SigningRoot {
object_root,
domain,
}
.tree_hash_root()
} else {
self.message.signing_root(domain)
};
self.signature.verify(message.as_bytes(), pubkey)
}
/// Convenience accessor for the block's slot.
pub fn slot(&self) -> Slot {
self.message.slot
}
/// Convenience accessor for the block's parent root.
pub fn parent_root(&self) -> Hash256 {
self.message.parent_root
}
/// Convenience accessor for the block's state root.
pub fn state_root(&self) -> Hash256 {
self.message.state_root
}
/// Returns the `tree_hash_root` of the block.
///
/// Spec v0.11.1
pub fn canonical_root(&self) -> Hash256 {
Hash256::from_slice(&self.message.tree_hash_root()[..])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MainnetEthSpec;
ssz_tests!(SignedBeaconBlock<MainnetEthSpec>);
}

View File

@@ -0,0 +1,24 @@
use crate::{test_utils::TestRandom, BeaconBlockHeader};
use bls::Signature;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// An exit voluntarily submitted a validator who wishes to withdraw.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct SignedBeaconBlockHeader {
pub message: BeaconBlockHeader,
pub signature: Signature,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(SignedBeaconBlockHeader);
}

View File

@@ -0,0 +1,24 @@
use crate::{test_utils::TestRandom, VoluntaryExit};
use bls::Signature;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// An exit voluntarily submitted a validator who wishes to withdraw.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct SignedVoluntaryExit {
pub message: VoluntaryExit,
pub signature: Signature,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(SignedVoluntaryExit);
}

View File

@@ -0,0 +1,25 @@
use crate::test_utils::TestRandom;
use crate::Hash256;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash::TreeHash;
use tree_hash_derive::TreeHash;
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct SigningRoot {
pub object_root: Hash256,
pub domain: Hash256,
}
pub trait SignedRoot: TreeHash {
fn signing_root(&self, domain: Hash256) -> Hash256 {
SigningRoot {
object_root: self.tree_hash_root(),
domain,
}
.tree_hash_root()
}
}

View File

@@ -0,0 +1,195 @@
//! The `Slot` and `Epoch` types are defined as new types over u64 to enforce type-safety between
//! the two types.
//!
//! `Slot` and `Epoch` have implementations which permit conversion, comparison and math operations
//! between each and `u64`, however specifically not between each other.
//!
//! All math operations on `Slot` and `Epoch` are saturating, they never wrap.
//!
//! It would be easy to define `PartialOrd` and other traits generically across all types which
//! implement `Into<u64>`, however this would allow operations between `Slots` and `Epochs` which
//! may lead to programming errors which are not detected by the compiler.
use crate::test_utils::TestRandom;
use crate::SignedRoot;
use rand::RngCore;
use serde_derive::{Deserialize, Serialize};
use ssz::{ssz_encode, Decode, DecodeError, Encode};
use std::cmp::{Ord, Ordering};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::iter::Iterator;
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, Sub, SubAssign};
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Eq, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Slot(u64);
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Eq, Clone, Copy, Default, Serialize, Deserialize)]
pub struct Epoch(u64);
impl_common!(Slot);
impl_common!(Epoch);
impl Slot {
pub const fn new(slot: u64) -> Slot {
Slot(slot)
}
pub fn epoch(self, slots_per_epoch: u64) -> Epoch {
Epoch::from(self.0) / Epoch::from(slots_per_epoch)
}
pub fn max_value() -> Slot {
Slot(u64::max_value())
}
}
impl Epoch {
pub const fn new(slot: u64) -> Epoch {
Epoch(slot)
}
pub fn max_value() -> Epoch {
Epoch(u64::max_value())
}
/// The first slot in the epoch.
pub fn start_slot(self, slots_per_epoch: u64) -> Slot {
Slot::from(self.0.saturating_mul(slots_per_epoch))
}
/// The last slot in the epoch.
pub fn end_slot(self, slots_per_epoch: u64) -> Slot {
Slot::from(
self.0
.saturating_add(1)
.saturating_mul(slots_per_epoch)
.saturating_sub(1),
)
}
/// Position of some slot inside an epoch, if any.
///
/// E.g., the first `slot` in `epoch` is at position `0`.
pub fn position(self, slot: Slot, slots_per_epoch: u64) -> Option<usize> {
let start = self.start_slot(slots_per_epoch);
let end = self.end_slot(slots_per_epoch);
if slot >= start && slot <= end {
slot.as_usize().checked_sub(start.as_usize())
} else {
None
}
}
pub fn slot_iter(&self, slots_per_epoch: u64) -> SlotIter {
SlotIter {
current_iteration: 0,
epoch: self,
slots_per_epoch,
}
}
}
impl SignedRoot for Epoch {}
impl SignedRoot for Slot {}
pub struct SlotIter<'a> {
current_iteration: u64,
epoch: &'a Epoch,
slots_per_epoch: u64,
}
impl<'a> Iterator for SlotIter<'a> {
type Item = Slot;
fn next(&mut self) -> Option<Slot> {
if self.current_iteration >= self.slots_per_epoch {
None
} else {
let start_slot = self.epoch.start_slot(self.slots_per_epoch);
let previous = self.current_iteration;
self.current_iteration = self.current_iteration.checked_add(1)?;
Some(start_slot + previous)
}
}
}
#[cfg(test)]
mod slot_tests {
use super::*;
all_tests!(Slot);
}
#[cfg(test)]
mod epoch_tests {
use super::*;
all_tests!(Epoch);
#[test]
fn epoch_start_end() {
let slots_per_epoch = 8;
let epoch = Epoch::new(0);
assert_eq!(epoch.start_slot(slots_per_epoch), Slot::new(0));
assert_eq!(epoch.end_slot(slots_per_epoch), Slot::new(7));
}
#[test]
fn position() {
let slots_per_epoch = 8;
let epoch = Epoch::new(0);
assert_eq!(epoch.position(Slot::new(0), slots_per_epoch), Some(0));
assert_eq!(epoch.position(Slot::new(1), slots_per_epoch), Some(1));
assert_eq!(epoch.position(Slot::new(2), slots_per_epoch), Some(2));
assert_eq!(epoch.position(Slot::new(3), slots_per_epoch), Some(3));
assert_eq!(epoch.position(Slot::new(4), slots_per_epoch), Some(4));
assert_eq!(epoch.position(Slot::new(5), slots_per_epoch), Some(5));
assert_eq!(epoch.position(Slot::new(6), slots_per_epoch), Some(6));
assert_eq!(epoch.position(Slot::new(7), slots_per_epoch), Some(7));
assert_eq!(epoch.position(Slot::new(8), slots_per_epoch), None);
let epoch = Epoch::new(1);
assert_eq!(epoch.position(Slot::new(7), slots_per_epoch), None);
assert_eq!(epoch.position(Slot::new(8), slots_per_epoch), Some(0));
}
#[test]
fn slot_iter() {
let slots_per_epoch = 8;
let epoch = Epoch::new(0);
let mut slots = vec![];
for slot in epoch.slot_iter(slots_per_epoch) {
slots.push(slot);
}
assert_eq!(slots.len(), slots_per_epoch as usize);
for i in 0..slots_per_epoch {
assert_eq!(Slot::from(i), slots[i as usize])
}
}
#[test]
fn max_epoch_ssz() {
let max_epoch = Epoch::max_value();
assert_eq!(
&max_epoch.as_ssz_bytes(),
&[255, 255, 255, 255, 255, 255, 255, 255]
);
assert_eq!(
max_epoch,
Epoch::from_ssz_bytes(&max_epoch.as_ssz_bytes()).unwrap()
);
}
}

View File

@@ -0,0 +1,620 @@
macro_rules! impl_from_into_u64 {
($main: ident) => {
impl From<u64> for $main {
fn from(n: u64) -> $main {
$main(n)
}
}
impl Into<u64> for $main {
fn into(self) -> u64 {
self.0
}
}
impl $main {
pub fn as_u64(&self) -> u64 {
self.0
}
}
};
}
macro_rules! impl_from_into_usize {
($main: ident) => {
impl From<usize> for $main {
fn from(n: usize) -> $main {
$main(n as u64)
}
}
impl Into<usize> for $main {
fn into(self) -> usize {
self.0 as usize
}
}
impl $main {
pub fn as_usize(&self) -> usize {
self.0 as usize
}
}
};
}
macro_rules! impl_math_between {
($main: ident, $other: ident) => {
impl PartialOrd<$other> for $main {
/// Utilizes `partial_cmp` on the underlying `u64`.
fn partial_cmp(&self, other: &$other) -> Option<Ordering> {
Some(self.0.cmp(&(*other).into()))
}
}
impl PartialEq<$other> for $main {
fn eq(&self, other: &$other) -> bool {
let other: u64 = (*other).into();
self.0 == other
}
}
impl Add<$other> for $main {
type Output = $main;
fn add(self, other: $other) -> $main {
$main::from(self.0.saturating_add(other.into()))
}
}
impl AddAssign<$other> for $main {
fn add_assign(&mut self, other: $other) {
self.0 = self.0.saturating_add(other.into());
}
}
impl Sub<$other> for $main {
type Output = $main;
fn sub(self, other: $other) -> $main {
$main::from(self.0.saturating_sub(other.into()))
}
}
impl SubAssign<$other> for $main {
fn sub_assign(&mut self, other: $other) {
self.0 = self.0.saturating_sub(other.into());
}
}
impl Mul<$other> for $main {
type Output = $main;
fn mul(self, rhs: $other) -> $main {
let rhs: u64 = rhs.into();
$main::from(self.0.saturating_mul(rhs))
}
}
impl MulAssign<$other> for $main {
fn mul_assign(&mut self, rhs: $other) {
let rhs: u64 = rhs.into();
self.0 = self.0.saturating_mul(rhs)
}
}
impl Div<$other> for $main {
type Output = $main;
fn div(self, rhs: $other) -> $main {
let rhs: u64 = rhs.into();
$main::from(
self.0
.checked_div(rhs)
.expect("Cannot divide by zero-valued Slot/Epoch"),
)
}
}
impl DivAssign<$other> for $main {
fn div_assign(&mut self, rhs: $other) {
let rhs: u64 = rhs.into();
self.0 = self
.0
.checked_div(rhs)
.expect("Cannot divide by zero-valued Slot/Epoch");
}
}
impl Rem<$other> for $main {
type Output = $main;
fn rem(self, modulus: $other) -> $main {
let modulus: u64 = modulus.into();
$main::from(
self.0
.checked_rem(modulus)
.expect("Cannot divide by zero-valued Slot/Epoch"),
)
}
}
};
}
macro_rules! impl_math {
($type: ident) => {
impl $type {
pub fn saturating_sub<T: Into<$type>>(&self, other: T) -> $type {
*self - other.into()
}
pub fn saturating_add<T: Into<$type>>(&self, other: T) -> $type {
*self + other.into()
}
pub fn checked_div<T: Into<$type>>(&self, rhs: T) -> Option<$type> {
let rhs: $type = rhs.into();
if rhs == 0 {
None
} else {
Some(*self / rhs)
}
}
pub fn is_power_of_two(&self) -> bool {
self.0.is_power_of_two()
}
}
impl Ord for $type {
fn cmp(&self, other: &$type) -> Ordering {
let other: u64 = (*other).into();
self.0.cmp(&other)
}
}
};
}
macro_rules! impl_display {
($type: ident) => {
impl fmt::Display for $type {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl slog::Value for $type {
fn serialize(
&self,
record: &slog::Record,
key: slog::Key,
serializer: &mut dyn slog::Serializer,
) -> slog::Result {
slog::Value::serialize(&self.0, record, key, serializer)
}
}
};
}
macro_rules! impl_debug {
($type: ident) => {
impl fmt::Debug for $type {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}({:?})", stringify!($type), self.0)
}
}
};
}
macro_rules! impl_ssz {
($type: ident) => {
impl Encode for $type {
fn is_ssz_fixed_len() -> bool {
<u64 as Encode>::is_ssz_fixed_len()
}
fn ssz_fixed_len() -> usize {
<u64 as Encode>::ssz_fixed_len()
}
fn ssz_bytes_len(&self) -> usize {
0_u64.ssz_bytes_len()
}
fn ssz_append(&self, buf: &mut Vec<u8>) {
self.0.ssz_append(buf)
}
}
impl Decode for $type {
fn is_ssz_fixed_len() -> bool {
<u64 as Decode>::is_ssz_fixed_len()
}
fn ssz_fixed_len() -> usize {
<u64 as Decode>::ssz_fixed_len()
}
fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
Ok($type(u64::from_ssz_bytes(bytes)?))
}
}
impl tree_hash::TreeHash for $type {
fn tree_hash_type() -> tree_hash::TreeHashType {
tree_hash::TreeHashType::Basic
}
fn tree_hash_packed_encoding(&self) -> Vec<u8> {
ssz_encode(self)
}
fn tree_hash_packing_factor() -> usize {
32usize.wrapping_div(8)
}
fn tree_hash_root(&self) -> tree_hash::Hash256 {
tree_hash::Hash256::from_slice(&int_to_bytes::int_to_fixed_bytes32(self.0))
}
}
impl TestRandom for $type {
fn random_for_test(rng: &mut impl RngCore) -> Self {
$type::from(u64::random_for_test(rng))
}
}
};
}
macro_rules! impl_hash {
($type: ident) => {
// Implemented to stop clippy lint:
// https://rust-lang.github.io/rust-clippy/master/index.html#derive_hash_xor_eq
impl Hash for $type {
fn hash<H: Hasher>(&self, state: &mut H) {
ssz_encode(self).hash(state)
}
}
};
}
macro_rules! impl_common {
($type: ident) => {
impl_from_into_u64!($type);
impl_from_into_usize!($type);
impl_math_between!($type, $type);
impl_math_between!($type, u64);
impl_math!($type);
impl_display!($type);
impl_debug!($type);
impl_ssz!($type);
impl_hash!($type);
};
}
// test macros
#[cfg(test)]
macro_rules! new_tests {
($type: ident) => {
#[test]
fn new() {
assert_eq!($type(0), $type::new(0));
assert_eq!($type(3), $type::new(3));
assert_eq!($type(u64::max_value()), $type::new(u64::max_value()));
}
};
}
#[cfg(test)]
macro_rules! from_into_tests {
($type: ident, $other: ident) => {
#[test]
fn into() {
let x: $other = $type(0).into();
assert_eq!(x, 0);
let x: $other = $type(3).into();
assert_eq!(x, 3);
let x: $other = $type(u64::max_value()).into();
// Note: this will fail on 32 bit systems. This is expected as we don't have a proper
// 32-bit system strategy in place.
assert_eq!(x, $other::max_value());
}
#[test]
fn from() {
assert_eq!($type(0), $type::from(0_u64));
assert_eq!($type(3), $type::from(3_u64));
assert_eq!($type(u64::max_value()), $type::from($other::max_value()));
}
};
}
#[cfg(test)]
macro_rules! math_between_tests {
($type: ident, $other: ident) => {
#[test]
fn partial_ord() {
let assert_partial_ord = |a: u64, partial_ord: Ordering, b: u64| {
let other: $other = $type(b).into();
assert_eq!($type(a).partial_cmp(&other), Some(partial_ord));
};
assert_partial_ord(1, Ordering::Less, 2);
assert_partial_ord(2, Ordering::Greater, 1);
assert_partial_ord(0, Ordering::Less, u64::max_value());
assert_partial_ord(u64::max_value(), Ordering::Greater, 0);
}
#[test]
fn partial_eq() {
let assert_partial_eq = |a: u64, b: u64, is_equal: bool| {
let other: $other = $type(b).into();
assert_eq!($type(a).eq(&other), is_equal);
};
assert_partial_eq(0, 0, true);
assert_partial_eq(0, 1, false);
assert_partial_eq(1, 0, false);
assert_partial_eq(1, 1, true);
assert_partial_eq(u64::max_value(), u64::max_value(), true);
assert_partial_eq(0, u64::max_value(), false);
assert_partial_eq(u64::max_value(), 0, false);
}
#[test]
fn add_and_add_assign() {
let assert_add = |a: u64, b: u64, result: u64| {
let other: $other = $type(b).into();
assert_eq!($type(a) + other, $type(result));
let mut add_assigned = $type(a);
add_assigned += other;
assert_eq!(add_assigned, $type(result));
};
assert_add(0, 1, 1);
assert_add(1, 0, 1);
assert_add(1, 2, 3);
assert_add(2, 1, 3);
assert_add(7, 7, 14);
// Addition should be saturating.
assert_add(u64::max_value(), 1, u64::max_value());
assert_add(u64::max_value(), u64::max_value(), u64::max_value());
}
#[test]
fn sub_and_sub_assign() {
let assert_sub = |a: u64, b: u64, result: u64| {
let other: $other = $type(b).into();
assert_eq!($type(a) - other, $type(result));
let mut sub_assigned = $type(a);
sub_assigned -= other;
assert_eq!(sub_assigned, $type(result));
};
assert_sub(1, 0, 1);
assert_sub(2, 1, 1);
assert_sub(14, 7, 7);
assert_sub(u64::max_value(), 1, u64::max_value() - 1);
assert_sub(u64::max_value(), u64::max_value(), 0);
// Subtraction should be saturating
assert_sub(0, 1, 0);
assert_sub(1, 2, 0);
}
#[test]
fn mul_and_mul_assign() {
let assert_mul = |a: u64, b: u64, result: u64| {
let other: $other = $type(b).into();
assert_eq!($type(a) * other, $type(result));
let mut mul_assigned = $type(a);
mul_assigned *= other;
assert_eq!(mul_assigned, $type(result));
};
assert_mul(2, 2, 4);
assert_mul(1, 2, 2);
assert_mul(0, 2, 0);
// Multiplication should be saturating.
assert_mul(u64::max_value(), 2, u64::max_value());
}
#[test]
fn div_and_div_assign() {
let assert_div = |a: u64, b: u64, result: u64| {
let other: $other = $type(b).into();
assert_eq!($type(a) / other, $type(result));
let mut div_assigned = $type(a);
div_assigned /= other;
assert_eq!(div_assigned, $type(result));
};
assert_div(0, 2, 0);
assert_div(2, 2, 1);
assert_div(100, 50, 2);
assert_div(128, 2, 64);
assert_div(u64::max_value(), 2, 2_u64.pow(63) - 1);
}
#[test]
#[should_panic]
fn div_panics_with_divide_by_zero() {
let other: $other = $type(0).into();
let _ = $type(2) / other;
}
#[test]
#[should_panic]
fn div_assign_panics_with_divide_by_zero() {
let other: $other = $type(0).into();
let mut assigned = $type(2);
assigned /= other;
}
#[test]
fn rem() {
let assert_rem = |a: u64, b: u64, result: u64| {
let other: $other = $type(b).into();
assert_eq!($type(a) % other, $type(result));
};
assert_rem(3, 2, 1);
assert_rem(40, 2, 0);
assert_rem(10, 100, 10);
assert_rem(302042, 3293, 2379);
}
};
}
#[cfg(test)]
macro_rules! math_tests {
($type: ident) => {
#[test]
fn saturating_sub() {
let assert_saturating_sub = |a: u64, b: u64, result: u64| {
assert_eq!($type(a).saturating_sub($type(b)), $type(result));
};
assert_saturating_sub(1, 0, 1);
assert_saturating_sub(2, 1, 1);
assert_saturating_sub(14, 7, 7);
assert_saturating_sub(u64::max_value(), 1, u64::max_value() - 1);
assert_saturating_sub(u64::max_value(), u64::max_value(), 0);
// Subtraction should be saturating
assert_saturating_sub(0, 1, 0);
assert_saturating_sub(1, 2, 0);
}
#[test]
fn saturating_add() {
let assert_saturating_add = |a: u64, b: u64, result: u64| {
assert_eq!($type(a).saturating_add($type(b)), $type(result));
};
assert_saturating_add(0, 1, 1);
assert_saturating_add(1, 0, 1);
assert_saturating_add(1, 2, 3);
assert_saturating_add(2, 1, 3);
assert_saturating_add(7, 7, 14);
// Addition should be saturating.
assert_saturating_add(u64::max_value(), 1, u64::max_value());
assert_saturating_add(u64::max_value(), u64::max_value(), u64::max_value());
}
#[test]
fn checked_div() {
let assert_checked_div = |a: u64, b: u64, result: Option<u64>| {
let division_result_as_u64 = match $type(a).checked_div($type(b)) {
None => None,
Some(val) => Some(val.as_u64()),
};
assert_eq!(division_result_as_u64, result);
};
assert_checked_div(0, 2, Some(0));
assert_checked_div(2, 2, Some(1));
assert_checked_div(100, 50, Some(2));
assert_checked_div(128, 2, Some(64));
assert_checked_div(u64::max_value(), 2, Some(2_u64.pow(63) - 1));
assert_checked_div(2, 0, None);
assert_checked_div(0, 0, None);
assert_checked_div(u64::max_value(), 0, None);
}
#[test]
fn is_power_of_two() {
let assert_is_power_of_two = |a: u64, result: bool| {
assert_eq!(
$type(a).is_power_of_two(),
result,
"{}.is_power_of_two() != {}",
a,
result
);
};
assert_is_power_of_two(0, false);
assert_is_power_of_two(1, true);
assert_is_power_of_two(2, true);
assert_is_power_of_two(3, false);
assert_is_power_of_two(4, true);
assert_is_power_of_two(2_u64.pow(4), true);
assert_is_power_of_two(u64::max_value(), false);
}
#[test]
fn ord() {
let assert_ord = |a: u64, ord: Ordering, b: u64| {
assert_eq!($type(a).cmp(&$type(b)), ord);
};
assert_ord(1, Ordering::Less, 2);
assert_ord(2, Ordering::Greater, 1);
assert_ord(0, Ordering::Less, u64::max_value());
assert_ord(u64::max_value(), Ordering::Greater, 0);
}
};
}
#[cfg(test)]
macro_rules! all_tests {
($type: ident) => {
new_tests!($type);
math_between_tests!($type, $type);
math_tests!($type);
ssz_and_tree_hash_tests!($type);
mod u64_tests {
use super::*;
from_into_tests!($type, u64);
math_between_tests!($type, u64);
#[test]
pub fn as_64() {
let x = $type(0).as_u64();
assert_eq!(x, 0);
let x = $type(3).as_u64();
assert_eq!(x, 3);
let x = $type(u64::max_value()).as_u64();
assert_eq!(x, u64::max_value());
}
}
mod usize_tests {
use super::*;
from_into_tests!($type, usize);
#[test]
pub fn as_usize() {
let x = $type(0).as_usize();
assert_eq!(x, 0);
let x = $type(3).as_usize();
assert_eq!(x, 3);
let x = $type(u64::max_value()).as_usize();
assert_eq!(x, usize::max_value());
}
}
};
}

View File

@@ -0,0 +1,30 @@
//! Implementations of SQLite compatibility traits.
use crate::{Epoch, Slot};
use rusqlite::{
types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef},
Error,
};
use std::convert::TryFrom;
macro_rules! impl_to_from_sql {
($type:ty) => {
impl ToSql for $type {
fn to_sql(&self) -> Result<ToSqlOutput, Error> {
let val_i64 = i64::try_from(self.as_u64())
.map_err(|e| Error::ToSqlConversionFailure(Box::new(e)))?;
Ok(ToSqlOutput::from(val_i64))
}
}
impl FromSql for $type {
fn column_result(value: ValueRef) -> Result<Self, FromSqlError> {
let val_u64 = u64::try_from(i64::column_result(value)?)
.map_err(|e| FromSqlError::Other(Box::new(e)))?;
Ok(Self::new(val_u64))
}
}
};
}
impl_to_from_sql!(Slot);
impl_to_from_sql!(Epoch);

View File

@@ -0,0 +1,27 @@
//! Identifies each shard by an integer identifier.
use serde_derive::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SubnetId(u64);
impl SubnetId {
pub fn new(id: u64) -> Self {
SubnetId(id)
}
}
impl Deref for SubnetId {
type Target = u64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SubnetId {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@@ -0,0 +1,19 @@
mod testing_attestation_builder;
mod testing_attestation_data_builder;
mod testing_attester_slashing_builder;
mod testing_beacon_block_builder;
mod testing_beacon_state_builder;
mod testing_deposit_builder;
mod testing_pending_attestation_builder;
mod testing_proposer_slashing_builder;
mod testing_voluntary_exit_builder;
pub use testing_attestation_builder::*;
pub use testing_attestation_data_builder::*;
pub use testing_attester_slashing_builder::*;
pub use testing_beacon_block_builder::*;
pub use testing_beacon_state_builder::*;
pub use testing_deposit_builder::*;
pub use testing_pending_attestation_builder::*;
pub use testing_proposer_slashing_builder::*;
pub use testing_voluntary_exit_builder::*;

View File

@@ -0,0 +1,112 @@
use crate::test_utils::{AttestationTestTask, TestingAttestationDataBuilder};
use crate::*;
/// Builds an attestation to be used for testing purposes.
///
/// This struct should **never be used for production purposes.**
pub struct TestingAttestationBuilder<T: EthSpec> {
committee: Vec<usize>,
attestation: Attestation<T>,
}
impl<T: EthSpec> TestingAttestationBuilder<T> {
/// Create a new attestation builder.
pub fn new(
test_task: AttestationTestTask,
state: &BeaconState<T>,
committee: &[usize],
slot: Slot,
index: u64,
spec: &ChainSpec,
) -> Self {
let data_builder = TestingAttestationDataBuilder::new(test_task, state, index, slot, spec);
let mut aggregation_bits_len = committee.len();
if test_task == AttestationTestTask::BadAggregationBitfieldLen {
aggregation_bits_len += 1
}
let mut aggregation_bits = BitList::with_capacity(aggregation_bits_len).unwrap();
for i in 0..committee.len() {
aggregation_bits.set(i, false).unwrap();
}
let attestation = Attestation {
aggregation_bits,
data: data_builder.build(),
signature: AggregateSignature::new(),
};
Self {
attestation,
committee: committee.to_vec(),
}
}
/// Signs the attestation with a subset (or all) committee members.
///
/// `secret_keys` must be supplied in the same order as `signing_validators`. I.e., the first
/// keypair must be that of the first signing validator.
pub fn sign(
&mut self,
test_task: AttestationTestTask,
signing_validators: &[usize],
secret_keys: &[&SecretKey],
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> &mut Self {
assert_eq!(
signing_validators.len(),
secret_keys.len(),
"Must be a key for each validator"
);
for (key_index, validator_index) in signing_validators.iter().enumerate() {
let committee_index = self
.committee
.iter()
.position(|v| *v == *validator_index)
.expect("Signing validator not in attestation committee");
let index = if test_task == AttestationTestTask::BadSignature {
0
} else {
key_index
};
self.attestation
.sign(
secret_keys[index],
committee_index,
fork,
genesis_validators_root,
spec,
)
.expect("can sign attestation");
self.attestation
.aggregation_bits
.set(committee_index, true)
.unwrap();
}
if test_task == AttestationTestTask::BadIndexedAttestationBadSignature {
// Flip an aggregation bit, to make the aggregate invalid
// (We also want to avoid making it completely empty)
self.attestation
.aggregation_bits
.set(0, !self.attestation.aggregation_bits.get(0).unwrap())
.unwrap();
}
self
}
/// Consume the builder and return the attestation.
pub fn build(self) -> Attestation<T> {
self.attestation
}
}

View File

@@ -0,0 +1,90 @@
use crate::test_utils::AttestationTestTask;
use crate::*;
/// Builds an `AttestationData` to be used for testing purposes.
///
/// This struct should **never be used for production purposes.**
pub struct TestingAttestationDataBuilder {
data: AttestationData,
}
impl TestingAttestationDataBuilder {
/// Configures a new `AttestationData` which attests to all of the same parameters as the
/// state.
pub fn new<T: EthSpec>(
test_task: AttestationTestTask,
state: &BeaconState<T>,
index: u64,
mut slot: Slot,
spec: &ChainSpec,
) -> Self {
let current_epoch = state.current_epoch();
let previous_epoch = state.previous_epoch();
let is_previous_epoch = slot.epoch(T::slots_per_epoch()) != current_epoch;
let mut source = if is_previous_epoch {
state.previous_justified_checkpoint.clone()
} else {
state.current_justified_checkpoint.clone()
};
let mut target = if is_previous_epoch {
Checkpoint {
epoch: previous_epoch,
root: *state
.get_block_root(previous_epoch.start_slot(T::slots_per_epoch()))
.unwrap(),
}
} else {
Checkpoint {
epoch: current_epoch,
root: *state
.get_block_root(current_epoch.start_slot(T::slots_per_epoch()))
.unwrap(),
}
};
let beacon_block_root = *state.get_block_root(slot).unwrap();
match test_task {
AttestationTestTask::IncludedTooEarly => {
slot = state.slot - spec.min_attestation_inclusion_delay + 1
}
AttestationTestTask::IncludedTooLate => slot -= T::SlotsPerEpoch::to_u64(),
AttestationTestTask::TargetEpochSlotMismatch => {
target = Checkpoint {
epoch: current_epoch + 1,
root: Hash256::zero(),
};
assert_ne!(target.epoch, slot.epoch(T::slots_per_epoch()));
}
AttestationTestTask::WrongJustifiedCheckpoint => {
source = Checkpoint {
epoch: Epoch::from(0 as u64),
root: Hash256::zero(),
}
}
_ => (),
}
let data = AttestationData {
slot,
index,
// LMD GHOST vote
beacon_block_root,
// FFG Vote
source,
target,
};
Self { data }
}
/// Returns the `AttestationData`, consuming the builder.
pub fn build(self) -> AttestationData {
self.data
}
}

View File

@@ -0,0 +1,109 @@
use crate::test_utils::AttesterSlashingTestTask;
use crate::*;
/// Builds an `AttesterSlashing`.
///
/// This struct should **never be used for production purposes.**
pub struct TestingAttesterSlashingBuilder();
impl TestingAttesterSlashingBuilder {
/// Builds an `AttesterSlashing` that is a double vote.
///
/// The `signer` function is used to sign the double-vote and accepts:
///
/// - `validator_index: u64`
/// - `message: &[u8]`
/// - `epoch: Epoch`
/// - `domain: Domain`
///
/// Where domain is a domain "constant" (e.g., `spec.domain_attestation`).
pub fn double_vote<F, T: EthSpec>(
test_task: AttesterSlashingTestTask,
validator_indices: &[u64],
signer: F,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> AttesterSlashing<T>
where
F: Fn(u64, &[u8]) -> Signature,
{
let slot = Slot::new(1);
let index = 0;
let epoch_1 = Epoch::new(1);
let hash_1 = Hash256::from_low_u64_le(1);
let hash_2 = Hash256::from_low_u64_le(2);
let checkpoint_1 = Checkpoint {
epoch: epoch_1,
root: hash_1,
};
let checkpoint_2 = Checkpoint {
epoch: epoch_1,
root: hash_2,
};
let data_1 = AttestationData {
slot,
index,
beacon_block_root: hash_1,
source: checkpoint_1.clone(),
target: checkpoint_1,
};
let data_2 = if test_task == AttesterSlashingTestTask::NotSlashable {
AttestationData { ..data_1.clone() }
} else {
AttestationData {
target: checkpoint_2,
..data_1.clone()
}
};
let mut attestation_1 = IndexedAttestation {
attesting_indices: if test_task == AttesterSlashingTestTask::IndexedAttestation1Invalid
{
// Trigger bad validator indices ordering error.
vec![1, 0].into()
} else {
validator_indices.to_vec().into()
},
data: data_1,
signature: AggregateSignature::new(),
};
let mut attestation_2 = IndexedAttestation {
attesting_indices: if test_task == AttesterSlashingTestTask::IndexedAttestation2Invalid
{
// Trigger bad validator indices ordering error.
vec![1, 0].into()
} else {
validator_indices.to_vec().into()
},
data: data_2,
signature: AggregateSignature::new(),
};
let add_signatures = |attestation: &mut IndexedAttestation<T>| {
let domain = spec.get_domain(
attestation.data.target.epoch,
Domain::BeaconAttester,
fork,
genesis_validators_root,
);
let message = attestation.data.signing_root(domain);
for validator_index in validator_indices {
let signature = signer(*validator_index, message.as_bytes());
attestation.signature.add(&signature);
}
};
add_signatures(&mut attestation_1);
add_signatures(&mut attestation_2);
AttesterSlashing {
attestation_1,
attestation_2,
}
}
}

View File

@@ -0,0 +1,424 @@
use crate::{
test_utils::{
TestingAttestationBuilder, TestingAttesterSlashingBuilder, TestingDepositBuilder,
TestingProposerSlashingBuilder, TestingVoluntaryExitBuilder,
},
typenum::U4294967296,
*,
};
use int_to_bytes::int_to_bytes32;
use merkle_proof::MerkleTree;
use rayon::prelude::*;
use tree_hash::TreeHash;
/// Builds a beacon block to be used for testing purposes.
///
/// This struct should **never be used for production purposes.**
pub struct TestingBeaconBlockBuilder<T: EthSpec> {
pub block: BeaconBlock<T>,
}
/// Enum used for passing test options to builder
#[derive(PartialEq, Clone, Copy)]
pub enum DepositTestTask {
Valid,
BadPubKey,
BadSig,
InvalidPubKey,
NoReset,
}
/// Enum used for passing test options to builder
#[derive(PartialEq, Clone, Copy)]
pub enum AttestationTestTask {
Valid,
WrongJustifiedCheckpoint,
BadIndexedAttestationBadSignature,
BadAggregationBitfieldLen,
BadSignature,
ValidatorUnknown,
IncludedTooEarly,
IncludedTooLate,
TargetEpochSlotMismatch,
// Note: BadTargetEpoch is unreachable in block processing due to valid inclusion window and
// slot check
}
/// Enum used for passing test options to builder
#[derive(PartialEq, Clone, Copy)]
pub enum AttesterSlashingTestTask {
Valid,
NotSlashable,
IndexedAttestation1Invalid,
IndexedAttestation2Invalid,
}
/// Enum used for passing test options to builder
#[derive(PartialEq, Clone, Copy)]
pub enum ProposerSlashingTestTask {
Valid,
ProposerUnknown,
ProposalEpochMismatch,
ProposalsIdentical,
ProposerNotSlashable,
BadProposal1Signature,
BadProposal2Signature,
}
impl<T: EthSpec> TestingBeaconBlockBuilder<T> {
/// Create a new builder from genesis.
pub fn new(spec: &ChainSpec) -> Self {
Self {
block: BeaconBlock::empty(spec),
}
}
/// Set the previous block root
pub fn set_parent_root(&mut self, root: Hash256) {
self.block.parent_root = root;
}
/// Set the slot of the block.
pub fn set_slot(&mut self, slot: Slot) {
self.block.slot = slot;
}
/// Set the proposer index of the block.
pub fn set_proposer_index(&mut self, proposer_index: u64) {
self.block.proposer_index = proposer_index;
}
/// Sets the randao to be a signature across the blocks epoch.
///
/// Modifying the block's slot after signing may invalidate the signature.
pub fn set_randao_reveal(
&mut self,
sk: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) {
let epoch = self.block.slot.epoch(T::slots_per_epoch());
let domain = spec.get_domain(epoch, Domain::Randao, fork, genesis_validators_root);
let message = epoch.signing_root(domain);
self.block.body.randao_reveal = Signature::new(message.as_bytes(), sk);
}
/// Has the randao reveal been set?
pub fn randao_reveal_not_set(&mut self) -> bool {
self.block.body.randao_reveal.is_empty()
}
/// Inserts a signed, valid `ProposerSlashing` for the validator.
pub fn insert_proposer_slashing(
&mut self,
test_task: ProposerSlashingTestTask,
validator_index: u64,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) {
let proposer_slashing = build_proposer_slashing::<T>(
test_task,
validator_index,
secret_key,
fork,
genesis_validators_root,
spec,
);
self.block
.body
.proposer_slashings
.push(proposer_slashing)
.unwrap();
}
/// Inserts a signed, valid `AttesterSlashing` for each validator index in `validator_indices`.
pub fn insert_attester_slashing(
&mut self,
test_task: AttesterSlashingTestTask,
validator_indices: &[u64],
secret_keys: &[&SecretKey],
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) {
let attester_slashing = build_double_vote_attester_slashing(
test_task,
validator_indices,
secret_keys,
fork,
genesis_validators_root,
spec,
);
let _ = self.block.body.attester_slashings.push(attester_slashing);
}
/// Fills the block with `num_attestations` attestations.
///
/// It will first go and get each committee that is able to include an attestation in this
/// block. If there _are_ enough committees, it will produce an attestation for each. If there
/// _are not_ enough committees, it will start splitting the committees in half until it
/// achieves the target. It will then produce separate attestations for each split committee.
///
/// Note: the signed messages of the split committees will be identical -- it would be possible
/// to aggregate these split attestations.
pub fn insert_attestations(
&mut self,
test_task: AttestationTestTask,
state: &BeaconState<T>,
secret_keys: &[&SecretKey],
num_attestations: usize,
spec: &ChainSpec,
) -> Result<(), BeaconStateError> {
let mut slot = self.block.slot - spec.min_attestation_inclusion_delay;
let mut attestations_added = 0;
// Stores the following (in order):
//
// - The slot of the committee.
// - A list of all validators in the committee.
// - A list of all validators in the committee that should sign the attestation.
// - The index of the committee.
let mut committees: Vec<(Slot, Vec<usize>, Vec<usize>, u64)> = vec![];
if slot < T::slots_per_epoch() {
panic!("slot is too low, will get stuck in loop")
}
// Loop backwards through slots gathering each committee, until:
//
// - The slot is too old to be included in a block at this slot.
// - The `MAX_ATTESTATIONS`.
loop {
if state.slot >= slot + T::slots_per_epoch() {
break;
}
for beacon_committee in state.get_beacon_committees_at_slot(slot)? {
if attestations_added >= num_attestations {
break;
}
committees.push((
slot,
beacon_committee.committee.to_vec(),
beacon_committee.committee.to_vec(),
beacon_committee.index,
));
attestations_added += 1;
}
slot -= 1;
}
// Loop through all the committees, splitting each one in half until we have
// `MAX_ATTESTATIONS` committees.
loop {
if committees.len() >= num_attestations as usize {
break;
}
for i in 0..committees.len() {
if committees.len() >= num_attestations as usize {
break;
}
let (slot, committee, mut signing_validators, index) = committees[i].clone();
let new_signing_validators =
signing_validators.split_off(signing_validators.len() / 2);
committees[i] = (slot, committee.clone(), signing_validators, index);
committees.push((slot, committee, new_signing_validators, index));
}
}
let attestations: Vec<_> = committees
.par_iter()
.map(|(slot, committee, signing_validators, index)| {
let mut builder = TestingAttestationBuilder::new(
test_task, state, committee, *slot, *index, spec,
);
let signing_secret_keys: Vec<&SecretKey> = signing_validators
.iter()
.map(|validator_index| secret_keys[*validator_index])
.collect();
builder.sign(
test_task,
signing_validators,
&signing_secret_keys,
&state.fork,
state.genesis_validators_root,
spec,
);
builder.build()
})
.collect();
for attestation in attestations {
self.block.body.attestations.push(attestation).unwrap();
}
Ok(())
}
/// Insert a `Valid` deposit into the state.
pub fn insert_deposits(
&mut self,
amount: u64,
test_task: DepositTestTask,
// TODO: deal with the fact deposits no longer have explicit indices
_index: u64,
num_deposits: u64,
state: &mut BeaconState<T>,
spec: &ChainSpec,
) {
// Vector containing deposits' data
let mut datas = vec![];
for _ in 0..num_deposits {
let keypair = Keypair::random();
let mut builder = TestingDepositBuilder::new(keypair.pk.clone(), amount);
builder.sign(test_task, &keypair, spec);
datas.push(builder.build().data);
}
// Vector containing all leaves
let leaves = datas
.iter()
.map(|data| data.tree_hash_root())
.collect::<Vec<_>>();
// Building a VarList from leaves
let deposit_data_list = VariableList::<_, U4294967296>::from(leaves.clone());
// Setting the deposit_root to be the tree_hash_root of the VarList
state.eth1_data.deposit_root = deposit_data_list.tree_hash_root();
// Building the merkle tree used for generating proofs
let tree = MerkleTree::create(&leaves[..], spec.deposit_contract_tree_depth as usize);
// Building proofs
let mut proofs = vec![];
for i in 0..leaves.len() {
let (_, mut proof) = tree.generate_proof(i, spec.deposit_contract_tree_depth as usize);
proof.push(Hash256::from_slice(&int_to_bytes32(leaves.len() as u64)));
proofs.push(proof);
}
// Building deposits
let deposits = datas
.into_par_iter()
.zip(proofs.into_par_iter())
.map(|(data, proof)| (data, proof.into()))
.map(|(data, proof)| Deposit { proof, data })
.collect::<Vec<_>>();
// Pushing deposits to block body
for deposit in deposits {
let _ = self.block.body.deposits.push(deposit);
}
// Manually setting the deposit_count to process deposits
// This is for test purposes only
if test_task == DepositTestTask::NoReset {
state.eth1_data.deposit_count += num_deposits;
} else {
state.eth1_deposit_index = 0;
state.eth1_data.deposit_count = num_deposits;
}
}
/// Insert an exit for the given validator at the given epoch into the block.
pub fn insert_exit(
&mut self,
validator_index: u64,
exit_epoch: Epoch,
secret_key: &SecretKey,
state: &BeaconState<T>,
spec: &ChainSpec,
) {
let builder = TestingVoluntaryExitBuilder::new(exit_epoch, validator_index);
let exit = builder.build(secret_key, &state.fork, state.genesis_validators_root, spec);
self.block.body.voluntary_exits.push(exit).unwrap();
}
/// Mutate the block before signing.
pub fn modify(&mut self, f: impl FnOnce(&mut BeaconBlock<T>)) {
f(&mut self.block)
}
/// Signs and returns the block, consuming the builder.
pub fn build(
self,
sk: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> SignedBeaconBlock<T> {
self.block.sign(sk, fork, genesis_validators_root, spec)
}
/// Returns the block, consuming the builder.
pub fn build_without_signing(self) -> SignedBeaconBlock<T> {
SignedBeaconBlock {
message: self.block,
signature: Signature::empty_signature(),
}
}
}
/// Builds an `ProposerSlashing` for some `validator_index`.
///
/// Signs the message using a `BeaconChainHarness`.
pub fn build_proposer_slashing<T: EthSpec>(
test_task: ProposerSlashingTestTask,
validator_index: u64,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> ProposerSlashing {
TestingProposerSlashingBuilder::double_vote::<T>(
test_task,
validator_index,
secret_key,
fork,
genesis_validators_root,
spec,
)
}
/// Builds an `AttesterSlashing` for some `validator_indices`.
///
/// Signs the message using a `BeaconChainHarness`.
pub fn build_double_vote_attester_slashing<T: EthSpec>(
test_task: AttesterSlashingTestTask,
validator_indices: &[u64],
secret_keys: &[&SecretKey],
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> AttesterSlashing<T> {
let signer = |validator_index: u64, message: &[u8]| {
let key_index = validator_indices
.iter()
.position(|&i| i == validator_index)
.expect("Unable to find attester slashing key");
Signature::new(message, secret_keys[key_index])
};
TestingAttesterSlashingBuilder::double_vote(
test_task,
validator_indices,
signer,
fork,
genesis_validators_root,
spec,
)
}

View File

@@ -0,0 +1,244 @@
use super::super::{generate_deterministic_keypairs, KeypairsFile};
use crate::test_utils::{AttestationTestTask, TestingPendingAttestationBuilder};
use crate::*;
use bls::get_withdrawal_credentials;
use log::debug;
use rayon::prelude::*;
use std::path::{Path, PathBuf};
pub const KEYPAIRS_FILE: &str = "keypairs.raw_keypairs";
/// Returns the directory where the generated keypairs should be stored.
///
/// It is either `$HOME/.lighthouse/keypairs.raw_keypairs` or, if `$HOME` is not available,
/// `./keypairs.raw_keypairs`.
pub fn keypairs_path() -> PathBuf {
let dir = dirs::home_dir()
.map(|home| (home.join(".lighthouse")))
.unwrap_or_else(|| PathBuf::from(""));
dir.join(KEYPAIRS_FILE)
}
/// Builds a beacon state to be used for testing purposes.
///
/// This struct should **never be used for production purposes.**
#[derive(Clone)]
pub struct TestingBeaconStateBuilder<T: EthSpec> {
state: BeaconState<T>,
keypairs: Vec<Keypair>,
}
impl<T: EthSpec> TestingBeaconStateBuilder<T> {
/// Attempts to load validators from a file in `$HOME/.lighthouse/keypairs.raw_keypairs`. If
/// the file is unavailable, it generates the keys at runtime.
///
/// If the `$HOME` environment variable is not set, the local directory is used.
///
/// See the `Self::from_keypairs_file` method for more info.
///
/// # Panics
///
/// If the file does not contain enough keypairs or is invalid.
pub fn from_default_keypairs_file_if_exists(validator_count: usize, spec: &ChainSpec) -> Self {
let dir = dirs::home_dir()
.map(|home| home.join(".lighthouse"))
.unwrap_or_else(|| PathBuf::from(""));
let file = dir.join(KEYPAIRS_FILE);
if file.exists() {
TestingBeaconStateBuilder::from_keypairs_file(validator_count, &file, spec)
} else {
TestingBeaconStateBuilder::from_deterministic_keypairs(validator_count, spec)
}
}
/// Loads the initial validator keypairs from a file on disk.
///
/// Loading keypairs from file is ~10x faster than generating them. Use the `gen_keys` command
/// on the `test_harness` binary to generate the keys. In the `test_harness` dir, run `cargo
/// run -- gen_keys -h` for help.
///
/// # Panics
///
/// If the file does not exist, is invalid or does not contain enough keypairs.
pub fn from_keypairs_file(validator_count: usize, path: &Path, spec: &ChainSpec) -> Self {
debug!("Loading {} keypairs from file...", validator_count);
let keypairs = Vec::from_raw_file(path, validator_count).unwrap();
TestingBeaconStateBuilder::from_keypairs(keypairs, spec)
}
/// Generates the validator keypairs deterministically.
pub fn from_deterministic_keypairs(validator_count: usize, spec: &ChainSpec) -> Self {
debug!("Generating {} deterministic keypairs...", validator_count);
let keypairs = generate_deterministic_keypairs(validator_count);
TestingBeaconStateBuilder::from_keypairs(keypairs, spec)
}
/// Uses the given keypair for all validators.
pub fn from_single_keypair(
validator_count: usize,
keypair: &Keypair,
spec: &ChainSpec,
) -> Self {
debug!("Generating {} cloned keypairs...", validator_count);
let mut keypairs = Vec::with_capacity(validator_count);
for _ in 0..validator_count {
keypairs.push(keypair.clone())
}
TestingBeaconStateBuilder::from_keypairs(keypairs, spec)
}
/// Creates the builder from an existing set of keypairs.
pub fn from_keypairs(keypairs: Vec<Keypair>, spec: &ChainSpec) -> Self {
let validator_count = keypairs.len();
let starting_balance = spec.max_effective_balance;
debug!(
"Building {} Validator objects from keypairs...",
validator_count
);
let validators = keypairs
.par_iter()
.map(|keypair| {
let withdrawal_credentials = Hash256::from_slice(&get_withdrawal_credentials(
&keypair.pk,
spec.bls_withdrawal_prefix_byte,
));
Validator {
pubkey: keypair.pk.clone().into(),
withdrawal_credentials,
// All validators start active.
activation_eligibility_epoch: T::genesis_epoch(),
activation_epoch: T::genesis_epoch(),
exit_epoch: spec.far_future_epoch,
withdrawable_epoch: spec.far_future_epoch,
slashed: false,
effective_balance: starting_balance,
}
})
.collect::<Vec<_>>()
.into();
let genesis_time = 1_567_052_589; // 29 August, 2019;
let mut state = BeaconState::new(
genesis_time,
Eth1Data {
deposit_root: Hash256::zero(),
deposit_count: 0,
block_hash: Hash256::zero(),
},
spec,
);
state.eth1_data.deposit_count = validator_count as u64;
state.eth1_deposit_index = validator_count as u64;
let balances = vec![starting_balance; validator_count].into();
debug!("Importing {} existing validators...", validator_count);
state.validators = validators;
state.balances = balances;
debug!("BeaconState initialized.");
Self { state, keypairs }
}
/// Consume the builder and return the `BeaconState` and the keypairs for each validator.
pub fn build(self) -> (BeaconState<T>, Vec<Keypair>) {
(self.state, self.keypairs)
}
/// Ensures that the state returned from `Self::build(..)` has all caches pre-built.
///
/// Note: this performs the build when called. Ensure that no changes are made that would
/// invalidate this cache.
pub fn build_caches(&mut self, spec: &ChainSpec) -> Result<(), BeaconStateError> {
self.state.build_all_caches(spec).unwrap();
Ok(())
}
/// Sets the `BeaconState` to be in a slot, calling `teleport_to_epoch` to update the epoch.
pub fn teleport_to_slot(&mut self, slot: Slot) -> &mut Self {
self.teleport_to_epoch(slot.epoch(T::slots_per_epoch()));
self.state.slot = slot;
self
}
/// Sets the `BeaconState` to be in the first slot of the given epoch.
///
/// Sets all justification/finalization parameters to be be as "perfect" as possible (i.e.,
/// highest justified and finalized slots, full justification bitfield, etc).
fn teleport_to_epoch(&mut self, epoch: Epoch) {
let state = &mut self.state;
let slot = epoch.start_slot(T::slots_per_epoch());
state.slot = slot;
state.previous_justified_checkpoint.epoch = epoch - 3;
state.current_justified_checkpoint.epoch = epoch - 2;
state.justification_bits = BitVector::from_bytes(vec![0b0000_1111]).unwrap();
state.finalized_checkpoint.epoch = epoch - 3;
}
/// Creates a full set of attestations for the `BeaconState`. Each attestation has full
/// participation from its committee and references the expected beacon_block hashes.
///
/// These attestations should be fully conducive to justification and finalization.
pub fn insert_attestations(&mut self, spec: &ChainSpec) {
let state = &mut self.state;
state
.build_committee_cache(RelativeEpoch::Previous, spec)
.unwrap();
state
.build_committee_cache(RelativeEpoch::Current, spec)
.unwrap();
let current_epoch = state.current_epoch();
let previous_epoch = state.previous_epoch();
let first_slot = previous_epoch.start_slot(T::slots_per_epoch()).as_u64();
let last_slot = current_epoch.end_slot(T::slots_per_epoch()).as_u64()
- spec.min_attestation_inclusion_delay;
let last_slot = std::cmp::min(state.slot.as_u64(), last_slot);
for slot in first_slot..=last_slot {
let slot = Slot::from(slot);
let committees: Vec<OwnedBeaconCommittee> = state
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(|c| c.clone().into_owned())
.collect();
for beacon_committee in committees {
let mut builder = TestingPendingAttestationBuilder::new(
AttestationTestTask::Valid,
state,
beacon_committee.index,
slot,
spec,
);
// The entire committee should have signed the pending attestation.
let signers = vec![true; beacon_committee.committee.len()];
builder.add_committee_participation(signers);
let attestation = builder.build();
if attestation.data.target.epoch < state.current_epoch() {
state.previous_epoch_attestations.push(attestation).unwrap()
} else {
state.current_epoch_attestations.push(attestation).unwrap()
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
use crate::test_utils::DepositTestTask;
use crate::*;
use bls::{get_withdrawal_credentials, PublicKeyBytes, SignatureBytes};
/// Builds an deposit to be used for testing purposes.
///
/// This struct should **never be used for production purposes.**
pub struct TestingDepositBuilder {
deposit: Deposit,
}
impl TestingDepositBuilder {
/// Instantiates a new builder.
pub fn new(pubkey: PublicKey, amount: u64) -> Self {
let deposit = Deposit {
proof: vec![].into(),
data: DepositData {
pubkey: PublicKeyBytes::from(pubkey),
withdrawal_credentials: Hash256::zero(),
amount,
signature: SignatureBytes::empty(),
},
};
Self { deposit }
}
/// Signs the deposit, also setting the following values:
///
/// - `pubkey` to the signing pubkey.
/// - `withdrawal_credentials` to the signing pubkey.
/// - `proof_of_possession`
pub fn sign(&mut self, test_task: DepositTestTask, keypair: &Keypair, spec: &ChainSpec) {
let new_key = Keypair::random();
let mut pubkeybytes = PublicKeyBytes::from(keypair.pk.clone());
let mut secret_key = keypair.sk.clone();
match test_task {
DepositTestTask::BadPubKey => pubkeybytes = PublicKeyBytes::from(new_key.pk),
DepositTestTask::InvalidPubKey => {
// Creating invalid public key bytes
let mut public_key_bytes: Vec<u8> = vec![0; 48];
public_key_bytes[0] = 255;
pubkeybytes = PublicKeyBytes::from_bytes(&public_key_bytes).unwrap();
}
DepositTestTask::BadSig => secret_key = new_key.sk,
_ => (),
}
let withdrawal_credentials = Hash256::from_slice(
&get_withdrawal_credentials(&keypair.pk, spec.bls_withdrawal_prefix_byte)[..],
);
// Building the data and signing it
self.deposit.data.pubkey = pubkeybytes;
self.deposit.data.withdrawal_credentials = withdrawal_credentials;
self.deposit.data.signature = self.deposit.data.create_signature(&secret_key, spec);
}
/// Builds the deposit, consuming the builder.
pub fn build(self) -> Deposit {
self.deposit
}
}

View File

@@ -0,0 +1,60 @@
use crate::test_utils::{AttestationTestTask, TestingAttestationDataBuilder};
use crate::*;
/// Builds an `AttesterSlashing` to be used for testing purposes.
///
/// This struct should **never be used for production purposes.**
pub struct TestingPendingAttestationBuilder<T: EthSpec> {
pending_attestation: PendingAttestation<T>,
}
impl<T: EthSpec> TestingPendingAttestationBuilder<T> {
/// Create a new valid* `PendingAttestation` for the given parameters.
///
/// The `inclusion_delay` will be set to `MIN_ATTESTATION_INCLUSION_DELAY`.
///
/// * The aggregation bitfield will be empty, it needs to be set with
/// `Self::add_committee_participation`.
pub fn new(
test_task: AttestationTestTask,
state: &BeaconState<T>,
index: u64,
slot: Slot,
spec: &ChainSpec,
) -> Self {
let data_builder = TestingAttestationDataBuilder::new(test_task, state, index, slot, spec);
let proposer_index = state.get_beacon_proposer_index(slot, spec).unwrap() as u64;
let pending_attestation = PendingAttestation {
aggregation_bits: BitList::with_capacity(T::MaxValidatorsPerCommittee::to_usize())
.unwrap(),
data: data_builder.build(),
inclusion_delay: spec.min_attestation_inclusion_delay,
proposer_index,
};
Self {
pending_attestation,
}
}
/// Sets the committee participation in the `PendingAttestation`.
///
/// The `PendingAttestation` will appear to be signed by each committee member who's value in
/// `signers` is true.
pub fn add_committee_participation(&mut self, signers: Vec<bool>) {
let mut aggregation_bits = BitList::with_capacity(signers.len()).unwrap();
for (i, signed) in signers.iter().enumerate() {
aggregation_bits.set(i, *signed).unwrap();
}
self.pending_attestation.aggregation_bits = aggregation_bits;
}
/// Returns the `PendingAttestation`, consuming the builder.
pub fn build(self) -> PendingAttestation<T> {
self.pending_attestation
}
}

View File

@@ -0,0 +1,82 @@
use crate::test_utils::ProposerSlashingTestTask;
use crate::*;
/// Builds a `ProposerSlashing`.
///
/// This struct should **never be used for production purposes.**
pub struct TestingProposerSlashingBuilder;
impl TestingProposerSlashingBuilder {
/// Builds a `ProposerSlashing` that is a double vote.
///
/// Where domain is a domain "constant" (e.g., `spec.domain_attestation`).
pub fn double_vote<T>(
test_task: ProposerSlashingTestTask,
proposer_index: u64,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> ProposerSlashing
where
T: EthSpec,
{
let slot = Slot::new(0);
let hash_1 = Hash256::from([1; 32]);
let hash_2 = if test_task == ProposerSlashingTestTask::ProposalsIdentical {
hash_1
} else {
Hash256::from([2; 32])
};
let mut signed_header_1 = SignedBeaconBlockHeader {
message: BeaconBlockHeader {
slot,
proposer_index,
parent_root: hash_1,
state_root: hash_1,
body_root: hash_1,
},
signature: Signature::empty_signature(),
};
let slot_2 = if test_task == ProposerSlashingTestTask::ProposalEpochMismatch {
Slot::new(128)
} else {
Slot::new(0)
};
let mut signed_header_2 = SignedBeaconBlockHeader {
message: BeaconBlockHeader {
parent_root: hash_2,
slot: slot_2,
..signed_header_1.message
},
signature: Signature::empty_signature(),
};
if test_task != ProposerSlashingTestTask::BadProposal1Signature {
signed_header_1 =
signed_header_1
.message
.sign::<T>(secret_key, fork, genesis_validators_root, spec);
}
if test_task != ProposerSlashingTestTask::BadProposal2Signature {
signed_header_2 =
signed_header_2
.message
.sign::<T>(secret_key, fork, genesis_validators_root, spec);
}
if test_task == ProposerSlashingTestTask::ProposerUnknown {
signed_header_1.message.proposer_index = 3_141_592;
signed_header_2.message.proposer_index = 3_141_592;
}
ProposerSlashing {
signed_header_1,
signed_header_2,
}
}
}

View File

@@ -0,0 +1,34 @@
use crate::*;
/// Builds an exit to be used for testing purposes.
///
/// This struct should **never be used for production purposes.**
pub struct TestingVoluntaryExitBuilder {
exit: VoluntaryExit,
}
impl TestingVoluntaryExitBuilder {
/// Instantiates a new builder.
pub fn new(epoch: Epoch, validator_index: u64) -> Self {
let exit = VoluntaryExit {
epoch,
validator_index,
};
Self { exit }
}
/// Build and sign the exit.
///
/// The signing secret key must match that of the exiting validator.
pub fn build(
self,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> SignedVoluntaryExit {
self.exit
.sign(secret_key, fork, genesis_validators_root, spec)
}
}

View File

@@ -0,0 +1,46 @@
use crate::*;
use eth2_interop_keypairs::{keypair, keypairs_from_yaml_file};
use log::debug;
use rayon::prelude::*;
use std::path::PathBuf;
/// Generates `validator_count` keypairs where the secret key is derived solely from the index of
/// the validator.
///
/// Uses the `eth2_interop_keypairs` crate to generate keys.
pub fn generate_deterministic_keypairs(validator_count: usize) -> Vec<Keypair> {
debug!(
"Generating {} deterministic validator keypairs...",
validator_count
);
let keypairs: Vec<Keypair> = (0..validator_count)
.collect::<Vec<usize>>()
.into_par_iter()
.map(generate_deterministic_keypair)
.collect();
keypairs
}
/// Generates a single deterministic keypair, where the secret key is `validator_index`.
///
/// This is used for testing only, and not to be used in production!
pub fn generate_deterministic_keypair(validator_index: usize) -> Keypair {
let raw = keypair(validator_index);
Keypair {
pk: PublicKey::from_raw(raw.pk),
sk: SecretKey::from_raw(raw.sk),
}
}
/// Loads a list of keypairs from file.
pub fn load_keypairs_from_yaml(path: PathBuf) -> Result<Vec<Keypair>, String> {
Ok(keypairs_from_yaml_file(path)?
.into_iter()
.map(|raw| Keypair {
pk: PublicKey::from_raw(raw.pk),
sk: SecretKey::from_raw(raw.sk),
})
.collect())
}

View File

@@ -0,0 +1,128 @@
use crate::*;
use rayon::prelude::*;
use std::fs::File;
use std::io::{Error, ErrorKind, Read, Write};
use std::path::Path;
pub const PUBLIC_KEY_BYTES_LEN: usize = 96;
pub const SECRET_KEY_BYTES_LEN: usize = 32;
pub const BATCH_SIZE: usize = 1_000; // ~15MB
pub const KEYPAIR_BYTES_LEN: usize = PUBLIC_KEY_BYTES_LEN + SECRET_KEY_BYTES_LEN;
pub const BATCH_BYTE_LEN: usize = KEYPAIR_BYTES_LEN * BATCH_SIZE;
/// Defines a trait that allows reading/writing a vec of `Keypair` from/to a file.
pub trait KeypairsFile {
/// Write to file, without guaranteeing interoperability with other clients.
fn to_raw_file(&self, path: &Path, keypairs: &[Keypair]) -> Result<(), Error>;
/// Read from file, without guaranteeing interoperability with other clients.
fn from_raw_file(path: &Path, count: usize) -> Result<Vec<Keypair>, Error>;
}
impl KeypairsFile for Vec<Keypair> {
/// Write the keypairs to file, using the fastest possible method without guaranteeing
/// interoperability with other clients.
fn to_raw_file(&self, path: &Path, keypairs: &[Keypair]) -> Result<(), Error> {
let mut keypairs_file = File::create(path)?;
for keypair_batch in keypairs.chunks(BATCH_SIZE) {
let mut buf = Vec::with_capacity(BATCH_BYTE_LEN);
for keypair in keypair_batch {
buf.append(&mut keypair.sk.as_raw().as_bytes());
buf.append(&mut keypair.pk.clone().as_uncompressed_bytes());
}
keypairs_file.write_all(&buf)?;
}
Ok(())
}
/// Read the keypairs from file, using the fastest possible method without guaranteeing
/// interoperability with other clients.
fn from_raw_file(path: &Path, count: usize) -> Result<Vec<Keypair>, Error> {
let mut keypairs_file = File::open(path)?;
let mut keypairs = Vec::with_capacity(count);
let indices: Vec<usize> = (0..count).collect();
for batch in indices.chunks(BATCH_SIZE) {
let mut buf = vec![0; batch.len() * KEYPAIR_BYTES_LEN];
keypairs_file.read_exact(&mut buf)?;
let mut keypair_batch = batch
.par_iter()
.enumerate()
.map(|(i, _)| {
let sk_start = i * KEYPAIR_BYTES_LEN;
let sk_end = sk_start + SECRET_KEY_BYTES_LEN;
let sk = SecretKey::from_bytes(&buf[sk_start..sk_end])
.map_err(|_| Error::new(ErrorKind::Other, "Invalid SecretKey bytes"))
.unwrap();
let pk_start = sk_end;
let pk_end = pk_start + PUBLIC_KEY_BYTES_LEN;
let pk = PublicKey::from_uncompressed_bytes(&buf[pk_start..pk_end])
.map_err(|_| Error::new(ErrorKind::Other, "Invalid PublicKey bytes"))
.unwrap();
Keypair { sk, pk }
})
.collect();
keypairs.append(&mut keypair_batch);
}
Ok(keypairs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use std::fs::remove_file;
fn random_keypairs(n: usize) -> Vec<Keypair> {
(0..n).into_par_iter().map(|_| Keypair::random()).collect()
}
fn random_tmp_file() -> String {
let rng = thread_rng();
rng.sample_iter(&Alphanumeric).take(7).collect()
}
#[test]
#[ignore]
fn read_write_consistency_small_batch() {
let num_keypairs = 10;
let keypairs = random_keypairs(num_keypairs);
let keypairs_path = Path::new("/tmp").join(random_tmp_file());
keypairs.to_raw_file(&keypairs_path, &keypairs).unwrap();
let decoded = Vec::from_raw_file(&keypairs_path, num_keypairs).unwrap();
remove_file(keypairs_path).unwrap();
assert_eq!(keypairs, decoded);
}
#[test]
#[ignore]
fn read_write_consistency_big_batch() {
let num_keypairs = BATCH_SIZE + 1;
let keypairs = random_keypairs(num_keypairs);
let keypairs_path = Path::new("/tmp").join(random_tmp_file());
keypairs.to_raw_file(&keypairs_path, &keypairs).unwrap();
let decoded = Vec::from_raw_file(&keypairs_path, num_keypairs).unwrap();
remove_file(keypairs_path).unwrap();
assert_eq!(keypairs, decoded);
}
}

View File

@@ -0,0 +1,46 @@
#![cfg(test)]
#[macro_export]
macro_rules! ssz_and_tree_hash_tests {
($type: ty) => {
ssz_tests!($type);
tree_hash_tests!($type);
};
}
#[macro_export]
macro_rules! ssz_tests {
($type: ty) => {
#[test]
pub fn test_ssz_round_trip() {
use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
use ssz::{ssz_encode, Decode};
let mut rng = XorShiftRng::from_seed([42; 16]);
let original = <$type>::random_for_test(&mut rng);
let bytes = ssz_encode(&original);
println!("bytes length: {}", bytes.len());
let decoded = <$type>::from_ssz_bytes(&bytes).unwrap();
assert_eq!(original, decoded);
}
};
}
#[macro_export]
macro_rules! tree_hash_tests {
($type: ty) => {
#[test]
pub fn test_tree_hash_root() {
use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
use tree_hash::TreeHash;
let mut rng = XorShiftRng::from_seed([42; 16]);
let original = <$type>::random_for_test(&mut rng);
// Tree hashing should not panic.
original.tree_hash_root();
}
};
}

View File

@@ -0,0 +1,17 @@
#![allow(clippy::integer_arithmetic)]
#[macro_use]
mod macros;
mod builders;
mod generate_deterministic_keypairs;
mod keypairs_file;
mod test_random;
pub use builders::*;
pub use generate_deterministic_keypairs::generate_deterministic_keypair;
pub use generate_deterministic_keypairs::generate_deterministic_keypairs;
pub use generate_deterministic_keypairs::load_keypairs_from_yaml;
pub use keypairs_file::KeypairsFile;
pub use rand::{RngCore, SeedableRng};
pub use rand_xorshift::XorShiftRng;
pub use test_random::{test_random_instance, TestRandom};

View File

@@ -0,0 +1,112 @@
use crate::*;
use rand::RngCore;
use rand::SeedableRng;
use rand_xorshift::XorShiftRng;
use ssz_types::typenum::Unsigned;
mod address;
mod aggregate_signature;
mod bitfield;
mod hash256;
mod public_key;
mod public_key_bytes;
mod secret_key;
mod signature;
mod signature_bytes;
pub fn test_random_instance<T: TestRandom>() -> T {
let mut rng = XorShiftRng::from_seed([0x42; 16]);
T::random_for_test(&mut rng)
}
pub trait TestRandom {
fn random_for_test(rng: &mut impl RngCore) -> Self;
}
impl TestRandom for bool {
fn random_for_test(rng: &mut impl RngCore) -> Self {
(rng.next_u32() % 2) == 1
}
}
impl TestRandom for u64 {
fn random_for_test(rng: &mut impl RngCore) -> Self {
rng.next_u64()
}
}
impl TestRandom for u32 {
fn random_for_test(rng: &mut impl RngCore) -> Self {
rng.next_u32()
}
}
impl TestRandom for usize {
fn random_for_test(rng: &mut impl RngCore) -> Self {
rng.next_u32() as usize
}
}
impl<U> TestRandom for Vec<U>
where
U: TestRandom,
{
fn random_for_test(rng: &mut impl RngCore) -> Self {
let mut output = vec![];
for _ in 0..(usize::random_for_test(rng) % 4) {
output.push(<U>::random_for_test(rng));
}
output
}
}
impl<T, N: Unsigned> TestRandom for FixedVector<T, N>
where
T: TestRandom + Default,
{
fn random_for_test(rng: &mut impl RngCore) -> Self {
let mut output = vec![];
for _ in 0..(usize::random_for_test(rng) % std::cmp::min(4, N::to_usize())) {
output.push(<T>::random_for_test(rng));
}
output.into()
}
}
impl<T, N: Unsigned> TestRandom for VariableList<T, N>
where
T: TestRandom,
{
fn random_for_test(rng: &mut impl RngCore) -> Self {
let mut output = vec![];
if N::to_usize() != 0 {
for _ in 0..(usize::random_for_test(rng) % std::cmp::min(4, N::to_usize())) {
output.push(<T>::random_for_test(rng));
}
}
output.into()
}
}
macro_rules! impl_test_random_for_u8_array {
($len: expr) => {
impl TestRandom for [u8; $len] {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let mut bytes = [0; $len];
rng.fill_bytes(&mut bytes);
bytes
}
}
};
}
impl_test_random_for_u8_array!(4);
impl_test_random_for_u8_array!(32);
impl_test_random_for_u8_array!(48);
impl_test_random_for_u8_array!(96);

View File

@@ -0,0 +1,10 @@
use super::*;
use crate::Address;
impl TestRandom for Address {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let mut key_bytes = vec![0; 20];
rng.fill_bytes(&mut key_bytes);
Address::from_slice(&key_bytes[..])
}
}

View File

@@ -0,0 +1,11 @@
use super::*;
use bls::{AggregateSignature, Signature};
impl TestRandom for AggregateSignature {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let signature = Signature::random_for_test(rng);
let mut aggregate_signature = AggregateSignature::new();
aggregate_signature.add(&signature);
aggregate_signature
}
}

View File

@@ -0,0 +1,18 @@
use super::*;
use crate::{BitList, BitVector, Unsigned};
impl<N: Unsigned + Clone> TestRandom for BitList<N> {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let mut raw_bytes = vec![0; std::cmp::max(1, (N::to_usize() + 7) / 8)];
rng.fill_bytes(&mut raw_bytes);
Self::from_bytes(raw_bytes).expect("we generate a valid BitList")
}
}
impl<N: Unsigned + Clone> TestRandom for BitVector<N> {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let mut raw_bytes = vec![0; std::cmp::max(1, (N::to_usize() + 7) / 8)];
rng.fill_bytes(&mut raw_bytes);
Self::from_bytes(raw_bytes).expect("we generate a valid BitVector")
}
}

View File

@@ -0,0 +1,10 @@
use super::*;
use crate::Hash256;
impl TestRandom for Hash256 {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let mut key_bytes = vec![0; 32];
rng.fill_bytes(&mut key_bytes);
Hash256::from_slice(&key_bytes[..])
}
}

View File

@@ -0,0 +1,9 @@
use super::*;
use bls::{PublicKey, SecretKey};
impl TestRandom for PublicKey {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let secret_key = SecretKey::random_for_test(rng);
PublicKey::from_secret_key(&secret_key)
}
}

View File

@@ -0,0 +1,19 @@
use std::convert::From;
use bls::{PublicKeyBytes, BLS_PUBLIC_KEY_BYTE_SIZE};
use super::*;
impl TestRandom for PublicKeyBytes {
fn random_for_test(rng: &mut impl RngCore) -> Self {
//50-50 chance for signature to be "valid" or invalid
if bool::random_for_test(rng) {
//valid signature
PublicKeyBytes::from(PublicKey::random_for_test(rng))
} else {
//invalid signature, just random bytes
PublicKeyBytes::from_bytes(&<[u8; BLS_PUBLIC_KEY_BYTE_SIZE]>::random_for_test(rng))
.unwrap()
}
}
}

View File

@@ -0,0 +1,8 @@
use super::*;
use bls::SecretKey;
impl TestRandom for SecretKey {
fn random_for_test(_rng: &mut impl RngCore) -> Self {
SecretKey::random()
}
}

View File

@@ -0,0 +1,12 @@
use super::*;
use bls::{SecretKey, Signature};
impl TestRandom for Signature {
fn random_for_test(rng: &mut impl RngCore) -> Self {
let secret_key = SecretKey::random_for_test(rng);
let mut message = vec![0; 32];
rng.fill_bytes(&mut message);
Signature::new(&message, &secret_key)
}
}

View File

@@ -0,0 +1,17 @@
use bls::{SignatureBytes, BLS_SIG_BYTE_SIZE};
use super::*;
use std::convert::From;
impl TestRandom for SignatureBytes {
fn random_for_test(rng: &mut impl RngCore) -> Self {
//50-50 chance for signature to be "valid" or invalid
if bool::random_for_test(rng) {
//valid signature
SignatureBytes::from(Signature::random_for_test(rng))
} else {
//invalid signature, just random bytes
SignatureBytes::from_bytes(&<[u8; BLS_SIG_BYTE_SIZE]>::random_for_test(rng)).unwrap()
}
}
}

View File

@@ -0,0 +1,163 @@
//! This module contains custom implementations of `CachedTreeHash` for ETH2-specific types.
//!
//! It makes some assumptions about the layouts and update patterns of other structs in this
//! crate, and should be updated carefully whenever those structs are changed.
use crate::{Epoch, Hash256, PublicKeyBytes, Validator};
use cached_tree_hash::{int_log, CacheArena, CachedTreeHash, Error, TreeHashCache};
use int_to_bytes::int_to_fixed_bytes32;
use tree_hash::merkle_root;
/// Number of struct fields on `Validator`.
const NUM_VALIDATOR_FIELDS: usize = 8;
impl CachedTreeHash<TreeHashCache> for Validator {
fn new_tree_hash_cache(&self, arena: &mut CacheArena) -> TreeHashCache {
TreeHashCache::new(arena, int_log(NUM_VALIDATOR_FIELDS), NUM_VALIDATOR_FIELDS)
}
/// Efficiently tree hash a `Validator`, assuming it was updated by a valid state transition.
///
/// Specifically, we assume that the `pubkey` and `withdrawal_credentials` fields are constant.
fn recalculate_tree_hash_root(
&self,
arena: &mut CacheArena,
cache: &mut TreeHashCache,
) -> Result<Hash256, Error> {
// Otherwise just check the fields which might have changed.
let dirty_indices = cache
.leaves()
.iter_mut(arena)?
.enumerate()
.flat_map(|(i, leaf)| {
// Fields pubkey and withdrawal_credentials are constant
if (i == 0 || i == 1) && cache.initialized {
None
} else if process_field_by_index(self, i, leaf, !cache.initialized) {
Some(i)
} else {
None
}
})
.collect();
cache.update_merkle_root(arena, dirty_indices)
}
}
fn process_field_by_index(
v: &Validator,
field_idx: usize,
leaf: &mut Hash256,
force_update: bool,
) -> bool {
match field_idx {
0 => process_pubkey_bytes_field(&v.pubkey, leaf, force_update),
1 => process_slice_field(v.withdrawal_credentials.as_bytes(), leaf, force_update),
2 => process_u64_field(v.effective_balance, leaf, force_update),
3 => process_bool_field(v.slashed, leaf, force_update),
4 => process_epoch_field(v.activation_eligibility_epoch, leaf, force_update),
5 => process_epoch_field(v.activation_epoch, leaf, force_update),
6 => process_epoch_field(v.exit_epoch, leaf, force_update),
7 => process_epoch_field(v.withdrawable_epoch, leaf, force_update),
_ => panic!(
"Validator type only has {} fields, {} out of bounds",
NUM_VALIDATOR_FIELDS, field_idx
),
}
}
fn process_pubkey_bytes_field(
val: &PublicKeyBytes,
leaf: &mut Hash256,
force_update: bool,
) -> bool {
let new_tree_hash = merkle_root(val.as_slice(), 0);
process_slice_field(new_tree_hash.as_bytes(), leaf, force_update)
}
fn process_slice_field(new_tree_hash: &[u8], leaf: &mut Hash256, force_update: bool) -> bool {
if force_update || leaf.as_bytes() != new_tree_hash {
leaf.assign_from_slice(&new_tree_hash);
true
} else {
false
}
}
fn process_u64_field(val: u64, leaf: &mut Hash256, force_update: bool) -> bool {
let new_tree_hash = int_to_fixed_bytes32(val);
process_slice_field(&new_tree_hash[..], leaf, force_update)
}
fn process_epoch_field(val: Epoch, leaf: &mut Hash256, force_update: bool) -> bool {
process_u64_field(val.as_u64(), leaf, force_update)
}
fn process_bool_field(val: bool, leaf: &mut Hash256, force_update: bool) -> bool {
process_u64_field(val as u64, leaf, force_update)
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_utils::TestRandom;
use crate::Epoch;
use rand::SeedableRng;
use rand_xorshift::XorShiftRng;
use tree_hash::TreeHash;
fn test_validator_tree_hash(v: &Validator) {
let arena = &mut CacheArena::default();
let mut cache = v.new_tree_hash_cache(arena);
// With a fresh cache
assert_eq!(
&v.tree_hash_root()[..],
v.recalculate_tree_hash_root(arena, &mut cache)
.unwrap()
.as_bytes(),
"{:?}",
v
);
// With a completely up-to-date cache
assert_eq!(
&v.tree_hash_root()[..],
v.recalculate_tree_hash_root(arena, &mut cache)
.unwrap()
.as_bytes(),
"{:?}",
v
);
}
#[test]
fn default_validator() {
test_validator_tree_hash(&Validator::default());
}
#[test]
fn zeroed_validator() {
let mut v = Validator::default();
v.activation_eligibility_epoch = Epoch::from(0u64);
v.activation_epoch = Epoch::from(0u64);
test_validator_tree_hash(&v);
}
#[test]
fn random_validators() {
let mut rng = XorShiftRng::from_seed([0xf1; 16]);
let num_validators = 1000;
(0..num_validators)
.map(|_| Validator::random_for_test(&mut rng))
.for_each(|v| test_validator_tree_hash(&v));
}
#[test]
pub fn smallvec_size_check() {
// If this test fails we need to go and reassess the length of the `SmallVec` in
// `cached_tree_hash::TreeHashCache`. If the size of the `SmallVec` is too slow we're going
// to start doing heap allocations for each validator, this will fragment memory and slow
// us down.
assert!(NUM_VALIDATOR_FIELDS <= 8,);
}
}

View File

@@ -0,0 +1,3 @@
mod serde_utils;
pub use serde_utils::*;

View File

@@ -0,0 +1,132 @@
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
pub const FORK_BYTES_LEN: usize = 4;
pub const GRAFFITI_BYTES_LEN: usize = 32;
pub fn u8_from_hex_str<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
let start = match s.as_str().get(2..) {
Some(start) => start,
None => return Err(D::Error::custom("string length too small")),
};
u8::from_str_radix(&start, 16).map_err(D::Error::custom)
}
#[allow(clippy::trivially_copy_pass_by_ref)] // Serde requires the `byte` to be a ref.
pub fn u8_to_hex_str<S>(byte: &u8, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut hex: String = "0x".to_string();
hex.push_str(&hex::encode(&[*byte]));
serializer.serialize_str(&hex)
}
pub fn u32_from_hex_str<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
let start = s
.as_str()
.get(2..)
.ok_or_else(|| D::Error::custom("string length too small"))?;
u32::from_str_radix(&start, 16)
.map_err(D::Error::custom)
.map(u32::from_be)
}
#[allow(clippy::trivially_copy_pass_by_ref)] // Serde requires the `num` to be a ref.
pub fn u32_to_hex_str<S>(num: &u32, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut hex: String = "0x".to_string();
let bytes = num.to_le_bytes();
hex.push_str(&hex::encode(&bytes));
serializer.serialize_str(&hex)
}
pub fn fork_from_hex_str<'de, D>(deserializer: D) -> Result<[u8; FORK_BYTES_LEN], D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
let mut array = [0 as u8; FORK_BYTES_LEN];
let start = s
.as_str()
.get(2..)
.ok_or_else(|| D::Error::custom("string length too small"))?;
let decoded: Vec<u8> = hex::decode(&start).map_err(D::Error::custom)?;
if decoded.len() != FORK_BYTES_LEN {
return Err(D::Error::custom("Fork length too long"));
}
for (i, item) in array.iter_mut().enumerate() {
if i > decoded.len() {
break;
}
*item = decoded[i];
}
Ok(array)
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn fork_to_hex_str<S>(bytes: &[u8; FORK_BYTES_LEN], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut hex_string: String = "0x".to_string();
hex_string.push_str(&hex::encode(&bytes));
serializer.serialize_str(&hex_string)
}
pub fn graffiti_to_hex_str<S>(
bytes: &[u8; GRAFFITI_BYTES_LEN],
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut hex_string: String = "0x".to_string();
hex_string.push_str(&hex::encode(&bytes));
serializer.serialize_str(&hex_string)
}
pub fn graffiti_from_hex_str<'de, D>(deserializer: D) -> Result<[u8; GRAFFITI_BYTES_LEN], D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
let mut array = [0 as u8; GRAFFITI_BYTES_LEN];
let start = s
.as_str()
.get(2..)
.ok_or_else(|| D::Error::custom("string length too small"))?;
let decoded: Vec<u8> = hex::decode(&start).map_err(D::Error::custom)?;
if decoded.len() > GRAFFITI_BYTES_LEN {
return Err(D::Error::custom("Fork length too long"));
}
for (i, item) in array.iter_mut().enumerate() {
if i > decoded.len() {
break;
}
*item = decoded[i];
}
Ok(array)
}

View File

@@ -0,0 +1,144 @@
use crate::{
test_utils::TestRandom, BeaconState, ChainSpec, Epoch, EthSpec, Hash256, PublicKeyBytes,
};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// Information about a `BeaconChain` validator.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom, TreeHash)]
pub struct Validator {
pub pubkey: PublicKeyBytes,
pub withdrawal_credentials: Hash256,
pub effective_balance: u64,
pub slashed: bool,
pub activation_eligibility_epoch: Epoch,
pub activation_epoch: Epoch,
pub exit_epoch: Epoch,
pub withdrawable_epoch: Epoch,
}
impl Validator {
/// Returns `true` if the validator is considered active at some epoch.
pub fn is_active_at(&self, epoch: Epoch) -> bool {
self.activation_epoch <= epoch && epoch < self.exit_epoch
}
/// Returns `true` if the validator is slashable at some epoch.
pub fn is_slashable_at(&self, epoch: Epoch) -> bool {
!self.slashed && self.activation_epoch <= epoch && epoch < self.withdrawable_epoch
}
/// Returns `true` if the validator is considered exited at some epoch.
pub fn is_exited_at(&self, epoch: Epoch) -> bool {
self.exit_epoch <= epoch
}
/// Returns `true` if the validator is able to withdraw at some epoch.
pub fn is_withdrawable_at(&self, epoch: Epoch) -> bool {
epoch >= self.withdrawable_epoch
}
/// Returns `true` if the validator is eligible to join the activation queue.
///
/// Spec v0.11.1
pub fn is_eligible_for_activation_queue(&self, spec: &ChainSpec) -> bool {
self.activation_eligibility_epoch == spec.far_future_epoch
&& self.effective_balance == spec.max_effective_balance
}
/// Returns `true` if the validator is eligible to be activated.
///
/// Spec v0.11.1
pub fn is_eligible_for_activation<E: EthSpec>(
&self,
state: &BeaconState<E>,
spec: &ChainSpec,
) -> bool {
// Placement in queue is finalized
self.activation_eligibility_epoch <= state.finalized_checkpoint.epoch
// Has not yet been activated
&& self.activation_epoch == spec.far_future_epoch
}
}
impl Default for Validator {
/// Yields a "default" `Validator`. Primarily used for testing.
fn default() -> Self {
Self {
pubkey: PublicKeyBytes::empty(),
withdrawal_credentials: Hash256::default(),
activation_eligibility_epoch: Epoch::from(std::u64::MAX),
activation_epoch: Epoch::from(std::u64::MAX),
exit_epoch: Epoch::from(std::u64::MAX),
withdrawable_epoch: Epoch::from(std::u64::MAX),
slashed: false,
effective_balance: std::u64::MAX,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default() {
let v = Validator::default();
let epoch = Epoch::new(0);
assert_eq!(v.is_active_at(epoch), false);
assert_eq!(v.is_exited_at(epoch), false);
assert_eq!(v.is_withdrawable_at(epoch), false);
assert_eq!(v.slashed, false);
}
#[test]
fn is_active_at() {
let epoch = Epoch::new(10);
let v = Validator {
activation_epoch: epoch,
..Validator::default()
};
assert_eq!(v.is_active_at(epoch - 1), false);
assert_eq!(v.is_active_at(epoch), true);
assert_eq!(v.is_active_at(epoch + 1), true);
}
#[test]
fn is_exited_at() {
let epoch = Epoch::new(10);
let v = Validator {
exit_epoch: epoch,
..Validator::default()
};
assert_eq!(v.is_exited_at(epoch - 1), false);
assert_eq!(v.is_exited_at(epoch), true);
assert_eq!(v.is_exited_at(epoch + 1), true);
}
#[test]
fn is_withdrawable_at() {
let epoch = Epoch::new(10);
let v = Validator {
withdrawable_epoch: epoch,
..Validator::default()
};
assert_eq!(v.is_withdrawable_at(epoch - 1), false);
assert_eq!(v.is_withdrawable_at(epoch), true);
assert_eq!(v.is_withdrawable_at(epoch + 1), true);
}
ssz_and_tree_hash_tests!(Validator);
}

View File

@@ -0,0 +1,52 @@
use crate::{
test_utils::TestRandom, ChainSpec, Domain, Epoch, Fork, Hash256, SecretKey, Signature,
SignedRoot, SignedVoluntaryExit,
};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// An exit voluntarily submitted a validator who wishes to withdraw.
///
/// Spec v0.11.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
pub struct VoluntaryExit {
/// Earliest epoch when voluntary exit can be processed.
pub epoch: Epoch,
pub validator_index: u64,
}
impl SignedRoot for VoluntaryExit {}
impl VoluntaryExit {
pub fn sign(
self,
secret_key: &SecretKey,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> SignedVoluntaryExit {
let domain = spec.get_domain(
self.epoch,
Domain::VoluntaryExit,
fork,
genesis_validators_root,
);
let message = self.signing_root(domain);
let signature = Signature::new(message.as_bytes(), &secret_key);
SignedVoluntaryExit {
message: self,
signature,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(VoluntaryExit);
}