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

@@ -10,8 +10,11 @@ use crate::{
BeaconChain, BeaconChainTypes, BlockError, ChainConfig, ServerSentEventHandler,
StateSkipConfig,
};
use bls::get_withdrawal_credentials;
use futures::channel::mpsc::Receiver;
use genesis::interop_genesis_state;
use int_to_bytes::int_to_bytes32;
use merkle_proof::MerkleTree;
use parking_lot::Mutex;
use rand::rngs::StdRng;
use rand::Rng;
@@ -29,11 +32,12 @@ use task_executor::ShutdownReason;
use tempfile::{tempdir, TempDir};
use tree_hash::TreeHash;
use types::{
AggregateSignature, Attestation, AttestationData, AttesterSlashing, BeaconState,
BeaconStateHash, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Graffiti, Hash256,
IndexedAttestation, Keypair, ProposerSlashing, SelectionProof, SignedAggregateAndProof,
SignedBeaconBlock, SignedBeaconBlockHash, SignedRoot, SignedVoluntaryExit, Slot, SubnetId,
VariableList, VoluntaryExit,
typenum::U4294967296, AggregateSignature, Attestation, AttestationData, AttesterSlashing,
BeaconBlock, BeaconState, BeaconStateHash, ChainSpec, Checkpoint, Deposit, DepositData, Domain,
Epoch, EthSpec, ForkName, Graffiti, Hash256, IndexedAttestation, Keypair, ProposerSlashing,
PublicKeyBytes, SelectionProof, SignatureBytes, SignedAggregateAndProof, SignedBeaconBlock,
SignedBeaconBlockHash, SignedRoot, SignedVoluntaryExit, Slot, SubnetId, VariableList,
VoluntaryExit,
};
pub use types::test_utils::generate_deterministic_keypairs;
@@ -42,6 +46,8 @@ pub use types::test_utils::generate_deterministic_keypairs;
pub const HARNESS_GENESIS_TIME: u64 = 1_567_552_690;
// This parameter is required by a builder but not used because we use the `TestingSlotClock`.
pub const HARNESS_SLOT_TIME: Duration = Duration::from_secs(1);
// Environment variable to read if `fork_from_env` feature is enabled.
const FORK_NAME_ENV_VAR: &str = "FORK_NAME";
pub type BaseHarnessType<TEthSpec, THotStore, TColdStore> =
Witness<TestingSlotClock, CachingEth1Backend<TEthSpec>, TEthSpec, THotStore, TColdStore>;
@@ -106,6 +112,29 @@ pub fn test_logger() -> Logger {
}
}
/// Return a `ChainSpec` suitable for test usage.
///
/// If the `fork_from_env` feature is enabled, read the fork to use from the FORK_NAME environment
/// variable. Otherwise use the default spec.
pub fn test_spec<E: EthSpec>() -> ChainSpec {
if cfg!(feature = "fork_from_env") {
let fork_name = std::env::var(FORK_NAME_ENV_VAR).unwrap_or_else(|e| {
panic!(
"{} env var must be defined when using fork_from_env: {:?}",
FORK_NAME_ENV_VAR, e
)
});
let fork = match fork_name.as_str() {
"base" => ForkName::Base,
"altair" => ForkName::Altair,
other => panic!("unknown FORK_NAME: {}", other),
};
fork.make_genesis_spec(E::default_spec())
} else {
E::default_spec()
}
}
/// A testing harness which can instantiate a `BeaconChain` and populate it with blocks and
/// attestations.
///
@@ -121,15 +150,20 @@ pub struct BeaconChainHarness<T: BeaconChainTypes> {
pub rng: Mutex<StdRng>,
}
type HarnessAttestations<E> = Vec<(
pub type HarnessAttestations<E> = Vec<(
Vec<(Attestation<E>, SubnetId)>,
Option<SignedAggregateAndProof<E>>,
)>;
impl<E: EthSpec> BeaconChainHarness<EphemeralHarnessType<E>> {
pub fn new(eth_spec_instance: E, validator_keypairs: Vec<Keypair>) -> Self {
pub fn new(
eth_spec_instance: E,
spec: Option<ChainSpec>,
validator_keypairs: Vec<Keypair>,
) -> Self {
Self::new_with_store_config(
eth_spec_instance,
spec,
validator_keypairs,
StoreConfig::default(),
)
@@ -137,6 +171,7 @@ impl<E: EthSpec> BeaconChainHarness<EphemeralHarnessType<E>> {
pub fn new_with_store_config(
eth_spec_instance: E,
spec: Option<ChainSpec>,
validator_keypairs: Vec<Keypair>,
config: StoreConfig,
) -> Self {
@@ -144,18 +179,26 @@ impl<E: EthSpec> BeaconChainHarness<EphemeralHarnessType<E>> {
// committee are required to produce an aggregate. This is overkill, however with small
// validator counts it's the only way to be certain there is _at least one_ aggregator per
// committee.
Self::new_with_target_aggregators(eth_spec_instance, validator_keypairs, 1 << 32, config)
Self::new_with_target_aggregators(
eth_spec_instance,
spec,
validator_keypairs,
1 << 32,
config,
)
}
/// Instantiate a new harness with a custom `target_aggregators_per_committee` spec value
pub fn new_with_target_aggregators(
eth_spec_instance: E,
spec: Option<ChainSpec>,
validator_keypairs: Vec<Keypair>,
target_aggregators_per_committee: u64,
store_config: StoreConfig,
) -> Self {
Self::new_with_chain_config(
eth_spec_instance,
spec,
validator_keypairs,
target_aggregators_per_committee,
store_config,
@@ -167,13 +210,14 @@ impl<E: EthSpec> BeaconChainHarness<EphemeralHarnessType<E>> {
/// `target_aggregators_per_committee` spec value, and a `ChainConfig`
pub fn new_with_chain_config(
eth_spec_instance: E,
spec: Option<ChainSpec>,
validator_keypairs: Vec<Keypair>,
target_aggregators_per_committee: u64,
store_config: StoreConfig,
chain_config: ChainConfig,
) -> Self {
let data_dir = tempdir().expect("should create temporary data_dir");
let mut spec = E::default_spec();
let mut spec = spec.unwrap_or_else(test_spec::<E>);
spec.target_aggregators_per_committee = target_aggregators_per_committee;
@@ -221,11 +265,12 @@ impl<E: EthSpec> BeaconChainHarness<DiskHarnessType<E>> {
/// Instantiate a new harness with `validator_count` initial validators.
pub fn new_with_disk_store(
eth_spec_instance: E,
spec: Option<ChainSpec>,
store: Arc<HotColdDB<E, LevelDB<E>, LevelDB<E>>>,
validator_keypairs: Vec<Keypair>,
) -> Self {
let data_dir = tempdir().expect("should create temporary data_dir");
let spec = E::default_spec();
let spec = spec.unwrap_or_else(test_spec::<E>);
let log = test_logger();
let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1);
@@ -265,11 +310,12 @@ impl<E: EthSpec> BeaconChainHarness<DiskHarnessType<E>> {
/// Instantiate a new harness with `validator_count` initial validators.
pub fn resume_from_disk_store(
eth_spec_instance: E,
spec: Option<ChainSpec>,
store: Arc<HotColdDB<E, LevelDB<E>, LevelDB<E>>>,
validator_keypairs: Vec<Keypair>,
data_dir: TempDir,
) -> Self {
let spec = E::default_spec();
let spec = spec.unwrap_or_else(test_spec::<E>);
let log = test_logger();
let (shutdown_tx, shutdown_receiver) = futures::channel::mpsc::channel(1);
@@ -379,7 +425,7 @@ where
slot: Slot,
) -> (SignedBeaconBlock<E>, BeaconState<E>) {
assert_ne!(slot, 0, "can't produce a block at slot 0");
assert!(slot >= state.slot);
assert!(slot >= state.slot());
complete_state_advance(&mut state, None, slot, &self.spec)
.expect("should be able to advance state to slot");
@@ -400,8 +446,8 @@ where
let domain = self.spec.get_domain(
epoch,
Domain::Randao,
&state.fork,
state.genesis_validators_root,
&state.fork(),
state.genesis_validators_root(),
);
let message = epoch.signing_root(domain);
let sk = &self.validator_keypairs[proposer_index].sk;
@@ -415,14 +461,68 @@ where
let signed_block = block.sign(
&self.validator_keypairs[proposer_index].sk,
&state.fork,
state.genesis_validators_root,
&state.fork(),
state.genesis_validators_root(),
&self.spec,
);
(signed_block, state)
}
/// Useful for the `per_block_processing` tests. Creates a block, and returns the state after
/// caches are built but before the generated block is processed.
pub fn make_block_return_pre_state(
&self,
mut state: BeaconState<E>,
slot: Slot,
) -> (SignedBeaconBlock<E>, BeaconState<E>) {
assert_ne!(slot, 0, "can't produce a block at slot 0");
assert!(slot >= state.slot());
complete_state_advance(&mut state, None, slot, &self.spec)
.expect("should be able to advance state to slot");
state
.build_all_caches(&self.spec)
.expect("should build caches");
let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();
// If we produce two blocks for the same slot, they hash up to the same value and
// BeaconChain errors out with `BlockIsAlreadyKnown`. Vary the graffiti so that we produce
// different blocks each time.
let graffiti = Graffiti::from(self.rng.lock().gen::<[u8; 32]>());
let randao_reveal = {
let epoch = slot.epoch(E::slots_per_epoch());
let domain = self.spec.get_domain(
epoch,
Domain::Randao,
&state.fork(),
state.genesis_validators_root(),
);
let message = epoch.signing_root(domain);
let sk = &self.validator_keypairs[proposer_index].sk;
sk.sign(message)
};
let pre_state = state.clone();
let (block, state) = self
.chain
.produce_block_on_state(state, None, slot, randao_reveal, Some(graffiti))
.unwrap();
let signed_block = block.sign(
&self.validator_keypairs[proposer_index].sk,
&state.fork(),
state.genesis_validators_root(),
&self.spec,
);
(signed_block, pre_state)
}
/// A list of attestations for each committee for the given slot.
///
/// The first layer of the Vec is organised per committee. For example, if the return value is
@@ -436,7 +536,7 @@ where
head_block_root: SignedBeaconBlockHash,
attestation_slot: Slot,
) -> Vec<Vec<(Attestation<E>, SubnetId)>> {
let committee_count = state.get_committee_count_at_slot(state.slot).unwrap();
let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap();
state
.get_beacon_committees_at_slot(attestation_slot)
@@ -467,8 +567,8 @@ where
let domain = self.spec.get_domain(
attestation.data.target.epoch,
Domain::BeaconAttester,
&state.fork,
state.genesis_validators_root,
&state.fork(),
state.genesis_validators_root(),
);
let message = attestation.data.signing_root(domain);
@@ -540,65 +640,71 @@ where
slot,
);
let aggregated_attestations: Vec<Option<SignedAggregateAndProof<E>>> = unaggregated_attestations
.iter()
.map(|committee_attestations| {
// If there are any attestations in this committee, create an aggregate.
if let Some((attestation, _)) = committee_attestations.first() {
let bc = state.get_beacon_committee(attestation.data.slot, attestation.data.index)
.unwrap();
let aggregated_attestations: Vec<Option<SignedAggregateAndProof<E>>> =
unaggregated_attestations
.iter()
.map(|committee_attestations| {
// If there are any attestations in this committee, create an aggregate.
if let Some((attestation, _)) = committee_attestations.first() {
let bc = state
.get_beacon_committee(attestation.data.slot, attestation.data.index)
.unwrap();
let aggregator_index = bc.committee
.iter()
.find(|&validator_index| {
if !attesting_validators.contains(validator_index) {
return false
}
// Find an aggregator if one exists. Return `None` if there are no
// aggregators.
let aggregator_index = bc
.committee
.iter()
.find(|&validator_index| {
if !attesting_validators.contains(validator_index) {
return false;
}
let selection_proof = SelectionProof::new::<E>(
state.slot,
&self.validator_keypairs[*validator_index].sk,
&state.fork,
state.genesis_validators_root,
&self.spec,
);
let selection_proof = SelectionProof::new::<E>(
state.slot(),
&self.validator_keypairs[*validator_index].sk,
&state.fork(),
state.genesis_validators_root(),
&self.spec,
);
selection_proof.is_aggregator(bc.committee.len(), &self.spec).unwrap_or(false)
})
.copied()
.unwrap_or_else(|| panic!(
"Committee {} at slot {} with {} attesting validators does not have any aggregators",
bc.index, state.slot, bc.committee.len()
));
// If the chain is able to produce an aggregate, use that. Otherwise, build an
// aggregate locally.
let aggregate = self
.chain
.get_aggregated_attestation(&attestation.data)
.unwrap_or_else(|| {
committee_attestations.iter().skip(1).fold(attestation.clone(), |mut agg, (att, _)| {
agg.aggregate(att);
agg
selection_proof
.is_aggregator(bc.committee.len(), &self.spec)
.unwrap_or(false)
})
});
.copied()?;
let signed_aggregate = SignedAggregateAndProof::from_aggregate(
aggregator_index as u64,
aggregate,
None,
&self.validator_keypairs[aggregator_index].sk,
&state.fork,
state.genesis_validators_root,
&self.spec,
);
// If the chain is able to produce an aggregate, use that. Otherwise, build an
// aggregate locally.
let aggregate = self
.chain
.get_aggregated_attestation(&attestation.data)
.unwrap_or_else(|| {
committee_attestations.iter().skip(1).fold(
attestation.clone(),
|mut agg, (att, _)| {
agg.aggregate(att);
agg
},
)
});
Some(signed_aggregate)
}
else {
None
}
}).collect();
let signed_aggregate = SignedAggregateAndProof::from_aggregate(
aggregator_index as u64,
aggregate,
None,
&self.validator_keypairs[aggregator_index].sk,
&state.fork(),
state.genesis_validators_root(),
&self.spec,
);
Some(signed_aggregate)
} else {
None
}
})
.collect();
unaggregated_attestations
.into_iter()
@@ -653,12 +759,70 @@ where
}
}
pub fn make_attester_slashing_different_indices(
&self,
validator_indices_1: Vec<u64>,
validator_indices_2: Vec<u64>,
) -> AttesterSlashing<E> {
let data = AttestationData {
slot: Slot::new(0),
index: 0,
beacon_block_root: Hash256::zero(),
target: Checkpoint {
root: Hash256::zero(),
epoch: Epoch::new(0),
},
source: Checkpoint {
root: Hash256::zero(),
epoch: Epoch::new(0),
},
};
let mut attestation_1 = IndexedAttestation {
attesting_indices: VariableList::new(validator_indices_1).unwrap(),
data: data.clone(),
signature: AggregateSignature::infinity(),
};
let mut attestation_2 = IndexedAttestation {
attesting_indices: VariableList::new(validator_indices_2).unwrap(),
data,
signature: AggregateSignature::infinity(),
};
attestation_2.data.index += 1;
for attestation in &mut [&mut attestation_1, &mut attestation_2] {
for &i in &attestation.attesting_indices {
let sk = &self.validator_keypairs[i as usize].sk;
let fork = self.chain.head_info().unwrap().fork;
let genesis_validators_root = self.chain.genesis_validators_root;
let domain = self.chain.spec.get_domain(
attestation.data.target.epoch,
Domain::BeaconAttester,
&fork,
genesis_validators_root,
);
let message = attestation.data.signing_root(domain);
attestation.signature.add_assign(&sk.sign(message));
}
}
AttesterSlashing {
attestation_1,
attestation_2,
}
}
pub fn make_proposer_slashing(&self, validator_index: u64) -> ProposerSlashing {
let mut block_header_1 = self
.chain
.head_beacon_block()
.unwrap()
.message
.message()
.block_header();
block_header_1.proposer_index = validator_index;
@@ -694,6 +858,116 @@ where
.sign(sk, &fork, genesis_validators_root, &self.chain.spec)
}
pub fn add_voluntary_exit(
&self,
block: &mut BeaconBlock<E>,
validator_index: u64,
epoch: Epoch,
) {
let exit = self.make_voluntary_exit(validator_index, epoch);
block.body_mut().voluntary_exits_mut().push(exit).unwrap();
}
/// Create a new block, apply `block_modifier` to it, sign it and return it.
///
/// The state returned is a pre-block state at the same slot as the produced block.
pub fn make_block_with_modifier(
&self,
state: BeaconState<E>,
slot: Slot,
block_modifier: impl FnOnce(&mut BeaconBlock<E>),
) -> (SignedBeaconBlock<E>, BeaconState<E>) {
assert_ne!(slot, 0, "can't produce a block at slot 0");
assert!(slot >= state.slot());
let (block, state) = self.make_block_return_pre_state(state, slot);
let (mut block, _) = block.deconstruct();
block_modifier(&mut block);
let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap();
let signed_block = block.sign(
&self.validator_keypairs[proposer_index as usize].sk,
&state.fork(),
state.genesis_validators_root(),
&self.spec,
);
(signed_block, state)
}
pub fn make_deposits<'a>(
&self,
state: &'a mut BeaconState<E>,
num_deposits: usize,
invalid_pubkey: Option<PublicKeyBytes>,
invalid_signature: Option<SignatureBytes>,
) -> (Vec<Deposit>, &'a mut BeaconState<E>) {
let mut datas = vec![];
for _ in 0..num_deposits {
let keypair = Keypair::random();
let pubkeybytes = PublicKeyBytes::from(keypair.pk.clone());
let mut data = DepositData {
pubkey: pubkeybytes,
withdrawal_credentials: Hash256::from_slice(
&get_withdrawal_credentials(&keypair.pk, self.spec.bls_withdrawal_prefix_byte)
[..],
),
amount: self.spec.min_deposit_amount,
signature: SignatureBytes::empty(),
};
data.signature = data.create_signature(&keypair.sk, &self.spec);
if let Some(invalid_pubkey) = invalid_pubkey {
data.pubkey = invalid_pubkey;
}
if let Some(invalid_signature) = invalid_signature.clone() {
data.signature = invalid_signature;
}
datas.push(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_mut().deposit_root = deposit_data_list.tree_hash_root();
state.eth1_data_mut().deposit_count = num_deposits as u64;
*state.eth1_deposit_index_mut() = 0;
// Building the merkle tree used for generating proofs
let tree = MerkleTree::create(&leaves[..], self.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, self.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
(deposits, state)
}
pub fn process_block(
&self,
slot: Slot,
@@ -771,13 +1045,8 @@ where
block: &SignedBeaconBlock<E>,
validators: &[usize],
) {
let attestations = self.make_attestations(
validators,
&state,
state_root,
block_hash,
block.message.slot,
);
let attestations =
self.make_attestations(validators, &state, state_root, block_hash, block.slot());
self.process_attestations(attestations);
}
@@ -932,7 +1201,7 @@ where
chain_dump
.iter()
.cloned()
.map(|checkpoint| checkpoint.beacon_state.finalized_checkpoint.root.into())
.map(|checkpoint| checkpoint.beacon_state.finalized_checkpoint().root.into())
.filter(|block_hash| *block_hash != Hash256::zero().into())
.collect()
}