Altair consensus changes and refactors (#2279)

## Proposed Changes

Implement the consensus changes necessary for the upcoming Altair hard fork.

## Additional Info

This is quite a heavy refactor, with pivotal types like the `BeaconState` and `BeaconBlock` changing from structs to enums. This ripples through the whole codebase with field accesses changing to methods, e.g. `state.slot` => `state.slot()`.


Co-authored-by: realbigsean <seananderson33@gmail.com>
This commit is contained in:
Michael Sproul
2021-07-09 06:15:32 +00:00
parent 89361573d4
commit b4689e20c6
271 changed files with 9652 additions and 8444 deletions

View File

@@ -1,58 +1,71 @@
use crate::beacon_block_body::{
BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyRef, BeaconBlockBodyRefMut,
};
use crate::test_utils::TestRandom;
use crate::*;
use bls::Signature;
use serde_derive::{Deserialize, Serialize};
use ssz::{Decode, DecodeError};
use ssz_derive::{Decode, Encode};
use superstruct::superstruct;
use test_random_derive::TestRandom;
use tree_hash::TreeHash;
use tree_hash_derive::TreeHash;
/// A block of the `BeaconChain`.
///
/// Spec v0.12.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[superstruct(
variants(Base, Altair),
variant_attributes(
derive(
Debug,
PartialEq,
Clone,
Serialize,
Deserialize,
Encode,
Decode,
TreeHash,
TestRandom
),
serde(bound = "T: EthSpec", deny_unknown_fields),
cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))
),
ref_attributes(derive(Debug, PartialEq, TreeHash))
)]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, TreeHash)]
#[serde(untagged)]
#[serde(bound = "T: EthSpec")]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
pub struct BeaconBlock<T: EthSpec> {
#[superstruct(getter(copy))]
pub slot: Slot,
#[superstruct(getter(copy))]
#[serde(with = "serde_utils::quoted_u64")]
pub proposer_index: u64,
#[superstruct(getter(copy))]
pub parent_root: Hash256,
#[superstruct(getter(copy))]
pub state_root: Hash256,
pub body: BeaconBlockBody<T>,
#[superstruct(only(Base), partial_getter(rename = "body_base"))]
pub body: BeaconBlockBodyBase<T>,
#[superstruct(only(Altair), partial_getter(rename = "body_altair"))]
pub body: BeaconBlockBodyAltair<T>,
}
impl<T: EthSpec> SignedRoot for BeaconBlock<T> {}
impl<'a, T: EthSpec> SignedRoot for BeaconBlockRef<'a, T> {}
impl<T: EthSpec> BeaconBlock<T> {
/// Returns an empty block to be used during genesis.
///
/// Spec v0.12.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(),
eth1_data: Eth1Data {
deposit_root: Hash256::zero(),
block_hash: Hash256::zero(),
deposit_count: 0,
},
graffiti: Graffiti::default(),
proposer_slashings: VariableList::empty(),
attester_slashings: VariableList::empty(),
attestations: VariableList::empty(),
deposits: VariableList::empty(),
voluntary_exits: VariableList::empty(),
},
if spec.altair_fork_epoch == Some(T::genesis_epoch()) {
Self::Altair(BeaconBlockAltair::empty(spec))
} else {
Self::Base(BeaconBlockBase::empty(spec))
}
}
/// Return a block where the block has the max possible operations.
/// Return a block where the block has maximum size.
pub fn full(spec: &ChainSpec) -> BeaconBlock<T> {
let header = BeaconBlockHeader {
slot: Slot::new(1),
@@ -114,7 +127,8 @@ impl<T: EthSpec> BeaconBlock<T> {
signature: Signature::empty(),
};
let mut block: BeaconBlock<T> = BeaconBlock::empty(spec);
// FIXME(altair): use an Altair block (they're bigger)
let mut block = BeaconBlockBase::<T>::empty(spec);
for _ in 0..T::MaxProposerSlashings::to_usize() {
block
.body
@@ -143,19 +157,50 @@ impl<T: EthSpec> BeaconBlock<T> {
for _ in 0..T::MaxAttestations::to_usize() {
block.body.attestations.push(attestation.clone()).unwrap();
}
block
BeaconBlock::Base(block)
}
/// Returns the epoch corresponding to `self.slot`.
/// Custom SSZ decoder that takes a `ChainSpec` as context.
pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result<Self, ssz::DecodeError> {
let slot_len = <Slot as Decode>::ssz_fixed_len();
let slot_bytes = bytes
.get(0..slot_len)
.ok_or(DecodeError::InvalidByteLength {
len: bytes.len(),
expected: slot_len,
})?;
let slot = Slot::from_ssz_bytes(slot_bytes)?;
let epoch = slot.epoch(T::slots_per_epoch());
if spec
.altair_fork_epoch
.map_or(true, |altair_epoch| epoch < altair_epoch)
{
BeaconBlockBase::from_ssz_bytes(bytes).map(Self::Base)
} else {
BeaconBlockAltair::from_ssz_bytes(bytes).map(Self::Altair)
}
}
/// Convenience accessor for the `body` as a `BeaconBlockBodyRef`.
pub fn body(&self) -> BeaconBlockBodyRef<'_, T> {
self.to_ref().body()
}
/// Convenience accessor for the `body` as a `BeaconBlockBodyRefMut`.
pub fn body_mut(&mut self) -> BeaconBlockBodyRefMut<'_, T> {
self.to_mut().body_mut()
}
/// Returns the epoch corresponding to `self.slot()`.
pub fn epoch(&self) -> Epoch {
self.slot.epoch(T::slots_per_epoch())
self.slot().epoch(T::slots_per_epoch())
}
/// Returns the `tree_hash_root` of the block.
///
/// Spec v0.12.1
pub fn canonical_root(&self) -> Hash256 {
Hash256::from_slice(&self.tree_hash_root()[..])
self.tree_hash_root()
}
/// Returns a full `BeaconBlockHeader` of this block.
@@ -164,26 +209,18 @@ impl<T: EthSpec> BeaconBlock<T> {
/// when you want to have the block _and_ the header.
///
/// Note: performs a full tree-hash of `self.body`.
///
/// Spec v0.12.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()[..]),
}
self.to_ref().block_header()
}
/// Returns a "temporary" header, where the `state_root` is `Hash256::zero()`.
///
/// Spec v0.12.1
pub fn temporary_block_header(&self) -> BeaconBlockHeader {
BeaconBlockHeader {
state_root: Hash256::zero(),
..self.block_header()
}
self.to_ref().temporary_block_header()
}
/// Return the tree hash root of the block's body.
pub fn body_root(&self) -> Hash256 {
self.to_ref().body_root()
}
/// Signs `self`, producing a `SignedBeaconBlock`.
@@ -202,9 +239,106 @@ impl<T: EthSpec> BeaconBlock<T> {
);
let message = self.signing_root(domain);
let signature = secret_key.sign(message);
SignedBeaconBlock {
message: self,
signature,
SignedBeaconBlock::from_block(self, signature)
}
}
impl<'a, T: EthSpec> BeaconBlockRef<'a, T> {
/// Convenience accessor for the `body` as a `BeaconBlockBodyRef`.
pub fn body(&self) -> BeaconBlockBodyRef<'a, T> {
match self {
BeaconBlockRef::Base(block) => BeaconBlockBodyRef::Base(&block.body),
BeaconBlockRef::Altair(block) => BeaconBlockBodyRef::Altair(&block.body),
}
}
/// Return the tree hash root of the block's body.
pub fn body_root(&self) -> Hash256 {
match self {
BeaconBlockRef::Base(block) => block.body.tree_hash_root(),
BeaconBlockRef::Altair(block) => block.body.tree_hash_root(),
}
}
/// Returns a full `BeaconBlockHeader` of this block.
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: self.body_root(),
}
}
/// Returns a "temporary" header, where the `state_root` is `Hash256::zero()`.
pub fn temporary_block_header(self) -> BeaconBlockHeader {
BeaconBlockHeader {
state_root: Hash256::zero(),
..self.block_header()
}
}
}
impl<'a, T: EthSpec> BeaconBlockRefMut<'a, T> {
/// Convert a mutable reference to a beacon block to a mutable ref to its body.
pub fn body_mut(self) -> BeaconBlockBodyRefMut<'a, T> {
match self {
BeaconBlockRefMut::Base(block) => BeaconBlockBodyRefMut::Base(&mut block.body),
BeaconBlockRefMut::Altair(block) => BeaconBlockBodyRefMut::Altair(&mut block.body),
}
}
}
impl<T: EthSpec> BeaconBlockBase<T> {
/// Returns an empty block to be used during genesis.
pub fn empty(spec: &ChainSpec) -> Self {
BeaconBlockBase {
slot: spec.genesis_slot,
proposer_index: 0,
parent_root: Hash256::zero(),
state_root: Hash256::zero(),
body: BeaconBlockBodyBase {
randao_reveal: Signature::empty(),
eth1_data: Eth1Data {
deposit_root: Hash256::zero(),
block_hash: Hash256::zero(),
deposit_count: 0,
},
graffiti: Graffiti::default(),
proposer_slashings: VariableList::empty(),
attester_slashings: VariableList::empty(),
attestations: VariableList::empty(),
deposits: VariableList::empty(),
voluntary_exits: VariableList::empty(),
},
}
}
}
impl<T: EthSpec> BeaconBlockAltair<T> {
/// Returns an empty block to be used during genesis.
pub fn empty(spec: &ChainSpec) -> Self {
BeaconBlockAltair {
slot: spec.genesis_slot,
proposer_index: 0,
parent_root: Hash256::zero(),
state_root: Hash256::zero(),
body: BeaconBlockBodyAltair {
randao_reveal: Signature::empty(),
eth1_data: Eth1Data {
deposit_root: Hash256::zero(),
block_hash: Hash256::zero(),
deposit_count: 0,
},
graffiti: Graffiti::default(),
proposer_slashings: VariableList::empty(),
attester_slashings: VariableList::empty(),
attestations: VariableList::empty(),
deposits: VariableList::empty(),
voluntary_exits: VariableList::empty(),
sync_aggregate: SyncAggregate::empty(),
},
}
}
}
@@ -212,6 +346,110 @@ impl<T: EthSpec> BeaconBlock<T> {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{test_ssz_tree_hash_pair_with, SeedableRng, TestRandom, XorShiftRng};
use crate::{ForkName, MainnetEthSpec};
use ssz::Encode;
ssz_and_tree_hash_tests!(BeaconBlock<MainnetEthSpec>);
type BeaconBlock = super::BeaconBlock<MainnetEthSpec>;
type BeaconBlockBase = super::BeaconBlockBase<MainnetEthSpec>;
type BeaconBlockAltair = super::BeaconBlockAltair<MainnetEthSpec>;
#[test]
fn roundtrip_base_block() {
let rng = &mut XorShiftRng::from_seed([42; 16]);
let spec = &ForkName::Base.make_genesis_spec(MainnetEthSpec::default_spec());
let inner_block = BeaconBlockBase {
slot: Slot::random_for_test(rng),
proposer_index: u64::random_for_test(rng),
parent_root: Hash256::random_for_test(rng),
state_root: Hash256::random_for_test(rng),
body: BeaconBlockBodyBase::random_for_test(rng),
};
let block = BeaconBlock::Base(inner_block.clone());
test_ssz_tree_hash_pair_with(&block, &inner_block, |bytes| {
BeaconBlock::from_ssz_bytes(bytes, spec)
});
}
#[test]
fn roundtrip_altair_block() {
let rng = &mut XorShiftRng::from_seed([42; 16]);
let spec = &ForkName::Altair.make_genesis_spec(MainnetEthSpec::default_spec());
let inner_block = BeaconBlockAltair {
slot: Slot::random_for_test(rng),
proposer_index: u64::random_for_test(rng),
parent_root: Hash256::random_for_test(rng),
state_root: Hash256::random_for_test(rng),
body: BeaconBlockBodyAltair::random_for_test(rng),
};
let block = BeaconBlock::Altair(inner_block.clone());
test_ssz_tree_hash_pair_with(&block, &inner_block, |bytes| {
BeaconBlock::from_ssz_bytes(bytes, spec)
});
}
#[test]
fn decode_base_and_altair() {
type E = MainnetEthSpec;
let rng = &mut XorShiftRng::from_seed([42; 16]);
let fork_epoch = Epoch::from_ssz_bytes(&[7, 6, 5, 4, 3, 2, 1, 0]).unwrap();
let base_epoch = fork_epoch.saturating_sub(1_u64);
let base_slot = base_epoch.end_slot(E::slots_per_epoch());
let altair_epoch = fork_epoch;
let altair_slot = altair_epoch.start_slot(E::slots_per_epoch());
let mut spec = E::default_spec();
spec.altair_fork_epoch = Some(fork_epoch);
// BeaconBlockBase
{
let good_base_block = BeaconBlock::Base(BeaconBlockBase {
slot: base_slot,
..<_>::random_for_test(rng)
});
// It's invalid to have a base block with a slot higher than the fork epoch.
let bad_base_block = {
let mut bad = good_base_block.clone();
*bad.slot_mut() = altair_slot;
bad
};
assert_eq!(
BeaconBlock::from_ssz_bytes(&good_base_block.as_ssz_bytes(), &spec)
.expect("good base block can be decoded"),
good_base_block
);
BeaconBlock::from_ssz_bytes(&bad_base_block.as_ssz_bytes(), &spec)
.expect_err("bad base block cannot be decoded");
}
// BeaconBlockAltair
{
let good_altair_block = BeaconBlock::Altair(BeaconBlockAltair {
slot: altair_slot,
..<_>::random_for_test(rng)
});
// It's invalid to have an Altair block with a epoch lower than the fork epoch.
let bad_altair_block = {
let mut bad = good_altair_block.clone();
*bad.slot_mut() = base_slot;
bad
};
assert_eq!(
BeaconBlock::from_ssz_bytes(&good_altair_block.as_ssz_bytes(), &spec)
.expect("good altair block can be decoded"),
good_altair_block
);
BeaconBlock::from_ssz_bytes(&bad_altair_block.as_ssz_bytes(), &spec)
.expect_err("bad altair block cannot be decoded");
}
}
}

View File

@@ -1,18 +1,37 @@
use crate::test_utils::TestRandom;
use crate::*;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use ssz_types::VariableList;
use superstruct::superstruct;
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// The body of a `BeaconChain` block, containing operations.
///
/// Spec v0.12.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
/// This *superstruct* abstracts over the hard-fork.
#[superstruct(
variants(Base, Altair),
variant_attributes(
derive(
Debug,
PartialEq,
Clone,
Serialize,
Deserialize,
Encode,
Decode,
TreeHash,
TestRandom
),
serde(bound = "T: EthSpec", deny_unknown_fields),
cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))
)
)]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[serde(bound = "T: EthSpec")]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
pub struct BeaconBlockBody<T: EthSpec> {
pub randao_reveal: Signature,
pub eth1_data: Eth1Data,
@@ -22,11 +41,18 @@ pub struct BeaconBlockBody<T: EthSpec> {
pub attestations: VariableList<Attestation<T>, T::MaxAttestations>,
pub deposits: VariableList<Deposit, T::MaxDeposits>,
pub voluntary_exits: VariableList<SignedVoluntaryExit, T::MaxVoluntaryExits>,
#[superstruct(only(Altair))]
pub sync_aggregate: SyncAggregate<T>,
}
#[cfg(test)]
mod tests {
use super::*;
ssz_and_tree_hash_tests!(BeaconBlockBody<MainnetEthSpec>);
mod base {
use super::super::*;
ssz_and_tree_hash_tests!(BeaconBlockBodyBase<MainnetEthSpec>);
}
mod altair {
use super::super::*;
ssz_and_tree_hash_tests!(BeaconBlockBodyAltair<MainnetEthSpec>);
}
}

View File

@@ -33,19 +33,6 @@ impl BeaconBlockHeader {
Hash256::from_slice(&self.tree_hash_root()[..])
}
/// Given a `body`, consumes `self` and returns a complete `BeaconBlock`.
///
/// Spec v0.12.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,

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@ impl CommitteeCache {
return Err(Error::ZeroSlotsPerEpoch);
}
let active_validator_indices = get_active_validator_indices(&state.validators, epoch);
let active_validator_indices = get_active_validator_indices(state.validators(), epoch);
if active_validator_indices.is_empty() {
return Err(Error::InsufficientValidators);
@@ -59,13 +59,15 @@ impl CommitteeCache {
.ok_or(Error::UnableToShuffle)?;
// The use of `NonZeroUsize` reduces the maximum number of possible validators by one.
if state.validators.len() == usize::max_value() {
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);
let mut shuffling_positions = vec![None; state.validators().len()];
for (i, &v) in shuffling.iter().enumerate() {
*shuffling_positions
.get_mut(v)
.ok_or(Error::ShuffleIndexOutOfBounds(v))? = NonZeroUsize::new(i + 1);
}
Ok(CommitteeCache {
@@ -229,7 +231,7 @@ impl CommitteeCache {
///
/// Spec v0.12.1
fn compute_committee(&self, index: usize) -> Option<&[usize]> {
Some(&self.shuffling[self.compute_committee_range(index)?])
self.shuffling.get(self.compute_committee_range(index)?)
}
/// Returns a range of `self.shuffling` that represents the `index`'th committee in the epoch.
@@ -255,7 +257,7 @@ impl CommitteeCache {
/// 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> {
pub fn shuffled_position(&self, validator_index: usize) -> Option<usize> {
self.shuffling_positions
.get(validator_index)?
.and_then(|p| Some(p.get() - 1))

View File

@@ -1,6 +1,27 @@
#![cfg(test)]
use super::*;
use crate::{test_utils::*, *};
use crate::test_utils::*;
use beacon_chain::store::StoreConfig;
use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType};
use beacon_chain::types::*;
use swap_or_not_shuffle::shuffle_list;
pub const VALIDATOR_COUNT: usize = 16;
lazy_static! {
/// A cached set of keys.
static ref KEYPAIRS: Vec<Keypair> = generate_deterministic_keypairs(VALIDATOR_COUNT);
}
fn get_harness<E: EthSpec>(validator_count: usize) -> BeaconChainHarness<EphemeralHarnessType<E>> {
let harness = BeaconChainHarness::new_with_store_config(
E::default(),
None,
KEYPAIRS[0..validator_count].to_vec(),
StoreConfig::default(),
);
harness.advance_slot();
harness
}
#[test]
fn default_values() {
@@ -16,27 +37,26 @@ fn default_values() {
}
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
let harness = get_harness(validator_count);
let head_state = harness.get_current_state();
if slot > Slot::new(0) {
harness.add_attested_blocks_at_slots(
head_state,
Hash256::zero(),
(1..slot.as_u64())
.map(Slot::new)
.collect::<Vec<_>>()
.as_slice(),
(0..validator_count).collect::<Vec<_>>().as_slice(),
);
}
harness.get_current_state()
}
#[test]
#[should_panic]
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)
);
new_state::<MinimalEthSpec>(0, Slot::new(0));
}
#[test]
@@ -45,24 +65,22 @@ fn initializes_with_the_right_epoch() {
let spec = &MinimalEthSpec::default_spec();
let cache = CommitteeCache::default();
assert_eq!(cache.initialized_epoch, None);
assert!(!cache.is_initialized_at(state.current_epoch()));
let cache = CommitteeCache::initialized(&state, state.current_epoch(), &spec).unwrap();
assert_eq!(cache.initialized_epoch, Some(state.current_epoch()));
assert!(cache.is_initialized_at(state.current_epoch()));
let cache = CommitteeCache::initialized(&state, state.previous_epoch(), &spec).unwrap();
assert_eq!(cache.initialized_epoch, Some(state.previous_epoch()));
assert!(cache.is_initialized_at(state.previous_epoch()));
let cache = CommitteeCache::initialized(&state, state.next_epoch().unwrap(), &spec).unwrap();
assert_eq!(cache.initialized_epoch, Some(state.next_epoch().unwrap()));
assert!(cache.is_initialized_at(state.next_epoch().unwrap()));
}
#[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 epoch = Epoch::new(6);
let slot = epoch.start_slot(MinimalEthSpec::slots_per_epoch());
let mut state = new_state::<MinimalEthSpec>(num_validators, slot);
@@ -72,7 +90,7 @@ fn shuffles_for_the_right_epoch() {
.map(|i| Hash256::from_low_u64_be(i as u64))
.collect();
state.randao_mixes = FixedVector::from(distinct_hashes);
*state.randao_mixes_mut() = FixedVector::from(distinct_hashes);
let previous_seed = state
.get_seed(state.previous_epoch(), Domain::BeaconAttester, spec)
@@ -97,9 +115,9 @@ fn shuffles_for_the_right_epoch() {
};
let assert_shuffling_positions_accurate = |cache: &CommitteeCache| {
for (i, v) in cache.shuffling.iter().enumerate() {
for (i, v) in cache.shuffling().iter().enumerate() {
assert_eq!(
cache.shuffling_positions[*v].unwrap().get() - 1,
cache.shuffled_position(*v).unwrap(),
i,
"Shuffling position inaccurate"
);
@@ -107,14 +125,14 @@ fn shuffles_for_the_right_epoch() {
};
let cache = CommitteeCache::initialized(&state, state.current_epoch(), spec).unwrap();
assert_eq!(cache.shuffling, shuffling_with_seed(current_seed));
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_eq!(cache.shuffling(), shuffling_with_seed(previous_seed));
assert_shuffling_positions_accurate(&cache);
let cache = CommitteeCache::initialized(&state, state.next_epoch().unwrap(), spec).unwrap();
assert_eq!(cache.shuffling, shuffling_with_seed(next_seed));
assert_eq!(cache.shuffling(), shuffling_with_seed(next_seed));
assert_shuffling_positions_accurate(&cache);
}

View File

@@ -11,22 +11,18 @@ pub struct ExitCache {
}
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;
/// Initialize a new cache for the given list of validators.
pub fn new(validators: &[Validator], spec: &ChainSpec) -> Result<Self, BeaconStateError> {
let mut exit_cache = ExitCache {
initialized: true,
..ExitCache::default()
};
// 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))
.try_for_each(|validator| exit_cache.record_validator_exit(validator.exit_epoch))?;
Ok(exit_cache)
}
/// Check that the cache is initialized and return an error if it is not.

View File

@@ -4,7 +4,7 @@ use crate::*;
///
/// The iterator has the following characteristics:
///
/// - Will only return *at most* `state.block_roots.len()` entries.
/// - Will only return *at most* `state.block_roots().len()` entries.
/// - Will not return slots prior to the genesis_slot.
/// - Each call to next will result in a slot one less than the prior one (or `None`).
/// - Skipped slots will contain the block root from the prior non-skipped slot.
@@ -22,7 +22,7 @@ impl<'a, T: EthSpec> BlockRootsIter<'a, T> {
Self {
state,
genesis_slot,
prev: state.slot,
prev: state.slot(),
}
}
}
@@ -35,8 +35,8 @@ impl<'a, T: EthSpec> Iterator for BlockRootsIter<'a, T> {
&& self.prev
> self
.state
.slot
.saturating_sub(self.state.block_roots.len() as u64)
.slot()
.saturating_sub(self.state.block_roots().len() as u64)
{
self.prev = self.prev.saturating_sub(1_u64);
Some(
@@ -73,12 +73,13 @@ mod test {
let mut state: BeaconState<E> = BeaconState::new(0, <_>::default(), &spec);
for i in 0..state.block_roots.len() {
state.block_roots[i] = root_slot(i).1;
for i in 0..state.block_roots().len() {
state.block_roots_mut()[i] = root_slot(i).1;
}
assert_eq!(
state.slot, spec.genesis_slot,
state.slot(),
spec.genesis_slot,
"test assume a genesis slot state"
);
assert_eq!(
@@ -87,22 +88,22 @@ mod test {
"state at genesis slot has no history"
);
state.slot = Slot::new(1);
*state.slot_mut() = Slot::new(1);
assert_eq!(
all_roots(&state, &spec),
vec![root_slot(0)],
"first slot after genesis has one slot history"
);
state.slot = Slot::new(2);
*state.slot_mut() = Slot::new(2);
assert_eq!(
all_roots(&state, &spec),
vec![root_slot(1), root_slot(0)],
"second slot after genesis has two slot history"
);
state.slot = Slot::from(state.block_roots.len() + 2);
let expected = (2..state.block_roots.len() + 2)
*state.slot_mut() = Slot::from(state.block_roots().len() + 2);
let expected = (2..state.block_roots().len() + 2)
.rev()
.map(|i| (Slot::from(i), *state.get_block_root(Slot::from(i)).unwrap()))
.collect::<Vec<_>>();
@@ -120,12 +121,13 @@ mod test {
let mut state: BeaconState<E> = BeaconState::new(0, <_>::default(), &spec);
for i in 0..state.block_roots.len() {
state.block_roots[i] = root_slot(i).1;
for i in 0..state.block_roots().len() {
state.block_roots_mut()[i] = root_slot(i).1;
}
assert_eq!(
state.slot, spec.genesis_slot,
state.slot(),
spec.genesis_slot,
"test assume a genesis slot state"
);
assert_eq!(
@@ -134,14 +136,14 @@ mod test {
"state at genesis slot has no history"
);
state.slot = Slot::new(5);
*state.slot_mut() = Slot::new(5);
assert_eq!(
all_roots(&state, &spec),
vec![root_slot(4)],
"first slot after genesis has one slot history"
);
state.slot = Slot::new(6);
*state.slot_mut() = Slot::new(6);
assert_eq!(
all_roots(&state, &spec),
vec![root_slot(5), root_slot(4)],

View File

@@ -1,23 +1,60 @@
#![cfg(test)]
use super::*;
use crate::test_utils::*;
use beacon_chain::store::config::StoreConfig;
use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType};
use beacon_chain::types::{
test_utils::TestRandom, BeaconState, BeaconStateAltair, BeaconStateBase, BeaconStateError,
ChainSpec, CloneConfig, Domain, Epoch, EthSpec, FixedVector, Hash256, Keypair, MainnetEthSpec,
MinimalEthSpec, RelativeEpoch, Slot,
};
use ssz::{Decode, Encode};
use std::ops::Mul;
use swap_or_not_shuffle::compute_shuffled_index;
ssz_and_tree_hash_tests!(FoundationBeaconState);
pub const MAX_VALIDATOR_COUNT: usize = 129;
pub const SLOT_OFFSET: Slot = Slot::new(1);
lazy_static! {
/// A cached set of keys.
static ref KEYPAIRS: Vec<Keypair> = generate_deterministic_keypairs(MAX_VALIDATOR_COUNT);
}
fn get_harness<E: EthSpec>(
validator_count: usize,
slot: Slot,
) -> BeaconChainHarness<EphemeralHarnessType<E>> {
let harness = BeaconChainHarness::new_with_store_config(
E::default(),
None,
KEYPAIRS[0..validator_count].to_vec(),
StoreConfig::default(),
);
let skip_to_slot = slot - SLOT_OFFSET;
if skip_to_slot > Slot::new(0) {
let slots = (skip_to_slot.as_u64()..=slot.as_u64())
.map(Slot::new)
.collect::<Vec<_>>();
let state = harness.get_current_state();
harness.add_attested_blocks_at_slots(
state,
Hash256::zero(),
slots.as_slice(),
(0..validator_count).collect::<Vec<_>>().as_slice(),
);
}
harness
}
fn build_state<E: EthSpec>(validator_count: usize) -> BeaconState<E> {
get_harness(validator_count, Slot::new(0))
.chain
.head_beacon_state()
.unwrap()
}
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_deterministic_keypairs(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, spec: &ChainSpec| {
@@ -56,9 +93,9 @@ fn test_beacon_proposer_index<T: EthSpec>() {
}
// Test with two validators per slot, first validator has zero balance.
let mut state = build_state((T::slots_per_epoch() as usize).mul(2));
let mut state = build_state::<T>((T::slots_per_epoch() as usize).mul(2));
let slot0_candidate0 = ith_candidate(&state, Slot::new(0), 0, &spec);
state.validators[slot0_candidate0].effective_balance = 0;
state.validators_mut()[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);
@@ -81,17 +118,9 @@ fn test_cache_initialization<T: EthSpec>(
spec: &ChainSpec,
) {
let slot = relative_epoch
.into_epoch(state.slot.epoch(T::slots_per_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();
@@ -99,7 +128,7 @@ fn test_cache_initialization<T: EthSpec>(
state.get_beacon_committee(slot, 0).unwrap();
// Drop the cache.
state.drop_committee_cache(relative_epoch);
state.drop_committee_cache(relative_epoch).unwrap();
// Assert a call to a cache-using function fail.
assert_eq!(
@@ -114,11 +143,9 @@ fn test_cache_initialization<T: EthSpec>(
fn cache_initialization() {
let spec = MinimalEthSpec::default_spec();
let builder: TestingBeaconStateBuilder<MinimalEthSpec> =
TestingBeaconStateBuilder::from_deterministic_keypairs(16, &spec);
let (mut state, _keypairs) = builder.build();
let mut state = build_state::<MinimalEthSpec>(16);
state.slot =
*state.slot_mut() =
(MinimalEthSpec::genesis_epoch() + 1).start_slot(MinimalEthSpec::slots_per_epoch());
test_cache_initialization(&mut state, RelativeEpoch::Previous, &spec);
@@ -150,25 +177,29 @@ fn test_clone_config<E: EthSpec>(base_state: &BeaconState<E>, clone_config: Clon
.expect_err("shouldn't exist");
}
if clone_config.pubkey_cache {
assert_ne!(state.pubkey_cache.len(), 0);
assert_ne!(state.pubkey_cache().len(), 0);
} else {
assert_eq!(state.pubkey_cache.len(), 0);
assert_eq!(state.pubkey_cache().len(), 0);
}
if clone_config.exit_cache {
state
.exit_cache
.exit_cache()
.check_initialized()
.expect("exit cache exists");
} else {
state
.exit_cache
.exit_cache()
.check_initialized()
.expect_err("exit cache doesn't exist");
}
if clone_config.tree_hash_cache {
assert!(state.tree_hash_cache.is_some());
assert!(state.tree_hash_cache().is_initialized());
} else {
assert!(state.tree_hash_cache.is_none(), "{:?}", clone_config);
assert!(
!state.tree_hash_cache().is_initialized(),
"{:?}",
clone_config
);
}
}
@@ -176,9 +207,7 @@ fn test_clone_config<E: EthSpec>(base_state: &BeaconState<E>, clone_config: Clon
fn clone_config() {
let spec = MinimalEthSpec::default_spec();
let builder: TestingBeaconStateBuilder<MinimalEthSpec> =
TestingBeaconStateBuilder::from_deterministic_keypairs(16, &spec);
let (mut state, _keypairs) = builder.build();
let mut state = build_state::<MinimalEthSpec>(16);
state.build_all_caches(&spec).unwrap();
state
@@ -198,69 +227,10 @@ fn clone_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()[..]);
/*
* A cache should hash twice without updating the slot.
*/
assert_eq!(
state.update_tree_hash_cache().unwrap(),
root,
"tree hash result should be identical on the same slot"
);
/*
* A cache should not hash after updating the slot but not updating the state roots.
*/
// The tree hash cache needs to be rebuilt since it was dropped when it failed.
state
.update_tree_hash_cache()
.expect("should rebuild cache");
state.slot += 1;
assert_eq!(
state.update_tree_hash_cache(),
Err(BeaconStateError::NonLinearTreeHashCacheHistory),
"should not build hash without updating the state root"
);
/*
* The cache should update if the slot and state root are updated.
*/
// The tree hash cache needs to be rebuilt since it was dropped when it failed.
let root = state
.update_tree_hash_cache()
.expect("should rebuild cache");
state.slot += 1;
state
.set_state_root(state.slot - 1, root)
.expect("should set state root");
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 std::ops::{Add, Div};
use swap_or_not_shuffle::shuffle_list;
@@ -343,35 +313,33 @@ mod committees {
) {
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 harness = get_harness::<T>(validator_count, slot);
let mut new_head_state = harness.get_current_state();
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);
*new_head_state.randao_mixes_mut() = FixedVector::from(distinct_hashes);
state
.build_committee_cache(RelativeEpoch::Previous, spec)
new_head_state
.force_build_committee_cache(RelativeEpoch::Previous, spec)
.unwrap();
state
.build_committee_cache(RelativeEpoch::Current, spec)
new_head_state
.force_build_committee_cache(RelativeEpoch::Current, spec)
.unwrap();
state
.build_committee_cache(RelativeEpoch::Next, spec)
new_head_state
.force_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);
execute_committee_consistency_test(
new_head_state,
cache_epoch,
validator_count as usize,
&spec,
);
}
fn committee_consistency_test_suite<T: EthSpec>(cached_epoch: RelativeEpoch) {
@@ -419,16 +387,12 @@ mod committees {
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_deterministic_keypairs(16, &spec);
let (state, _keypairs) = builder.build();
state
get_harness(16, Slot::new(0))
.chain
.head_beacon_state()
.unwrap()
}
#[test]
@@ -436,8 +400,8 @@ mod get_outstanding_deposit_len {
let mut state = state();
assert_eq!(state.get_outstanding_deposit_len(), Ok(0));
state.eth1_data.deposit_count = 17;
state.eth1_deposit_index = 16;
state.eth1_data_mut().deposit_count = 17;
*state.eth1_deposit_index_mut() = 16;
assert_eq!(state.get_outstanding_deposit_len(), Ok(1));
}
@@ -445,8 +409,8 @@ mod get_outstanding_deposit_len {
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;
state.eth1_data_mut().deposit_count = 16;
*state.eth1_deposit_index_mut() = 17;
assert_eq!(
state.get_outstanding_deposit_len(),
@@ -457,3 +421,124 @@ mod get_outstanding_deposit_len {
);
}
}
#[test]
fn decode_base_and_altair() {
type E = MainnetEthSpec;
let rng = &mut XorShiftRng::from_seed([42; 16]);
let fork_epoch = Epoch::from_ssz_bytes(&[7, 6, 5, 4, 3, 2, 1, 0]).unwrap();
let base_epoch = fork_epoch.saturating_sub(1_u64);
let base_slot = base_epoch.end_slot(E::slots_per_epoch());
let altair_epoch = fork_epoch;
let altair_slot = altair_epoch.start_slot(E::slots_per_epoch());
let mut spec = E::default_spec();
spec.altair_fork_epoch = Some(altair_epoch);
// BeaconStateBase
{
let good_base_block: BeaconState<MainnetEthSpec> = BeaconState::Base(BeaconStateBase {
slot: base_slot,
..<_>::random_for_test(rng)
});
// It's invalid to have a base block with a slot higher than the fork slot.
let bad_base_block = {
let mut bad = good_base_block.clone();
*bad.slot_mut() = altair_slot;
bad
};
assert_eq!(
BeaconState::from_ssz_bytes(&good_base_block.as_ssz_bytes(), &spec)
.expect("good base block can be decoded"),
good_base_block
);
<BeaconState<MainnetEthSpec>>::from_ssz_bytes(&bad_base_block.as_ssz_bytes(), &spec)
.expect_err("bad base block cannot be decoded");
}
// BeaconStateAltair
{
let good_altair_block: BeaconState<MainnetEthSpec> =
BeaconState::Altair(BeaconStateAltair {
slot: altair_slot,
..<_>::random_for_test(rng)
});
// It's invalid to have an Altair block with a slot lower than the fork slot.
let bad_altair_block = {
let mut bad = good_altair_block.clone();
*bad.slot_mut() = base_slot;
bad
};
assert_eq!(
BeaconState::from_ssz_bytes(&good_altair_block.as_ssz_bytes(), &spec)
.expect("good altair block can be decoded"),
good_altair_block
);
<BeaconState<MainnetEthSpec>>::from_ssz_bytes(&bad_altair_block.as_ssz_bytes(), &spec)
.expect_err("bad altair block cannot be decoded");
}
}
#[test]
fn tree_hash_cache_linear_history() {
use crate::test_utils::{SeedableRng, XorShiftRng};
use tree_hash::TreeHash;
let mut rng = XorShiftRng::from_seed([42; 16]);
let mut state: BeaconState<MainnetEthSpec> =
BeaconState::Base(BeaconStateBase::random_for_test(&mut rng));
let root = state.update_tree_hash_cache().unwrap();
assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]);
/*
* A cache should hash twice without updating the slot.
*/
assert_eq!(
state.update_tree_hash_cache().unwrap(),
root,
"tree hash result should be identical on the same slot"
);
/*
* A cache should not hash after updating the slot but not updating the state roots.
*/
// The tree hash cache needs to be rebuilt since it was dropped when it failed.
state
.update_tree_hash_cache()
.expect("should rebuild cache");
*state.slot_mut() += 1;
assert_eq!(
state.update_tree_hash_cache(),
Err(BeaconStateError::NonLinearTreeHashCacheHistory),
"should not build hash without updating the state root"
);
/*
* The cache should update if the slot and state root are updated.
*/
// The tree hash cache needs to be rebuilt since it was dropped when it failed.
let root = state
.update_tree_hash_cache()
.expect("should rebuild cache");
*state.slot_mut() += 1;
state
.set_state_root(state.slot() - 1, root)
.expect("should set state root");
let root = state.update_tree_hash_cache().unwrap();
assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]);
}

View File

@@ -1,5 +1,6 @@
#![allow(clippy::integer_arithmetic)]
#![allow(clippy::disallowed_method)]
#![allow(clippy::indexing_slicing)]
use super::Error;
use crate::{BeaconState, EthSpec, Hash256, Slot, Unsigned, Validator};
@@ -11,8 +12,13 @@ use std::cmp::Ordering;
use std::iter::ExactSizeIterator;
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 leaves (including padding) on the `BeaconState` Merkle tree.
///
/// ## Note
///
/// This constant is set with the assumption that there are `> 16` and `<= 32` fields on the
/// `BeaconState`. **Tree hashing will fail if this value is set incorrectly.**
const NUM_BEACON_STATE_HASH_TREE_ROOT_LEAVES: usize = 32;
/// The number of nodes in the Merkle tree of a validator record.
const NODES_PER_VALIDATOR: usize = 15;
@@ -41,7 +47,7 @@ impl<T: EthSpec> Eth1DataVotesTreeHashCache<T> {
pub fn new(state: &BeaconState<T>) -> Self {
let mut arena = CacheArena::default();
let roots: VariableList<_, _> = state
.eth1_data_votes
.eth1_data_votes()
.iter()
.map(|eth1_data| eth1_data.tree_hash_root())
.collect::<Vec<_>>()
@@ -51,7 +57,7 @@ impl<T: EthSpec> Eth1DataVotesTreeHashCache<T> {
Self {
arena,
tree_hash_cache,
voting_period: Self::voting_period(state.slot),
voting_period: Self::voting_period(state.slot()),
roots,
}
}
@@ -61,14 +67,14 @@ impl<T: EthSpec> Eth1DataVotesTreeHashCache<T> {
}
pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState<T>) -> Result<Hash256, Error> {
if state.eth1_data_votes.len() < self.roots.len()
|| Self::voting_period(state.slot) != self.voting_period
if state.eth1_data_votes().len() < self.roots.len()
|| Self::voting_period(state.slot()) != self.voting_period
{
*self = Self::new(state);
}
state
.eth1_data_votes
.eth1_data_votes()
.iter()
.skip(self.roots.len())
.try_for_each(|eth1_data| self.roots.push(eth1_data.tree_hash_root()))?;
@@ -80,8 +86,42 @@ impl<T: EthSpec> Eth1DataVotesTreeHashCache<T> {
}
/// A cache that performs a caching tree hash of the entire `BeaconState` struct.
#[derive(Debug, PartialEq, Clone, Encode, Decode)]
///
/// This type is a wrapper around the inner cache, which does all the work.
#[derive(Debug, Default, PartialEq, Clone)]
pub struct BeaconTreeHashCache<T: EthSpec> {
inner: Option<BeaconTreeHashCacheInner<T>>,
}
impl<T: EthSpec> BeaconTreeHashCache<T> {
pub fn new(state: &BeaconState<T>) -> Self {
Self {
inner: Some(BeaconTreeHashCacheInner::new(state)),
}
}
pub fn is_initialized(&self) -> bool {
self.inner.is_some()
}
/// Move the inner cache out so that the containing `BeaconState` can be borrowed.
pub fn take(&mut self) -> Option<BeaconTreeHashCacheInner<T>> {
self.inner.take()
}
/// Restore the inner cache after using `take`.
pub fn restore(&mut self, inner: BeaconTreeHashCacheInner<T>) {
self.inner = Some(inner);
}
/// Make the cache empty.
pub fn uninitialize(&mut self) {
self.inner = None;
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct BeaconTreeHashCacheInner<T: EthSpec> {
/// Tracks the previously generated state root to ensure the next state root provided descends
/// directly from this state.
previous_state: Option<(Hash256, Slot)>,
@@ -101,25 +141,27 @@ pub struct BeaconTreeHashCache<T: EthSpec> {
eth1_data_votes: Eth1DataVotesTreeHashCache<T>,
}
impl<T: EthSpec> BeaconTreeHashCache<T> {
impl<T: EthSpec> BeaconTreeHashCacheInner<T> {
/// Instantiates a new cache.
///
/// Allocates the necessary memory to store all of the cached Merkle trees. Only the leaves are
/// hashed, leaving the internal nodes as all-zeros.
pub fn new(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 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 validators = ValidatorsListTreeHashCache::new::<T>(state.validators());
let mut balances_arena = CacheArena::default();
let balances = state.balances.new_tree_hash_cache(&mut balances_arena);
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);
let slashings = state.slashings().new_tree_hash_cache(&mut slashings_arena);
Self {
previous_state: None,
@@ -150,100 +192,132 @@ impl<T: EthSpec> BeaconTreeHashCache<T> {
// efficient algorithm.
if let Some((previous_root, previous_slot)) = self.previous_state {
// The previously-hashed state must not be newer than `state`.
if previous_slot > state.slot {
if previous_slot > state.slot() {
return Err(Error::TreeHashCacheSkippedSlot {
cache: previous_slot,
state: state.slot,
state: state.slot(),
});
}
// If the state is newer, the previous root must be in the history of the given state.
if previous_slot < state.slot && *state.get_state_root(previous_slot)? != previous_root
if previous_slot < state.slot()
&& *state.get_state_root(previous_slot)? != previous_root
{
return Err(Error::NonLinearTreeHashCacheHistory);
}
}
let mut hasher = MerkleHasher::with_leaves(NUM_BEACON_STATE_HASHING_FIELDS);
let mut hasher = MerkleHasher::with_leaves(NUM_BEACON_STATE_HASH_TREE_ROOT_LEAVES);
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.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
.block_roots()
.recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.block_roots)?
.as_bytes(),
)?;
hasher.write(
state
.state_roots
.state_roots()
.recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.state_roots)?
.as_bytes(),
)?;
hasher.write(
state
.historical_roots
.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().tree_hash_root().as_bytes())?;
hasher.write(
self.eth1_data_votes
.recalculate_tree_hash_root(&state)?
.as_bytes(),
)?;
hasher.write(state.eth1_deposit_index.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[..])?
.recalculate_tree_hash_root(state.validators())?
.as_bytes(),
)?;
hasher.write(
state
.balances
.balances()
.recalculate_tree_hash_root(&mut self.balances_arena, &mut self.balances)?
.as_bytes(),
)?;
hasher.write(
state
.randao_mixes
.randao_mixes()
.recalculate_tree_hash_root(&mut self.fixed_arena, &mut self.randao_mixes)?
.as_bytes(),
)?;
hasher.write(
state
.slashings
.slashings()
.recalculate_tree_hash_root(&mut self.slashings_arena, &mut self.slashings)?
.as_bytes(),
)?;
// Participation
match state {
BeaconState::Base(state) => {
hasher.write(
state
.previous_epoch_attestations
.tree_hash_root()
.as_bytes(),
)?;
hasher.write(state.current_epoch_attestations.tree_hash_root().as_bytes())?;
}
// FIXME(altair): add a cache to accelerate hashing of these fields
BeaconState::Altair(state) => {
hasher.write(
state
.previous_epoch_participation
.tree_hash_root()
.as_bytes(),
)?;
hasher.write(
state
.current_epoch_participation
.tree_hash_root()
.as_bytes(),
)?;
}
}
hasher.write(state.justification_bits().tree_hash_root().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
.previous_justified_checkpoint()
.tree_hash_root()
.as_bytes(),
)?;
hasher.write(
state
.current_justified_checkpoint
.current_justified_checkpoint()
.tree_hash_root()
.as_bytes(),
)?;
hasher.write(state.finalized_checkpoint.tree_hash_root().as_bytes())?;
hasher.write(state.finalized_checkpoint().tree_hash_root().as_bytes())?;
// Inactivity & light-client sync committees
if let BeaconState::Altair(ref state) = state {
// FIXME(altair): add cache for this field
hasher.write(state.inactivity_scores.tree_hash_root().as_bytes())?;
hasher.write(state.current_sync_committee.tree_hash_root().as_bytes())?;
hasher.write(state.next_sync_committee.tree_hash_root().as_bytes())?;
}
let root = hasher.finish()?;
self.previous_state = Some((root, state.slot));
self.previous_state = Some((root, state.slot()));
Ok(root)
}
@@ -432,6 +506,13 @@ impl ParallelValidatorTreeHash {
}
}
#[cfg(feature = "arbitrary-fuzz")]
impl<T: EthSpec> arbitrary::Arbitrary for BeaconTreeHashCache<T> {
fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
Ok(Self::default())
}
}
#[cfg(test)]
mod test {
use super::*;

View File

@@ -1,14 +1,12 @@
use crate::*;
use int_to_bytes::int_to_bytes4;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
use serde_utils::quoted_u64::MaybeQuoted;
use std::fs::File;
use std::path::Path;
use tree_hash::TreeHash;
/// Each of the BLS signature domains.
///
/// Spec v0.12.1
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Domain {
BeaconProposer,
@@ -18,11 +16,12 @@ pub enum Domain {
VoluntaryExit,
SelectionProof,
AggregateAndProof,
SyncCommittee,
}
/// Holds all the "constants" for a BeaconChain.
/// Lighthouse's internal configuration struct.
///
/// Spec v0.12.1
/// Contains a mixture of "preset" and "config" values w.r.t to the EF definitions.
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(PartialEq, Debug, Clone)]
pub struct ChainSpec {
@@ -87,13 +86,13 @@ pub struct ChainSpec {
/*
* 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,
pub(crate) domain_beacon_proposer: u32,
pub(crate) domain_beacon_attester: u32,
pub(crate) domain_randao: u32,
pub(crate) domain_deposit: u32,
pub(crate) domain_voluntary_exit: u32,
pub(crate) domain_selection_proof: u32,
pub(crate) domain_aggregate_and_proof: u32,
/*
* Fork choice
@@ -109,6 +108,23 @@ pub struct ChainSpec {
pub deposit_network_id: u64,
pub deposit_contract_address: Address,
/*
* Altair hard fork params
*/
pub inactivity_penalty_quotient_altair: u64,
pub min_slashing_penalty_quotient_altair: u64,
pub proportional_slashing_multiplier_altair: u64,
pub epochs_per_sync_committee_period: Epoch,
pub inactivity_score_bias: u64,
pub inactivity_score_recovery_rate: u64,
pub min_sync_committee_participants: u64,
pub(crate) domain_sync_committee: u32,
pub(crate) domain_sync_committee_selection_proof: u32,
pub(crate) domain_contribution_and_proof: u32,
pub altair_fork_version: [u8; 4],
/// The Altair fork epoch is optional, with `None` representing "Altair never happens".
pub altair_fork_epoch: Option<Epoch>,
/*
* Networking
*/
@@ -123,6 +139,12 @@ pub struct ChainSpec {
}
impl ChainSpec {
/// Construct a `ChainSpec` from a standard config.
pub fn from_config<T: EthSpec>(config: &Config) -> Option<Self> {
let spec = T::default_spec();
config.apply_to_chain_spec::<T>(&spec)
}
/// 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
@@ -146,6 +168,19 @@ impl ChainSpec {
None
}
/// Returns the name of the fork which is active at `slot`.
pub fn fork_name_at_slot<E: EthSpec>(&self, slot: Slot) -> ForkName {
self.fork_name_at_epoch(slot.epoch(E::slots_per_epoch()))
}
/// Returns the name of the fork which is active at `epoch`.
pub fn fork_name_at_epoch(&self, epoch: Epoch) -> ForkName {
match self.altair_fork_epoch {
Some(fork_epoch) if epoch >= fork_epoch => ForkName::Altair,
_ => ForkName::Base,
}
}
/// Get the domain number, unmodified by the fork.
///
/// Spec v0.12.1
@@ -158,6 +193,7 @@ impl ChainSpec {
Domain::VoluntaryExit => self.domain_voluntary_exit,
Domain::SelectionProof => self.domain_selection_proof,
Domain::AggregateAndProof => self.domain_aggregate_and_proof,
Domain::SyncCommittee => self.domain_sync_committee,
}
}
@@ -211,13 +247,15 @@ impl ChainSpec {
) -> [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.copy_from_slice(
root.as_bytes()
.get(0..4)
.expect("root hash is at least 4 bytes"),
);
result
}
/// Compute a domain by applying the given `fork_version`.
///
/// Spec v0.12.1
pub fn compute_domain(
&self,
domain: Domain,
@@ -229,7 +267,10 @@ impl ChainSpec {
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],
Self::compute_fork_data_root(fork_version, genesis_validators_root)
.as_bytes()
.get(..28)
.expect("fork has is 32 bytes so first 28 bytes should exist"),
);
Hash256::from(domain)
@@ -265,10 +306,22 @@ impl ChainSpec {
/*
* 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)),
min_deposit_amount: option_wrapper(|| {
u64::checked_pow(2, 0)?.checked_mul(u64::checked_pow(10, 9)?)
})
.expect("calculation does not overflow"),
max_effective_balance: option_wrapper(|| {
u64::checked_pow(2, 5)?.checked_mul(u64::checked_pow(10, 9)?)
})
.expect("calculation does not overflow"),
ejection_balance: option_wrapper(|| {
u64::checked_pow(2, 4)?.checked_mul(u64::checked_pow(10, 9)?)
})
.expect("calculation does not overflow"),
effective_balance_increment: option_wrapper(|| {
u64::checked_pow(2, 0)?.checked_mul(u64::checked_pow(10, 9)?)
})
.expect("calculation does not overflow"),
/*
* Initial Values
@@ -294,7 +347,7 @@ impl ChainSpec {
base_reward_factor: 64,
whistleblower_reward_quotient: 512,
proposer_reward_quotient: 8,
inactivity_penalty_quotient: u64::pow(2, 26),
inactivity_penalty_quotient: u64::checked_pow(2, 26).expect("pow does not overflow"),
min_slashing_penalty_quotient: 128,
proportional_slashing_multiplier: 1,
@@ -325,6 +378,26 @@ impl ChainSpec {
.parse()
.expect("chain spec deposit contract address"),
/*
* Altair hard fork params
*/
inactivity_penalty_quotient_altair: option_wrapper(|| {
u64::checked_pow(2, 24)?.checked_mul(3)
})
.expect("calculation does not overflow"),
min_slashing_penalty_quotient_altair: u64::checked_pow(2, 6)
.expect("pow does not overflow"),
proportional_slashing_multiplier_altair: 2,
inactivity_score_bias: 4,
inactivity_score_recovery_rate: 16,
min_sync_committee_participants: 1,
epochs_per_sync_committee_period: Epoch::new(256),
domain_sync_committee: 7,
domain_sync_committee_selection_proof: 8,
domain_contribution_and_proof: 9,
altair_fork_version: [0x01, 0x00, 0x00, 0x00],
altair_fork_epoch: Some(Epoch::new(u64::MAX)),
/*
* Network specific
*/
@@ -340,8 +413,6 @@ impl ChainSpec {
}
/// Ethereum Foundation minimal spec, as defined in the eth2.0-specs repo.
///
/// Spec v0.12.1
pub fn minimal() -> Self {
// Note: bootnodes to be updated when static nodes exist.
let boot_nodes = vec![];
@@ -357,10 +428,15 @@ impl ChainSpec {
shard_committee_period: 64,
genesis_delay: 300,
seconds_per_slot: 6,
inactivity_penalty_quotient: u64::pow(2, 25),
inactivity_penalty_quotient: u64::checked_pow(2, 25).expect("pow does not overflow"),
min_slashing_penalty_quotient: 64,
proportional_slashing_multiplier: 2,
safe_slots_to_update_justified: 2,
// Altair
epochs_per_sync_committee_period: Epoch::new(8),
altair_fork_version: [0x01, 0x00, 0x00, 0x01],
altair_fork_epoch: Some(Epoch::new(u64::MAX)),
// Other
network_id: 2, // lighthouse testnet network id
deposit_chain_id: 5,
deposit_network_id: 5,
@@ -371,24 +447,6 @@ impl ChainSpec {
..ChainSpec::mainnet()
}
}
/// Suits the `v0.12.3` version of the eth2 spec:
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.3/configs/mainnet/phase0.yaml
///
/// This method only needs to exist whilst we provide support for "legacy" testnets prior to v1.0.0
/// (e.g., Medalla, Pyrmont, Spadina, Altona, etc.).
pub fn v012_legacy() -> Self {
let boot_nodes = vec![];
Self {
genesis_delay: 172_800, // 2 days
inactivity_penalty_quotient: u64::pow(2, 24),
min_slashing_penalty_quotient: 32,
eth1_follow_distance: 1024,
boot_nodes,
..ChainSpec::mainnet()
}
}
}
impl Default for ChainSpec {
@@ -397,6 +455,175 @@ impl Default for ChainSpec {
}
}
/// Exact implementation of the *config* object from the Ethereum spec (YAML/JSON).
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub struct Config {
#[serde(default)]
pub preset_base: String,
#[serde(with = "serde_utils::quoted_u64")]
min_genesis_active_validator_count: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_genesis_time: u64,
#[serde(with = "serde_utils::bytes_4_hex")]
genesis_fork_version: [u8; 4],
#[serde(with = "serde_utils::quoted_u64")]
genesis_delay: u64,
#[serde(with = "serde_utils::bytes_4_hex")]
altair_fork_version: [u8; 4],
altair_fork_epoch: Option<MaybeQuoted<Epoch>>,
#[serde(with = "serde_utils::quoted_u64")]
seconds_per_slot: u64,
#[serde(with = "serde_utils::quoted_u64")]
seconds_per_eth1_block: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_validator_withdrawability_delay: Epoch,
#[serde(with = "serde_utils::quoted_u64")]
shard_committee_period: u64,
#[serde(with = "serde_utils::quoted_u64")]
eth1_follow_distance: u64,
#[serde(with = "serde_utils::quoted_u64")]
inactivity_score_bias: u64,
#[serde(with = "serde_utils::quoted_u64")]
inactivity_score_recovery_rate: u64,
#[serde(with = "serde_utils::quoted_u64")]
ejection_balance: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_per_epoch_churn_limit: u64,
#[serde(with = "serde_utils::quoted_u64")]
churn_limit_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
deposit_chain_id: u64,
#[serde(with = "serde_utils::quoted_u64")]
deposit_network_id: u64,
deposit_contract_address: Address,
}
impl Default for Config {
fn default() -> Self {
let chain_spec = MainnetEthSpec::default_spec();
Config::from_chain_spec::<MainnetEthSpec>(&chain_spec)
}
}
impl Config {
/// Maps `self` to an identifier for an `EthSpec` instance.
///
/// Returns `None` if there is no match.
pub fn eth_spec_id(&self) -> Option<EthSpecId> {
match self.preset_base.as_str() {
"minimal" => Some(EthSpecId::Minimal),
"mainnet" => Some(EthSpecId::Mainnet),
_ => None,
}
}
pub fn from_chain_spec<T: EthSpec>(spec: &ChainSpec) -> Self {
Self {
preset_base: T::spec_name().to_string(),
min_genesis_active_validator_count: spec.min_genesis_active_validator_count,
min_genesis_time: spec.min_genesis_time,
genesis_fork_version: spec.genesis_fork_version,
genesis_delay: spec.genesis_delay,
altair_fork_version: spec.altair_fork_version,
altair_fork_epoch: spec
.altair_fork_epoch
.map(|slot| MaybeQuoted { value: slot }),
seconds_per_slot: spec.seconds_per_slot,
seconds_per_eth1_block: spec.seconds_per_eth1_block,
min_validator_withdrawability_delay: spec.min_validator_withdrawability_delay,
shard_committee_period: spec.shard_committee_period,
eth1_follow_distance: spec.eth1_follow_distance,
inactivity_score_bias: spec.inactivity_score_bias,
inactivity_score_recovery_rate: spec.inactivity_score_recovery_rate,
ejection_balance: spec.ejection_balance,
churn_limit_quotient: spec.churn_limit_quotient,
min_per_epoch_churn_limit: spec.min_per_epoch_churn_limit,
deposit_chain_id: spec.deposit_chain_id,
deposit_network_id: spec.deposit_network_id,
deposit_contract_address: spec.deposit_contract_address,
}
}
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> {
// Pattern match here to avoid missing any fields.
let &Config {
ref preset_base,
min_genesis_active_validator_count,
min_genesis_time,
genesis_fork_version,
genesis_delay,
altair_fork_version,
altair_fork_epoch,
seconds_per_slot,
seconds_per_eth1_block,
min_validator_withdrawability_delay,
shard_committee_period,
eth1_follow_distance,
inactivity_score_bias,
inactivity_score_recovery_rate,
ejection_balance,
min_per_epoch_churn_limit,
churn_limit_quotient,
deposit_chain_id,
deposit_network_id,
deposit_contract_address,
} = self;
if preset_base != T::spec_name().to_string().as_str() {
return None;
}
Some(ChainSpec {
min_genesis_active_validator_count,
min_genesis_time,
genesis_fork_version,
genesis_delay,
altair_fork_version,
altair_fork_epoch: altair_fork_epoch.map(|q| q.value),
seconds_per_slot,
seconds_per_eth1_block,
min_validator_withdrawability_delay,
shard_committee_period,
eth1_follow_distance,
inactivity_score_bias,
inactivity_score_recovery_rate,
ejection_balance,
min_per_epoch_churn_limit,
churn_limit_quotient,
deposit_chain_id,
deposit_network_id,
deposit_contract_address,
..chain_spec.clone()
})
}
}
/// A simple wrapper to permit the in-line use of `?`.
fn option_wrapper<F, T>(f: F) -> Option<T>
where
F: Fn() -> Option<T>,
{
f()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -446,359 +673,7 @@ mod tests {
spec.domain_aggregate_and_proof,
&spec,
);
}
}
/// YAML config file as defined by the spec.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub struct YamlConfig {
pub config_name: String,
// ChainSpec
#[serde(with = "serde_utils::quoted_u64")]
max_committees_per_slot: u64,
#[serde(with = "serde_utils::quoted_u64")]
target_committee_size: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_per_epoch_churn_limit: u64,
#[serde(with = "serde_utils::quoted_u64")]
churn_limit_quotient: u64,
#[serde(with = "serde_utils::quoted_u8")]
shuffle_round_count: u8,
#[serde(with = "serde_utils::quoted_u64")]
min_genesis_active_validator_count: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_genesis_time: u64,
#[serde(with = "serde_utils::quoted_u64")]
genesis_delay: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_deposit_amount: u64,
#[serde(with = "serde_utils::quoted_u64")]
max_effective_balance: u64,
#[serde(with = "serde_utils::quoted_u64")]
ejection_balance: u64,
#[serde(with = "serde_utils::quoted_u64")]
effective_balance_increment: u64,
#[serde(with = "serde_utils::quoted_u64")]
hysteresis_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
hysteresis_downward_multiplier: u64,
#[serde(with = "serde_utils::quoted_u64")]
hysteresis_upward_multiplier: u64,
#[serde(with = "serde_utils::bytes_4_hex")]
genesis_fork_version: [u8; 4],
#[serde(with = "serde_utils::u8_hex")]
bls_withdrawal_prefix: u8,
#[serde(with = "serde_utils::quoted_u64")]
seconds_per_slot: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_attestation_inclusion_delay: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_seed_lookahead: u64,
#[serde(with = "serde_utils::quoted_u64")]
max_seed_lookahead: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_epochs_to_inactivity_penalty: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_validator_withdrawability_delay: u64,
#[serde(with = "serde_utils::quoted_u64")]
shard_committee_period: u64,
#[serde(with = "serde_utils::quoted_u64")]
base_reward_factor: u64,
#[serde(with = "serde_utils::quoted_u64")]
whistleblower_reward_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
proposer_reward_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
inactivity_penalty_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
min_slashing_penalty_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
proportional_slashing_multiplier: u64,
#[serde(with = "serde_utils::quoted_u64")]
safe_slots_to_update_justified: u64,
#[serde(with = "serde_utils::u32_hex")]
domain_beacon_proposer: u32,
#[serde(with = "serde_utils::u32_hex")]
domain_beacon_attester: u32,
#[serde(with = "serde_utils::u32_hex")]
domain_randao: u32,
#[serde(with = "serde_utils::u32_hex")]
domain_deposit: u32,
#[serde(with = "serde_utils::u32_hex")]
domain_voluntary_exit: u32,
#[serde(with = "serde_utils::u32_hex")]
domain_selection_proof: u32,
#[serde(with = "serde_utils::u32_hex")]
domain_aggregate_and_proof: u32,
// EthSpec
#[serde(with = "serde_utils::quoted_u32")]
max_validators_per_committee: u32,
#[serde(with = "serde_utils::quoted_u64")]
slots_per_epoch: u64,
#[serde(with = "serde_utils::quoted_u64")]
epochs_per_eth1_voting_period: u64,
#[serde(with = "serde_utils::quoted_u64")]
slots_per_historical_root: u64,
#[serde(with = "serde_utils::quoted_u64")]
epochs_per_historical_vector: u64,
#[serde(with = "serde_utils::quoted_u64")]
epochs_per_slashings_vector: u64,
#[serde(with = "serde_utils::quoted_u64")]
historical_roots_limit: u64,
#[serde(with = "serde_utils::quoted_u64")]
validator_registry_limit: u64,
#[serde(with = "serde_utils::quoted_u32")]
max_proposer_slashings: u32,
#[serde(with = "serde_utils::quoted_u32")]
max_attester_slashings: u32,
#[serde(with = "serde_utils::quoted_u32")]
max_attestations: u32,
#[serde(with = "serde_utils::quoted_u32")]
max_deposits: u32,
#[serde(with = "serde_utils::quoted_u32")]
max_voluntary_exits: u32,
// Validator
#[serde(with = "serde_utils::quoted_u64")]
eth1_follow_distance: u64,
#[serde(with = "serde_utils::quoted_u64")]
target_aggregators_per_committee: u64,
#[serde(with = "serde_utils::quoted_u64")]
random_subnets_per_validator: u64,
#[serde(with = "serde_utils::quoted_u64")]
epochs_per_random_subnet_subscription: u64,
#[serde(with = "serde_utils::quoted_u64")]
seconds_per_eth1_block: u64,
#[serde(with = "serde_utils::quoted_u64")]
deposit_chain_id: u64,
#[serde(with = "serde_utils::quoted_u64")]
deposit_network_id: u64,
deposit_contract_address: Address,
// Extra fields (could be from a future hard-fork that we don't yet know).
#[serde(flatten)]
pub extra_fields: HashMap<String, String>,
}
impl Default for YamlConfig {
fn default() -> Self {
let chain_spec = MainnetEthSpec::default_spec();
YamlConfig::from_spec::<MainnetEthSpec>(&chain_spec)
}
}
/// Spec v0.12.1
impl YamlConfig {
/// Maps `self.config_name` to an identifier for an `EthSpec` instance.
///
/// Returns `None` if there is no match.
pub fn eth_spec_id(&self) -> Option<EthSpecId> {
Some(match self.config_name.as_str() {
"mainnet" => EthSpecId::Mainnet,
"minimal" => EthSpecId::Minimal,
"toledo" => EthSpecId::Mainnet,
"prater" => EthSpecId::Mainnet,
"pyrmont" => EthSpecId::Mainnet,
"spadina" => EthSpecId::V012Legacy,
"medalla" => EthSpecId::V012Legacy,
"altona" => EthSpecId::V012Legacy,
_ => return None,
})
}
pub fn from_spec<T: EthSpec>(spec: &ChainSpec) -> Self {
Self {
config_name: T::spec_name().to_string(),
// ChainSpec
max_committees_per_slot: spec.max_committees_per_slot as u64,
target_committee_size: spec.target_committee_size as u64,
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,
genesis_delay: spec.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,
proportional_slashing_multiplier: spec.proportional_slashing_multiplier,
bls_withdrawal_prefix: spec.bls_withdrawal_prefix_byte,
seconds_per_slot: spec.seconds_per_slot,
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(),
shard_committee_period: spec.shard_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
max_validators_per_committee: T::MaxValidatorsPerCommittee::to_u32(),
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() as u64,
epochs_per_historical_vector: T::epochs_per_historical_vector() as u64,
epochs_per_slashings_vector: T::EpochsPerSlashingsVector::to_u64(),
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,
deposit_chain_id: spec.deposit_chain_id,
deposit_network_id: spec.deposit_network_id,
deposit_contract_address: spec.deposit_contract_address,
extra_fields: HashMap::new(),
}
}
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> {
// Check that YAML values match type-level EthSpec constants
if self.max_validators_per_committee != T::MaxValidatorsPerCommittee::to_u32()
|| 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() as u64
|| self.epochs_per_historical_vector != T::epochs_per_historical_vector() as u64
|| self.epochs_per_slashings_vector != T::EpochsPerSlashingsVector::to_u64()
|| 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 {
/*
* Misc
*/
max_committees_per_slot: self.max_committees_per_slot as usize,
target_committee_size: self.target_committee_size as usize,
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,
hysteresis_quotient: self.hysteresis_quotient,
hysteresis_downward_multiplier: self.hysteresis_downward_multiplier,
hysteresis_upward_multiplier: self.hysteresis_upward_multiplier,
proportional_slashing_multiplier: self.proportional_slashing_multiplier,
/*
* Fork Choice
*/
safe_slots_to_update_justified: self.safe_slots_to_update_justified,
/*
* Validator
*/
eth1_follow_distance: self.eth1_follow_distance,
target_aggregators_per_committee: self.target_aggregators_per_committee,
random_subnets_per_validator: self.random_subnets_per_validator,
epochs_per_random_subnet_subscription: self.epochs_per_random_subnet_subscription,
seconds_per_eth1_block: self.seconds_per_eth1_block,
deposit_chain_id: self.deposit_chain_id,
deposit_network_id: self.deposit_network_id,
deposit_contract_address: self.deposit_contract_address,
/*
* Gwei values
*/
min_deposit_amount: self.min_deposit_amount,
max_effective_balance: self.max_effective_balance,
ejection_balance: self.ejection_balance,
effective_balance_increment: self.effective_balance_increment,
/*
* Initial values
*/
genesis_fork_version: self.genesis_fork_version,
bls_withdrawal_prefix_byte: self.bls_withdrawal_prefix,
/*
* Time parameters
*/
genesis_delay: self.genesis_delay,
seconds_per_slot: self.seconds_per_slot,
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,
),
shard_committee_period: self.shard_committee_period,
min_epochs_to_inactivity_penalty: self.min_epochs_to_inactivity_penalty,
/*
* Reward and penalty quotients
*/
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,
/*
* Signature domains
*/
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,
domain_selection_proof: self.domain_selection_proof,
domain_aggregate_and_proof: self.domain_aggregate_and_proof,
/*
* Lighthouse-specific parameters
*
* These are paramaters that are present in the chain spec but aren't part of the YAML
* config. We avoid using `..chain_spec` so that changes to the set of fields don't
* accidentally get forgotten (explicit better than implicit, yada yada).
*/
boot_nodes: chain_spec.boot_nodes.clone(),
network_id: chain_spec.network_id,
attestation_propagation_slot_range: chain_spec.attestation_propagation_slot_range,
maximum_gossip_clock_disparity_millis: chain_spec.maximum_gossip_clock_disparity_millis,
attestation_subnet_count: chain_spec.attestation_subnet_count,
/*
* Constants, not configurable.
*/
genesis_slot: chain_spec.genesis_slot,
far_future_epoch: chain_spec.far_future_epoch,
base_rewards_per_epoch: chain_spec.base_rewards_per_epoch,
deposit_contract_tree_depth: chain_spec.deposit_contract_tree_depth,
})
test_domain(Domain::SyncCommittee, spec.domain_sync_committee, &spec);
}
}
@@ -819,7 +694,7 @@ mod yaml_tests {
.expect("error opening file");
let minimal_spec = ChainSpec::minimal();
let yamlconfig = YamlConfig::from_spec::<MinimalEthSpec>(&minimal_spec);
let yamlconfig = Config::from_chain_spec::<MinimalEthSpec>(&minimal_spec);
// write fresh minimal config to file
serde_yaml::to_writer(writer, &yamlconfig).expect("failed to write or serialize");
@@ -829,7 +704,7 @@ mod yaml_tests {
.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");
let from: Config = serde_yaml::from_reader(reader).expect("error while deserializing");
assert_eq!(from, yamlconfig);
}
@@ -842,7 +717,7 @@ mod yaml_tests {
.open(tmp_file.as_ref())
.expect("error opening file");
let mainnet_spec = ChainSpec::mainnet();
let yamlconfig = YamlConfig::from_spec::<MainnetEthSpec>(&mainnet_spec);
let yamlconfig = Config::from_chain_spec::<MainnetEthSpec>(&mainnet_spec);
serde_yaml::to_writer(writer, &yamlconfig).expect("failed to write or serialize");
let reader = OpenOptions::new()
@@ -850,42 +725,17 @@ mod yaml_tests {
.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 extra_fields_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 mut yamlconfig = YamlConfig::from_spec::<MainnetEthSpec>(&mainnet_spec);
let (k1, v1) = ("SAMPLE_HARDFORK_KEY1", "123456789");
let (k2, v2) = ("SAMPLE_HARDFORK_KEY2", "987654321");
yamlconfig.extra_fields.insert(k1.into(), v1.into());
yamlconfig.extra_fields.insert(k2.into(), v2.into());
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");
let from: Config = 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);
let yamlconfig = Config::from_chain_spec::<MinimalEthSpec>(&spec);
// modifying the original spec
spec.max_committees_per_slot += 1;
spec.min_genesis_active_validator_count += 1;
spec.deposit_chain_id += 1;
spec.deposit_network_id += 1;
// Applying a yaml config with incorrect EthSpec should fail

View File

@@ -0,0 +1,119 @@
use crate::{AltairPreset, BasePreset, ChainSpec, Config, EthSpec};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
/// Fusion of a runtime-config with the compile-time preset values.
///
/// Mostly useful for the API.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct ConfigAndPreset {
#[serde(flatten)]
pub config: Config,
#[serde(flatten)]
pub base_preset: BasePreset,
#[serde(flatten)]
pub altair_preset: AltairPreset,
/// The `extra_fields` map allows us to gracefully decode fields intended for future hard forks.
#[serde(flatten)]
pub extra_fields: HashMap<String, String>,
}
impl ConfigAndPreset {
pub fn from_chain_spec<T: EthSpec>(spec: &ChainSpec) -> Self {
let config = Config::from_chain_spec::<T>(spec);
let base_preset = BasePreset::from_chain_spec::<T>(spec);
let altair_preset = AltairPreset::from_chain_spec::<T>(spec);
let extra_fields = HashMap::new();
Self {
config,
base_preset,
altair_preset,
extra_fields,
}
}
/// Add fields that were previously part of the config but are now constants.
pub fn make_backwards_compat(&mut self, spec: &ChainSpec) {
let hex_string = |value: &[u8]| format!("0x{}", hex::encode(&value));
let u32_hex = |v: u32| hex_string(&v.to_le_bytes());
let u8_hex = |v: u8| hex_string(&v.to_le_bytes());
let fields = vec![
("config_name", self.config.preset_base.clone()),
(
"bls_withdrawal_prefix",
u8_hex(spec.bls_withdrawal_prefix_byte),
),
(
"domain_beacon_proposer",
u32_hex(spec.domain_beacon_proposer),
),
(
"domain_beacon_attester",
u32_hex(spec.domain_beacon_attester),
),
("domain_randao", u32_hex(spec.domain_randao)),
("domain_deposit", u32_hex(spec.domain_deposit)),
("domain_voluntary_exit", u32_hex(spec.domain_voluntary_exit)),
(
"domain_selection_proof",
u32_hex(spec.domain_selection_proof),
),
(
"domain_aggregate_and_proof",
u32_hex(spec.domain_aggregate_and_proof),
),
(
"target_aggregators_per_committee",
spec.target_aggregators_per_committee.to_string(),
),
(
"random_subnets_per_validator",
spec.random_subnets_per_validator.to_string(),
),
(
"epochs_per_random_subnet_subscription",
spec.epochs_per_random_subnet_subscription.to_string(),
),
];
for (key, value) in fields {
self.extra_fields.insert(key.to_uppercase(), value);
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::MainnetEthSpec;
use std::fs::OpenOptions;
use tempfile::NamedTempFile;
#[test]
fn extra_fields_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 mut yamlconfig = ConfigAndPreset::from_chain_spec::<MainnetEthSpec>(&mainnet_spec);
let (k1, v1) = ("SAMPLE_HARDFORK_KEY1", "123456789");
let (k2, v2) = ("SAMPLE_HARDFORK_KEY2", "987654321");
yamlconfig.extra_fields.insert(k1.into(), v1.into());
yamlconfig.extra_fields.insert(k2.into(), v2.into());
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: ConfigAndPreset =
serde_yaml::from_reader(reader).expect("error while deserializing");
assert_eq!(from, yamlconfig);
}
}

View File

@@ -0,0 +1,19 @@
pub mod altair {
pub const TIMELY_SOURCE_FLAG_INDEX: usize = 0;
pub const TIMELY_TARGET_FLAG_INDEX: usize = 1;
pub const TIMELY_HEAD_FLAG_INDEX: usize = 2;
pub const TIMELY_SOURCE_WEIGHT: u64 = 14;
pub const TIMELY_TARGET_WEIGHT: u64 = 26;
pub const TIMELY_HEAD_WEIGHT: u64 = 14;
pub const SYNC_REWARD_WEIGHT: u64 = 2;
pub const PROPOSER_WEIGHT: u64 = 8;
pub const WEIGHT_DENOMINATOR: u64 = 64;
pub const PARTICIPATION_FLAG_WEIGHTS: [u64; NUM_FLAG_INDICES] = [
TIMELY_SOURCE_WEIGHT,
TIMELY_TARGET_WEIGHT,
TIMELY_HEAD_WEIGHT,
];
pub const NUM_FLAG_INDICES: usize = 3;
}

View File

@@ -3,15 +3,14 @@ use crate::*;
use safe_arith::SafeArith;
use serde_derive::{Deserialize, Serialize};
use ssz_types::typenum::{
Unsigned, U0, U1024, U1099511627776, U128, U16, U16777216, U2, U2048, U32, U4, U4096, U64,
U65536, U8, U8192,
Unsigned, U0, U1024, U1099511627776, U128, U16, U16777216, U2, U2048, U32, U4, U4096, U512,
U64, U65536, U8, U8192,
};
use std::fmt::{self, Debug};
use std::str::FromStr;
const MAINNET: &str = "mainnet";
const MINIMAL: &str = "minimal";
const LEGACY: &str = "v0.12-legacy";
/// Used to identify one of the `EthSpec` instances defined here.
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -19,7 +18,6 @@ const LEGACY: &str = "v0.12-legacy";
pub enum EthSpecId {
Mainnet,
Minimal,
V012Legacy,
}
impl FromStr for EthSpecId {
@@ -29,7 +27,6 @@ impl FromStr for EthSpecId {
match s {
MAINNET => Ok(EthSpecId::Mainnet),
MINIMAL => Ok(EthSpecId::Minimal),
LEGACY => Ok(EthSpecId::V012Legacy),
_ => Err(format!("Unknown eth spec: {}", s)),
}
}
@@ -40,7 +37,6 @@ impl fmt::Display for EthSpecId {
let s = match self {
EthSpecId::Mainnet => MAINNET,
EthSpecId::Minimal => MINIMAL,
EthSpecId::V012Legacy => LEGACY,
};
write!(f, "{}", s)
}
@@ -78,6 +74,10 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + 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;
/*
* New in Altair
*/
type SyncCommitteeSize: Unsigned + Clone + Sync + Send + Debug + PartialEq;
/*
* Derived values (set these CAREFULLY)
*/
@@ -182,8 +182,6 @@ macro_rules! params_from_eth_spec {
}
/// Ethereum Foundation specifications.
///
/// Spec v0.12.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct MainnetEthSpec;
@@ -205,6 +203,7 @@ impl EthSpec for MainnetEthSpec {
type MaxAttestations = U128;
type MaxDeposits = U16;
type MaxVoluntaryExits = U16;
type SyncCommitteeSize = U512;
type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch
type SlotsPerEth1VotingPeriod = U2048; // 64 epochs * 32 slots per epoch
@@ -217,11 +216,7 @@ impl EthSpec for MainnetEthSpec {
}
}
pub type FoundationBeaconState = BeaconState<MainnetEthSpec>;
/// Ethereum Foundation minimal spec, as defined in the eth2.0-specs repo.
///
/// Spec v0.12.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct MinimalEthSpec;
@@ -232,6 +227,7 @@ impl EthSpec for MinimalEthSpec {
type SlotsPerHistoricalRoot = U64;
type EpochsPerHistoricalVector = U64;
type EpochsPerSlashingsVector = U64;
type SyncCommitteeSize = U32;
type MaxPendingAttestations = U1024; // 128 max attestations * 8 slots per epoch
type SlotsPerEth1VotingPeriod = U32; // 4 epochs * 8 slots per epoch
@@ -257,46 +253,3 @@ impl EthSpec for MinimalEthSpec {
EthSpecId::Minimal
}
}
pub type MinimalBeaconState = BeaconState<MinimalEthSpec>;
/// Suits the `v0.12.3` version of the eth2 spec:
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.3/configs/mainnet/phase0.yaml
///
/// This struct only needs to exist whilst we provide support for "legacy" testnets prior to v1.0.0
/// (e.g., Medalla, Pyrmont, Spadina, Altona, etc.).
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct V012LegacyEthSpec;
impl EthSpec for V012LegacyEthSpec {
type EpochsPerEth1VotingPeriod = U32;
type SlotsPerEth1VotingPeriod = U1024; // 32 epochs * 32 slots per epoch
params_from_eth_spec!(MainnetEthSpec {
SlotsPerEpoch,
SlotsPerHistoricalRoot,
EpochsPerHistoricalVector,
EpochsPerSlashingsVector,
MaxPendingAttestations,
JustificationBitsLength,
SubnetBitfieldLength,
MaxValidatorsPerCommittee,
GenesisEpoch,
HistoricalRootsLimit,
ValidatorRegistryLimit,
MaxProposerSlashings,
MaxAttesterSlashings,
MaxAttestations,
MaxDeposits,
MaxVoluntaryExits
});
fn default_spec() -> ChainSpec {
ChainSpec::v012_legacy()
}
fn spec_name() -> EthSpecId {
EthSpecId::V012Legacy
}
}

View File

@@ -0,0 +1,47 @@
use crate::{ChainSpec, Epoch};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ForkName {
Base,
Altair,
}
impl ForkName {
pub fn list_all() -> Vec<ForkName> {
vec![ForkName::Base, ForkName::Altair]
}
/// Set the activation slots in the given `ChainSpec` so that the fork named by `self`
/// is the only fork in effect from genesis.
pub fn make_genesis_spec(&self, mut spec: ChainSpec) -> ChainSpec {
// Assumes GENESIS_EPOCH = 0, which is safe because it's a constant.
match self {
ForkName::Base => {
spec.altair_fork_epoch = None;
spec
}
ForkName::Altair => {
spec.altair_fork_epoch = Some(Epoch::new(0));
spec
}
}
}
}
impl std::str::FromStr for ForkName {
type Err = ();
fn from_str(fork_name: &str) -> Result<Self, ()> {
Ok(match fork_name {
"phase0" | "base" => ForkName::Base,
"altair" => ForkName::Altair,
_ => return Err(()),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InconsistentFork {
pub fork_at_slot: ForkName,
pub object_fork: ForkName,
}

View File

@@ -74,14 +74,17 @@ impl<'de> Deserialize<'de> for GraffitiString {
impl Into<Graffiti> for GraffitiString {
fn into(self) -> Graffiti {
let graffiti_bytes = self.0.as_bytes();
let mut graffiti = [0; 32];
let mut graffiti = [0; GRAFFITI_BYTES_LEN];
let graffiti_len = std::cmp::min(graffiti_bytes.len(), 32);
let graffiti_len = std::cmp::min(graffiti_bytes.len(), GRAFFITI_BYTES_LEN);
// Copy the provided bytes over.
//
// Panic-free because `graffiti_bytes.len()` <= `GRAFFITI_BYTES_LEN`.
graffiti[..graffiti_len].copy_from_slice(&graffiti_bytes);
graffiti
.get_mut(..graffiti_len)
.expect("graffiti_len <= GRAFFITI_BYTES_LEN")
.copy_from_slice(&graffiti_bytes);
graffiti.into()
}
}

View File

@@ -3,8 +3,14 @@
// Required for big type-level numbers
#![recursion_limit = "128"]
// Clippy lint set up
#![deny(clippy::integer_arithmetic)]
#![deny(clippy::disallowed_method)]
#![cfg_attr(
not(test),
deny(
clippy::integer_arithmetic,
clippy::disallowed_method,
clippy::indexing_slicing
)
)]
#[macro_use]
extern crate lazy_static;
@@ -23,6 +29,7 @@ pub mod beacon_committee;
pub mod beacon_state;
pub mod chain_spec;
pub mod checkpoint;
pub mod consts;
pub mod deposit;
pub mod deposit_data;
pub mod deposit_message;
@@ -31,6 +38,7 @@ pub mod eth1_data;
pub mod eth_spec;
pub mod fork;
pub mod fork_data;
pub mod fork_name;
pub mod free_attestation;
pub mod graffiti;
pub mod historical_batch;
@@ -50,8 +58,13 @@ pub mod validator_subscription;
pub mod voluntary_exit;
#[macro_use]
pub mod slot_epoch_macros;
pub mod config_and_preset;
pub mod participation_flags;
pub mod preset;
pub mod slot_epoch;
pub mod subnet_id;
pub mod sync_aggregate;
pub mod sync_committee;
mod tree_hash_impls;
#[cfg(feature = "sqlite")]
@@ -64,13 +77,19 @@ 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::{
BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockRef, BeaconBlockRefMut,
};
pub use crate::beacon_block_body::{
BeaconBlockBody, BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyRef,
BeaconBlockBodyRefMut,
};
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::chain_spec::{ChainSpec, Config, Domain};
pub use crate::checkpoint::Checkpoint;
pub use crate::config_and_preset::ConfigAndPreset;
pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH};
pub use crate::deposit_data::DepositData;
pub use crate::deposit_message::DepositMessage;
@@ -79,22 +98,29 @@ pub use crate::eth1_data::Eth1Data;
pub use crate::eth_spec::EthSpecId;
pub use crate::fork::Fork;
pub use crate::fork_data::ForkData;
pub use crate::fork_name::{ForkName, InconsistentFork};
pub use crate::free_attestation::FreeAttestation;
pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN};
pub use crate::historical_batch::HistoricalBatch;
pub use crate::indexed_attestation::IndexedAttestation;
pub use crate::participation_flags::ParticipationFlags;
pub use crate::pending_attestation::PendingAttestation;
pub use crate::preset::{AltairPreset, BasePreset};
pub use crate::proposer_slashing::ProposerSlashing;
pub use crate::relative_epoch::{Error as RelativeEpochError, RelativeEpoch};
pub use crate::selection_proof::SelectionProof;
pub use crate::shuffling_id::AttestationShufflingId;
pub use crate::signed_aggregate_and_proof::SignedAggregateAndProof;
pub use crate::signed_beacon_block::{SignedBeaconBlock, SignedBeaconBlockHash};
pub use crate::signed_beacon_block::{
SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockHash,
};
pub use crate::signed_beacon_block_header::SignedBeaconBlockHeader;
pub use crate::signed_voluntary_exit::SignedVoluntaryExit;
pub use crate::signing_data::{SignedRoot, SigningData};
pub use crate::slot_epoch::{Epoch, Slot};
pub use crate::subnet_id::SubnetId;
pub use crate::sync_aggregate::SyncAggregate;
pub use crate::sync_committee::SyncCommittee;
pub use crate::validator::Validator;
pub use crate::validator_subscription::ValidatorSubscription;
pub use crate::voluntary_exit::VoluntaryExit;
@@ -105,6 +131,8 @@ pub type Address = H160;
pub type ForkVersion = [u8; 4];
pub use bls::{
AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, Signature, SignatureBytes,
AggregatePublicKey, AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey,
Signature, SignatureBytes,
};
pub use ssz_types::{typenum, typenum::Unsigned, BitList, BitVector, FixedVector, VariableList};
pub use superstruct::superstruct;

View File

@@ -0,0 +1,87 @@
use crate::{consts::altair::NUM_FLAG_INDICES, test_utils::TestRandom, Hash256};
use safe_arith::{ArithError, SafeArith};
use serde_derive::{Deserialize, Serialize};
use ssz::{Decode, DecodeError, Encode};
use test_random_derive::TestRandom;
use tree_hash::{TreeHash, TreeHashType};
#[derive(Debug, Default, Clone, Copy, PartialEq, Deserialize, Serialize, TestRandom)]
#[serde(transparent)]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
pub struct ParticipationFlags {
bits: u8,
}
impl ParticipationFlags {
pub fn add_flag(&mut self, flag_index: usize) -> Result<(), ArithError> {
if flag_index >= NUM_FLAG_INDICES {
return Err(ArithError::Overflow);
}
self.bits |= 1u8.safe_shl(flag_index as u32)?;
Ok(())
}
pub fn has_flag(&self, flag_index: usize) -> Result<bool, ArithError> {
if flag_index >= NUM_FLAG_INDICES {
return Err(ArithError::Overflow);
}
let mask = 1u8.safe_shl(flag_index as u32)?;
Ok(self.bits & mask == mask)
}
}
/// Decode implementation that transparently behaves like the inner `u8`.
impl Decode for ParticipationFlags {
fn is_ssz_fixed_len() -> bool {
<u8 as Decode>::is_ssz_fixed_len()
}
fn ssz_fixed_len() -> usize {
<u8 as Decode>::ssz_fixed_len()
}
fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
u8::from_ssz_bytes(bytes).map(|bits| Self { bits })
}
}
/// Encode implementation that transparently behaves like the inner `u8`.
impl Encode for ParticipationFlags {
fn is_ssz_fixed_len() -> bool {
<u8 as Encode>::is_ssz_fixed_len()
}
fn ssz_append(&self, buf: &mut Vec<u8>) {
self.bits.ssz_append(buf);
}
fn ssz_fixed_len() -> usize {
<u8 as Encode>::ssz_fixed_len()
}
fn ssz_bytes_len(&self) -> usize {
self.bits.ssz_bytes_len()
}
fn as_ssz_bytes(&self) -> Vec<u8> {
self.bits.as_ssz_bytes()
}
}
impl TreeHash for ParticipationFlags {
fn tree_hash_type() -> TreeHashType {
u8::tree_hash_type()
}
fn tree_hash_packed_encoding(&self) -> Vec<u8> {
self.bits.tree_hash_packed_encoding()
}
fn tree_hash_packing_factor() -> usize {
u8::tree_hash_packing_factor()
}
fn tree_hash_root(&self) -> Hash256 {
self.bits.tree_hash_root()
}
}

View File

@@ -0,0 +1,196 @@
use crate::{ChainSpec, Epoch, EthSpec, Unsigned};
use serde_derive::{Deserialize, Serialize};
/// Value-level representation of an Ethereum consensus "preset".
///
/// This should only be used to check consistency of the compile-time constants
/// with a preset YAML file, or to make preset values available to the API. Prefer
/// the constants on `EthSpec` or the fields on `ChainSpec` to constructing and using
/// one of these structs.
///
/// https://github.com/ethereum/eth2.0-specs/blob/dev/presets/mainnet/phase0.yaml
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct BasePreset {
#[serde(with = "serde_utils::quoted_u64")]
pub max_committees_per_slot: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub target_committee_size: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub max_validators_per_committee: u64,
#[serde(with = "serde_utils::quoted_u8")]
pub shuffle_round_count: u8,
#[serde(with = "serde_utils::quoted_u64")]
pub hysteresis_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub hysteresis_downward_multiplier: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub hysteresis_upward_multiplier: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub safe_slots_to_update_justified: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub min_deposit_amount: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub max_effective_balance: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub effective_balance_increment: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub min_attestation_inclusion_delay: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub slots_per_epoch: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub min_seed_lookahead: Epoch,
#[serde(with = "serde_utils::quoted_u64")]
pub max_seed_lookahead: Epoch,
#[serde(with = "serde_utils::quoted_u64")]
pub epochs_per_eth1_voting_period: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub slots_per_historical_root: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub min_epochs_to_inactivity_penalty: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub epochs_per_historical_vector: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub epochs_per_slashings_vector: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub historical_roots_limit: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub validator_registry_limit: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub base_reward_factor: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub whistleblower_reward_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub proposer_reward_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub inactivity_penalty_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub min_slashing_penalty_quotient: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub proportional_slashing_multiplier: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub max_proposer_slashings: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub max_attester_slashings: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub max_attestations: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub max_deposits: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub max_voluntary_exits: u64,
}
impl BasePreset {
pub fn from_chain_spec<T: EthSpec>(spec: &ChainSpec) -> Self {
Self {
max_committees_per_slot: spec.max_committees_per_slot as u64,
target_committee_size: spec.target_committee_size as u64,
max_validators_per_committee: T::MaxValidatorsPerCommittee::to_u64(),
shuffle_round_count: spec.shuffle_round_count,
hysteresis_quotient: spec.hysteresis_quotient,
hysteresis_downward_multiplier: spec.hysteresis_downward_multiplier,
hysteresis_upward_multiplier: spec.hysteresis_upward_multiplier,
safe_slots_to_update_justified: spec.safe_slots_to_update_justified,
min_deposit_amount: spec.min_deposit_amount,
max_effective_balance: spec.max_effective_balance,
effective_balance_increment: spec.effective_balance_increment,
min_attestation_inclusion_delay: spec.min_attestation_inclusion_delay,
slots_per_epoch: T::SlotsPerEpoch::to_u64(),
min_seed_lookahead: spec.min_seed_lookahead,
max_seed_lookahead: spec.max_seed_lookahead,
epochs_per_eth1_voting_period: T::EpochsPerEth1VotingPeriod::to_u64(),
slots_per_historical_root: T::SlotsPerHistoricalRoot::to_u64(),
min_epochs_to_inactivity_penalty: spec.min_epochs_to_inactivity_penalty,
epochs_per_historical_vector: T::EpochsPerHistoricalVector::to_u64(),
epochs_per_slashings_vector: T::EpochsPerSlashingsVector::to_u64(),
historical_roots_limit: T::HistoricalRootsLimit::to_u64(),
validator_registry_limit: T::ValidatorRegistryLimit::to_u64(),
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,
proportional_slashing_multiplier: spec.proportional_slashing_multiplier,
max_proposer_slashings: T::MaxProposerSlashings::to_u64(),
max_attester_slashings: T::MaxAttesterSlashings::to_u64(),
max_attestations: T::MaxAttestations::to_u64(),
max_deposits: T::MaxDeposits::to_u64(),
max_voluntary_exits: T::MaxVoluntaryExits::to_u64(),
}
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub struct AltairPreset {
#[serde(with = "serde_utils::quoted_u64")]
pub inactivity_penalty_quotient_altair: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub min_slashing_penalty_quotient_altair: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub proportional_slashing_multiplier_altair: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub sync_committee_size: u64,
#[serde(with = "serde_utils::quoted_u64")]
pub epochs_per_sync_committee_period: Epoch,
#[serde(with = "serde_utils::quoted_u64")]
pub min_sync_committee_participants: u64,
}
impl AltairPreset {
pub fn from_chain_spec<T: EthSpec>(spec: &ChainSpec) -> Self {
Self {
inactivity_penalty_quotient_altair: spec.inactivity_penalty_quotient_altair,
min_slashing_penalty_quotient_altair: spec.min_slashing_penalty_quotient_altair,
proportional_slashing_multiplier_altair: spec.proportional_slashing_multiplier_altair,
sync_committee_size: T::SyncCommitteeSize::to_u64(),
epochs_per_sync_committee_period: spec.epochs_per_sync_committee_period,
min_sync_committee_participants: spec.min_sync_committee_participants,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{MainnetEthSpec, MinimalEthSpec};
use serde::de::DeserializeOwned;
use std::env;
use std::fs::File;
use std::path::PathBuf;
fn presets_base_path() -> PathBuf {
env::var("CARGO_MANIFEST_DIR")
.expect("should know manifest dir")
.parse::<PathBuf>()
.expect("should parse manifest dir as path")
.join("presets")
}
fn preset_from_file<T: DeserializeOwned>(preset_name: &str, filename: &str) -> T {
let f = File::open(&presets_base_path().join(preset_name).join(filename))
.expect("preset file exists");
serde_yaml::from_reader(f).unwrap()
}
fn preset_test<E: EthSpec>() {
let preset_name = E::spec_name().to_string();
let spec = E::default_spec();
let phase0: BasePreset = preset_from_file(&preset_name, "phase0.yaml");
assert_eq!(phase0, BasePreset::from_chain_spec::<E>(&spec));
let altair: AltairPreset = preset_from_file(&preset_name, "altair.yaml");
assert_eq!(altair, AltairPreset::from_chain_spec::<E>(&spec));
}
#[test]
fn mainnet_presets_consistent() {
preset_test::<MainnetEthSpec>();
}
#[test]
fn minimal_presets_consistent() {
preset_test::<MinimalEthSpec>();
}
}

View File

@@ -49,8 +49,9 @@ impl SelectionProof {
pub fn is_aggregator_from_modulo(&self, modulo: u64) -> Result<bool, ArithError> {
let signature_hash = hash(&self.0.as_ssz_bytes());
let signature_hash_int = u64::from_le_bytes(
signature_hash[0..8]
.as_ref()
signature_hash
.get(0..8)
.expect("hash is 32 bytes")
.try_into()
.expect("first 8 bytes of signature should always convert to fixed array"),
);

View File

@@ -1,12 +1,9 @@
use crate::{
test_utils::TestRandom, BeaconBlock, ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey,
SignedBeaconBlockHeader, SignedRoot, SigningData, Slot,
};
use crate::*;
use bls::Signature;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use std::fmt;
use test_random_derive::TestRandom;
use superstruct::superstruct;
use tree_hash::TreeHash;
use tree_hash_derive::TreeHash;
@@ -39,17 +36,116 @@ impl From<SignedBeaconBlockHash> for Hash256 {
}
/// A `BeaconBlock` and a signature from its proposer.
///
/// Spec v0.12.1
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[superstruct(
variants(Base, Altair),
variant_attributes(
derive(
Debug,
PartialEq,
Clone,
Serialize,
Deserialize,
Encode,
Decode,
TreeHash
),
cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary)),
serde(bound = "E: EthSpec")
)
)]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, TreeHash)]
#[serde(untagged)]
#[serde(bound = "E: EthSpec")]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
pub struct SignedBeaconBlock<E: EthSpec> {
pub message: BeaconBlock<E>,
#[superstruct(only(Base), partial_getter(rename = "message_base"))]
pub message: BeaconBlockBase<E>,
#[superstruct(only(Altair), partial_getter(rename = "message_altair"))]
pub message: BeaconBlockAltair<E>,
pub signature: Signature,
}
impl<E: EthSpec> SignedBeaconBlock<E> {
/// Returns the name of the fork pertaining to `self`.
///
/// Will return an `Err` if `self` has been instantiated to a variant conflicting with the fork
/// dictated by `self.slot()`.
pub fn fork_name(&self, spec: &ChainSpec) -> Result<ForkName, InconsistentFork> {
let fork_at_slot = spec.fork_name_at_slot::<E>(self.slot());
let object_fork = match self {
SignedBeaconBlock::Base { .. } => ForkName::Base,
SignedBeaconBlock::Altair { .. } => ForkName::Altair,
};
if fork_at_slot == object_fork {
Ok(object_fork)
} else {
Err(InconsistentFork {
fork_at_slot,
object_fork,
})
}
}
/// SSZ decode.
pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result<Self, ssz::DecodeError> {
// We need to use the slot-switching `from_ssz_bytes` of `BeaconBlock`, which doesn't
// compose with the other SSZ utils, so we duplicate some parts of `ssz_derive` here.
let mut builder = ssz::SszDecoderBuilder::new(bytes);
builder.register_anonymous_variable_length_item()?;
builder.register_type::<Signature>()?;
let mut decoder = builder.build()?;
// Read the first item as a `BeaconBlock`.
let message = decoder.decode_next_with(|bytes| BeaconBlock::from_ssz_bytes(bytes, spec))?;
let signature = decoder.decode_next()?;
Ok(Self::from_block(message, signature))
}
/// Create a new `SignedBeaconBlock` from a `BeaconBlock` and `Signature`.
pub fn from_block(block: BeaconBlock<E>, signature: Signature) -> Self {
match block {
BeaconBlock::Base(message) => {
SignedBeaconBlock::Base(SignedBeaconBlockBase { message, signature })
}
BeaconBlock::Altair(message) => {
SignedBeaconBlock::Altair(SignedBeaconBlockAltair { message, signature })
}
}
}
/// Deconstruct the `SignedBeaconBlock` into a `BeaconBlock` and `Signature`.
///
/// This is necessary to get a `&BeaconBlock` from a `SignedBeaconBlock` because
/// `SignedBeaconBlock` only contains a `BeaconBlock` _variant_.
pub fn deconstruct(self) -> (BeaconBlock<E>, Signature) {
match self {
SignedBeaconBlock::Base(block) => (BeaconBlock::Base(block.message), block.signature),
SignedBeaconBlock::Altair(block) => {
(BeaconBlock::Altair(block.message), block.signature)
}
}
}
/// Accessor for the block's `message` field as a ref.
pub fn message(&self) -> BeaconBlockRef<'_, E> {
match self {
SignedBeaconBlock::Base(inner) => BeaconBlockRef::Base(&inner.message),
SignedBeaconBlock::Altair(inner) => BeaconBlockRef::Altair(&inner.message),
}
}
/// Accessor for the block's `message` as a mutable reference (for testing only).
pub fn message_mut(&mut self) -> BeaconBlockRefMut<'_, E> {
match self {
SignedBeaconBlock::Base(inner) => BeaconBlockRefMut::Base(&mut inner.message),
SignedBeaconBlock::Altair(inner) => BeaconBlockRefMut::Altair(&mut inner.message),
}
}
/// Verify `self.signature`.
///
/// If the root of `block.message` is already known it can be passed in via `object_root_opt`.
@@ -62,8 +158,14 @@ impl<E: EthSpec> SignedBeaconBlock<E> {
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> bool {
// Refuse to verify the signature of a block if its structure does not match the fork at
// `self.slot()`.
if self.fork_name(spec).is_err() {
return false;
}
let domain = spec.get_domain(
self.message.slot.epoch(E::slots_per_epoch()),
self.slot().epoch(E::slots_per_epoch()),
Domain::BeaconProposer,
fork,
genesis_validators_root,
@@ -76,47 +178,37 @@ impl<E: EthSpec> SignedBeaconBlock<E> {
}
.tree_hash_root()
} else {
self.message.signing_root(domain)
self.message().signing_root(domain)
};
self.signature.verify(pubkey, message)
self.signature().verify(pubkey, message)
}
/// Produce a signed beacon block header corresponding to this block.
pub fn signed_block_header(&self) -> SignedBeaconBlockHeader {
SignedBeaconBlockHeader {
message: self.message.block_header(),
signature: self.signature.clone(),
message: self.message().block_header(),
signature: self.signature().clone(),
}
}
/// Convenience accessor for the block's slot.
pub fn slot(&self) -> Slot {
self.message.slot
self.message().slot()
}
/// Convenience accessor for the block's parent root.
pub fn parent_root(&self) -> Hash256 {
self.message.parent_root
self.message().parent_root()
}
/// Convenience accessor for the block's state root.
pub fn state_root(&self) -> Hash256 {
self.message.state_root
self.message().state_root()
}
/// Returns the `tree_hash_root` of the block.
///
/// Spec v0.12.1
pub fn canonical_root(&self) -> Hash256 {
Hash256::from_slice(&self.message.tree_hash_root()[..])
self.message().tree_hash_root()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MainnetEthSpec;
ssz_tests!(SignedBeaconBlock<MainnetEthSpec>);
}

View File

@@ -0,0 +1,36 @@
use crate::test_utils::TestRandom;
use crate::{AggregateSignature, BitVector, EthSpec};
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct SyncAggregate<T: EthSpec> {
pub sync_committee_bits: BitVector<T::SyncCommitteeSize>,
pub sync_committee_signature: AggregateSignature,
}
impl<T: EthSpec> SyncAggregate<T> {
/// New aggregate to be used as the seed for aggregating other signatures.
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
sync_committee_bits: BitVector::default(),
sync_committee_signature: AggregateSignature::infinity(),
}
}
/// Empty aggregate to be used at genesis.
///
/// Contains an empty signature and should *not* be used as the starting point for aggregation,
/// use `new` instead.
pub fn empty() -> Self {
Self {
sync_committee_bits: BitVector::default(),
sync_committee_signature: AggregateSignature::empty(),
}
}
}

View File

@@ -0,0 +1,29 @@
use crate::test_utils::TestRandom;
use crate::typenum::Unsigned;
use crate::{EthSpec, FixedVector};
use bls::PublicKeyBytes;
use serde_derive::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)]
#[serde(bound = "T: EthSpec")]
pub struct SyncCommittee<T: EthSpec> {
pub pubkeys: FixedVector<PublicKeyBytes, T::SyncCommitteeSize>,
pub aggregate_pubkey: PublicKeyBytes,
}
impl<T: EthSpec> SyncCommittee<T> {
/// Create a temporary sync committee that should *never* be included in a legitimate consensus object.
pub fn temporary() -> Result<Self, ssz_types::Error> {
Ok(Self {
pubkeys: FixedVector::new(vec![
PublicKeyBytes::empty();
T::SyncCommitteeSize::to_usize()
])?,
aggregate_pubkey: PublicKeyBytes::empty(),
})
}
}

View File

@@ -1,19 +0,0 @@
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

@@ -1,112 +0,0 @@
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::empty(),
};
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

@@ -1,98 +0,0 @@
use crate::test_utils::AttestationTestTask;
use crate::*;
use safe_arith::SafeArith;
/// 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
} else {
state.current_justified_checkpoint
};
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
.safe_sub(spec.min_attestation_inclusion_delay)
.unwrap()
.safe_add(1u64)
.unwrap();
}
AttestationTestTask::IncludedTooLate => slot
.safe_sub_assign(Slot::new(T::SlotsPerEpoch::to_u64()))
.unwrap(),
AttestationTestTask::TargetEpochSlotMismatch => {
target = Checkpoint {
epoch: current_epoch.safe_add(1u64).unwrap(),
root: Hash256::zero(),
};
assert_ne!(target.epoch, slot.epoch(T::slots_per_epoch()));
}
AttestationTestTask::WrongJustifiedCheckpoint => {
source = Checkpoint {
epoch: Epoch::from(0_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

@@ -1,134 +0,0 @@
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]`
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,
{
TestingAttesterSlashingBuilder::double_vote_with_additional_indices(
test_task,
validator_indices,
None,
signer,
fork,
genesis_validators_root,
spec,
)
}
pub fn double_vote_with_additional_indices<F, T: EthSpec>(
test_task: AttesterSlashingTestTask,
validator_indices: &[u64],
additional_validator_indices: Option<&[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,
target: checkpoint_1,
};
let data_2 = if test_task == AttesterSlashingTestTask::NotSlashable {
data_1.clone()
} else {
AttestationData {
target: checkpoint_2,
..data_1
}
};
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::empty(),
};
let mut attestation_2 = IndexedAttestation {
attesting_indices: if test_task == AttesterSlashingTestTask::IndexedAttestation2Invalid
{
// Trigger bad validator indices ordering error.
vec![1, 0].into()
} else {
match additional_validator_indices {
Some(x) => x.to_vec().into(),
None => validator_indices.to_vec().into(),
}
},
data: data_2,
signature: AggregateSignature::empty(),
};
let add_signatures = |attestation: &mut IndexedAttestation<T>, indices_to_sign: &[u64]| {
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 indices_to_sign {
let signature = signer(*validator_index, message.as_bytes());
attestation.signature.add_assign(&signature);
}
};
add_signatures(&mut attestation_1, validator_indices);
add_signatures(
&mut attestation_2,
additional_validator_indices.unwrap_or(validator_indices),
);
AttesterSlashing {
attestation_1,
attestation_2,
}
}
}

View File

@@ -1,428 +0,0 @@
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 safe_arith::SafeArith;
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 = sk.sign(message);
}
/// 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
.safe_sub(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.safe_add(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.safe_sub_assign(1u64)?;
}
// 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(),
}
}
}
/// 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");
secret_keys[key_index].sign(Hash256::from_slice(message))
};
TestingAttesterSlashingBuilder::double_vote(
test_task,
validator_indices,
signer,
fork,
genesis_validators_root,
spec,
)
}

View File

@@ -1,194 +0,0 @@
use super::super::generate_deterministic_keypairs;
use crate::test_utils::{AttestationTestTask, TestingPendingAttestationBuilder};
use crate::*;
use bls::get_withdrawal_credentials;
use log::debug;
use rayon::prelude::*;
pub const KEYPAIRS_FILE: &str = "keypairs.raw_keypairs";
/// 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> {
/// 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.saturating_sub(3u64);
state.current_justified_checkpoint.epoch = epoch.saturating_sub(2u64);
state.justification_bits = BitVector::from_bytes(vec![0b0000_1111]).unwrap();
state.finalized_checkpoint.epoch = state.previous_justified_checkpoint.epoch;
}
/// 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

@@ -1,64 +0,0 @@
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::deserialize(&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

@@ -1,60 +0,0 @@
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

@@ -1,82 +0,0 @@
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(),
};
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(),
};
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

@@ -1,34 +0,0 @@
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

@@ -1,15 +1,50 @@
#![allow(clippy::integer_arithmetic)]
#[macro_use]
mod macros;
mod builders;
mod generate_deterministic_keypairs;
mod test_random;
use std::fmt::Debug;
pub use rand::{RngCore, SeedableRng};
pub use rand_xorshift::XorShiftRng;
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 rand::{RngCore, SeedableRng};
pub use rand_xorshift::XorShiftRng;
use ssz::{ssz_encode, Decode, Encode};
pub use test_random::{test_random_instance, TestRandom};
use tree_hash::TreeHash;
#[macro_use]
mod macros;
mod generate_deterministic_keypairs;
mod test_random;
pub fn test_ssz_tree_hash_pair<T, U>(v1: &T, v2: &U)
where
T: TreeHash + Encode + Decode + Debug + PartialEq,
U: TreeHash + Encode + Decode + Debug + PartialEq,
{
test_ssz_tree_hash_pair_with(v1, v2, T::from_ssz_bytes)
}
pub fn test_ssz_tree_hash_pair_with<T, U>(
v1: &T,
v2: &U,
t_decoder: impl FnOnce(&[u8]) -> Result<T, ssz::DecodeError>,
) where
T: TreeHash + Encode + Debug + PartialEq,
U: TreeHash + Encode + Decode + Debug + PartialEq,
{
// SSZ encoding should agree between the two types.
let encoding1 = ssz_encode(v1);
let encoding2 = ssz_encode(v2);
assert_eq!(encoding1, encoding2);
// Decoding the encoding should yield either value.
let decoded1 = t_decoder(&encoding1).unwrap();
assert_eq!(&decoded1, v1);
let decoded2 = U::from_ssz_bytes(&encoding1).unwrap();
assert_eq!(&decoded2, v2);
// Tree hashing should agree.
assert_eq!(v1.tree_hash_root(), v2.tree_hash_root());
}

View File

@@ -41,6 +41,12 @@ impl TestRandom for u32 {
}
}
impl TestRandom for u8 {
fn random_for_test(rng: &mut impl RngCore) -> Self {
rng.next_u32().to_be_bytes()[0]
}
}
impl TestRandom for usize {
fn random_for_test(rng: &mut impl RngCore) -> Self {
rng.next_u32() as usize
@@ -64,16 +70,15 @@ where
impl<T, N: Unsigned> TestRandom for FixedVector<T, N>
where
T: TestRandom + Default,
T: TestRandom,
{
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()
Self::new(
(0..N::to_usize())
.map(|_| T::random_for_test(rng))
.collect(),
)
.expect("N items provided")
}
}

View File

@@ -61,7 +61,7 @@ impl Validator {
spec: &ChainSpec,
) -> bool {
// Placement in queue is finalized
self.activation_eligibility_epoch <= state.finalized_checkpoint.epoch
self.activation_eligibility_epoch <= state.finalized_checkpoint().epoch
// Has not yet been activated
&& self.activation_epoch == spec.far_future_epoch
}