diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..1b0e150ce --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/ef_tests/eth2.0-spec-tests"] + path = tests/ef_tests/eth2.0-spec-tests + url = https://github.com/ethereum/eth2.0-spec-tests diff --git a/Cargo.toml b/Cargo.toml index b4d53d420..e9acb2be4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ members = [ "eth2/utils/bls", "eth2/utils/boolean-bitfield", "eth2/utils/cached_tree_hash", + "eth2/utils/compare_fields", + "eth2/utils/compare_fields_derive", "eth2/utils/fixed_len_vec", "eth2/utils/hashing", "eth2/utils/honey-badger-split", @@ -30,6 +32,7 @@ members = [ "beacon_node/rpc", "beacon_node/version", "beacon_node/beacon_chain", + "tests/ef_tests", "protos", "validator_client", "account_manager", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 63a00747e..76de2d431 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -18,6 +18,7 @@ use state_processing::{ }; use std::sync::Arc; use store::{Error as DBError, Store}; +use tree_hash::TreeHash; use types::*; #[derive(Debug, PartialEq)] @@ -380,8 +381,7 @@ impl BeaconChain { // If required, transition the new state to the present slot. for _ in state.slot.as_u64()..present_slot.as_u64() { // Ensure the next epoch state caches are built in case of an epoch transition. - state.build_epoch_cache(RelativeEpoch::NextWithoutRegistryChange, spec)?; - state.build_epoch_cache(RelativeEpoch::NextWithRegistryChange, spec)?; + state.build_committee_cache(RelativeEpoch::Next, spec)?; per_slot_processing(&mut *state, spec)?; } @@ -463,7 +463,7 @@ impl BeaconChain { pub fn block_proposer(&self, slot: Slot) -> Result { self.state .write() - .build_epoch_cache(RelativeEpoch::Current, &T::EthSpec::spec())?; + .build_committee_cache(RelativeEpoch::Current, &T::EthSpec::spec())?; let index = self.state.read().get_beacon_proposer_index( slot, @@ -489,7 +489,7 @@ impl BeaconChain { if let Some(attestation_duty) = self .state .read() - .get_attestation_duties(validator_index, &T::EthSpec::spec())? + .get_attestation_duties(validator_index, RelativeEpoch::Current)? { Ok(Some((attestation_duty.slot, attestation_duty.shard))) } else { @@ -531,18 +531,21 @@ impl BeaconChain { *self.state.read().get_block_root(current_epoch_start_slot)? }; + let previous_crosslink_root = + Hash256::from_slice(&state.get_current_crosslink(shard)?.tree_hash_root()); + self.metrics.attestation_production_successes.inc(); timer.observe_duration(); Ok(AttestationData { - slot: self.state.read().slot, - shard, beacon_block_root: self.head().beacon_block_root, - target_root, - crosslink_data_root: Hash256::zero(), - previous_crosslink: state.latest_crosslinks[shard as usize].clone(), source_epoch: state.current_justified_epoch, source_root: state.current_justified_root, + target_epoch: state.current_epoch(), + target_root, + shard, + previous_crosslink_root, + crosslink_data_root: Hash256::zero(), }) } @@ -701,7 +704,9 @@ impl BeaconChain { self.fork_choice()?; self.metrics.block_processing_successes.inc(); - self.metrics.operations_per_block_attestation.observe(block.body.attestations.len() as f64); + self.metrics + .operations_per_block_attestation + .observe(block.body.attestations.len() as f64); timer.observe_duration(); Ok(BlockProcessingOutcome::ValidBlock(ValidBlock::Processed)) @@ -742,9 +747,12 @@ impl BeaconChain { randao_reveal, eth1_data: Eth1Data { // TODO: replace with real data + deposit_count: 0, deposit_root: Hash256::zero(), block_hash: Hash256::zero(), }, + // TODO: badass Lighthouse graffiti + graffiti: [0; 32], proposer_slashings, attester_slashings, attestations: self @@ -793,7 +801,7 @@ impl BeaconChain { let justified_root = { let root = self.head().beacon_state.current_justified_root; if root == T::EthSpec::spec().zero_hash { - self.genesis_block_root + self.genesis_block_root } else { root } diff --git a/beacon_node/rpc/src/validator.rs b/beacon_node/rpc/src/validator.rs index 68ce60b96..2cd374d25 100644 --- a/beacon_node/rpc/src/validator.rs +++ b/beacon_node/rpc/src/validator.rs @@ -115,7 +115,9 @@ impl ValidatorService for ValidatorServiceInstance { }; // get attestation duties and check if validator is active - let attestation_duties = match state.get_attestation_duties(val_index, &spec) { + let attestation_duties = match state + .get_attestation_duties(val_index, RelativeEpoch::Current) + { Ok(Some(v)) => v, Ok(_) => { // validator is inactive, go to the next validator diff --git a/eth2/fork_choice/src/bitwise_lmd_ghost.rs b/eth2/fork_choice/src/bitwise_lmd_ghost.rs index 2d1b4e508..f78a8e780 100644 --- a/eth2/fork_choice/src/bitwise_lmd_ghost.rs +++ b/eth2/fork_choice/src/bitwise_lmd_ghost.rs @@ -71,10 +71,8 @@ impl BitwiseLMDGhost { current_state.get_active_validator_indices(block_slot.epoch(spec.slots_per_epoch)); for index in active_validator_indices { - let balance = std::cmp::min( - current_state.validator_balances[index], - spec.max_deposit_amount, - ) / spec.fork_choice_balance_increment; + let balance = std::cmp::min(current_state.balances[index], spec.max_effective_balance) + / spec.effective_balance_increment; if balance > 0 { if let Some(target) = self.latest_attestation_targets.get(&(index as u64)) { *latest_votes.entry(*target).or_insert_with(|| 0) += balance; diff --git a/eth2/fork_choice/src/optimized_lmd_ghost.rs b/eth2/fork_choice/src/optimized_lmd_ghost.rs index d3f159876..b389d0981 100644 --- a/eth2/fork_choice/src/optimized_lmd_ghost.rs +++ b/eth2/fork_choice/src/optimized_lmd_ghost.rs @@ -69,11 +69,10 @@ impl OptimizedLMDGhost { let active_validator_indices = current_state.get_active_validator_indices(block_slot.epoch(spec.slots_per_epoch)); - let validator_balances = ¤t_state.validator_balances; for index in active_validator_indices { - let balance = std::cmp::min(validator_balances[index], spec.max_deposit_amount) - / spec.fork_choice_balance_increment; + let balance = std::cmp::min(current_state.balances[index], spec.max_effective_balance) + / spec.effective_balance_increment; if balance > 0 { if let Some(target) = self.latest_attestation_targets.get(&(index as u64)) { *latest_votes.entry(*target).or_insert_with(|| 0) += balance; diff --git a/eth2/fork_choice/src/slow_lmd_ghost.rs b/eth2/fork_choice/src/slow_lmd_ghost.rs index 38b1e8dab..7b5114887 100644 --- a/eth2/fork_choice/src/slow_lmd_ghost.rs +++ b/eth2/fork_choice/src/slow_lmd_ghost.rs @@ -43,10 +43,8 @@ impl SlowLMDGhost { current_state.get_active_validator_indices(block_slot.epoch(spec.slots_per_epoch)); for index in active_validator_indices { - let balance = std::cmp::min( - current_state.validator_balances[index], - spec.max_deposit_amount, - ) / spec.fork_choice_balance_increment; + let balance = std::cmp::min(current_state.balances[index], spec.max_effective_balance) + / spec.effective_balance_increment; if balance > 0 { if let Some(target) = self.latest_attestation_targets.get(&(index as u64)) { *latest_votes.entry(*target).or_insert_with(|| 0) += balance; diff --git a/eth2/fork_choice/tests/tests.rs b/eth2/fork_choice/tests/tests.rs index 2063ccc67..a7d2ec7aa 100644 --- a/eth2/fork_choice/tests/tests.rs +++ b/eth2/fork_choice/tests/tests.rs @@ -64,6 +64,7 @@ fn test_yaml_vectors>( let spec = FoundationEthSpec::spec(); let zero_hash = Hash256::zero(); let eth1_data = Eth1Data { + deposit_count: 0, deposit_root: zero_hash.clone(), block_hash: zero_hash.clone(), }; @@ -72,6 +73,7 @@ fn test_yaml_vectors>( let body = BeaconBlockBody { eth1_data, randao_reveal, + graffiti: [0; 32], proposer_slashings: vec![], attester_slashings: vec![], attestations: vec![], diff --git a/eth2/operation_pool/src/lib.rs b/eth2/operation_pool/src/lib.rs index c5653e7f9..0affba3f4 100644 --- a/eth2/operation_pool/src/lib.rs +++ b/eth2/operation_pool/src/lib.rs @@ -6,10 +6,12 @@ use state_processing::per_block_processing::errors::{ AttestationValidationError, AttesterSlashingValidationError, DepositValidationError, ExitValidationError, ProposerSlashingValidationError, TransferValidationError, }; +#[cfg(not(test))] +use state_processing::per_block_processing::verify_deposit_merkle_proof; use state_processing::per_block_processing::{ - gather_attester_slashing_indices_modular, validate_attestation, - validate_attestation_time_independent_only, verify_attester_slashing, verify_deposit, - verify_exit, verify_exit_time_independent_only, verify_proposer_slashing, verify_transfer, + get_slashable_indices_modular, validate_attestation, + validate_attestation_time_independent_only, verify_attester_slashing, verify_exit, + verify_exit_time_independent_only, verify_proposer_slashing, verify_transfer, verify_transfer_time_independent_only, }; use std::collections::{btree_map::Entry, hash_map, BTreeMap, HashMap, HashSet}; @@ -20,11 +22,6 @@ use types::{ EthSpec, ProposerSlashing, Transfer, Validator, VoluntaryExit, }; -#[cfg(test)] -const VERIFY_DEPOSIT_PROOFS: bool = false; -#[cfg(not(test))] -const VERIFY_DEPOSIT_PROOFS: bool = false; // TODO: enable this - #[derive(Default)] pub struct OperationPool { /// Map from attestation ID (see below) to vectors of attestations. @@ -60,7 +57,7 @@ impl AttestationId { spec: &ChainSpec, ) -> Self { let mut bytes = ssz_encode(attestation); - let epoch = attestation.slot.epoch(spec.slots_per_epoch); + let epoch = attestation.target_epoch; bytes.extend_from_slice(&AttestationId::compute_domain_bytes(epoch, state, spec)); AttestationId(bytes) } @@ -85,19 +82,13 @@ impl AttestationId { /// receive for including it in a block. // TODO: this could be optimised with a map from validator index to whether that validator has // attested in each of the current and previous epochs. Currently quadractic in number of validators. -fn attestation_score( - attestation: &Attestation, - state: &BeaconState, - spec: &ChainSpec, -) -> usize { +fn attestation_score(attestation: &Attestation, state: &BeaconState) -> usize { // Bitfield of validators whose attestations are new/fresh. let mut new_validators = attestation.aggregation_bitfield.clone(); - let attestation_epoch = attestation.data.slot.epoch(spec.slots_per_epoch); - - let state_attestations = if attestation_epoch == state.current_epoch(spec) { + let state_attestations = if attestation.data.target_epoch == state.current_epoch() { &state.current_epoch_attestations - } else if attestation_epoch == state.previous_epoch(spec) { + } else if attestation.data.target_epoch == state.previous_epoch() { &state.previous_epoch_attestations } else { return 0; @@ -181,8 +172,8 @@ impl OperationPool { /// Get a list of attestations for inclusion in a block. pub fn get_attestations(&self, state: &BeaconState, spec: &ChainSpec) -> Vec { // Attestations for the current fork, which may be from the current or previous epoch. - let prev_epoch = state.previous_epoch(spec); - let current_epoch = state.current_epoch(spec); + let prev_epoch = state.previous_epoch(); + let current_epoch = state.current_epoch(); let prev_domain_bytes = AttestationId::compute_domain_bytes(prev_epoch, state, spec); let curr_domain_bytes = AttestationId::compute_domain_bytes(current_epoch, state, spec); self.attestations @@ -199,7 +190,7 @@ impl OperationPool { .filter(|attestation| validate_attestation(state, attestation, spec).is_ok()) // Scored by the number of new attestations they introduce (descending) // TODO: need to consider attestations introduced in THIS block - .map(|att| (att, attestation_score(att, state, spec))) + .map(|att| (att, attestation_score(att, state))) // Don't include any useless attestations (score 0) .filter(|&(_, score)| score != 0) .sorted_by_key(|&(_, score)| std::cmp::Reverse(score)) @@ -211,15 +202,16 @@ impl OperationPool { } /// Remove attestations which are too old to be included in a block. - // TODO: we could probably prune other attestations here: - // - ones that are completely covered by attestations included in the state - // - maybe ones invalidated by the confirmation of one fork over another - pub fn prune_attestations(&self, finalized_state: &BeaconState, spec: &ChainSpec) { + pub fn prune_attestations(&self, finalized_state: &BeaconState) { + // We know we can include an attestation if: + // state.slot <= attestation_slot + SLOTS_PER_EPOCH + // We approximate this check using the attestation's epoch, to avoid computing + // the slot or relying on the committee cache of the finalized state. self.attestations.write().retain(|_, attestations| { // All the attestations in this bucket have the same data, so we only need to // check the first one. attestations.first().map_or(false, |att| { - finalized_state.slot < att.data.slot + spec.slots_per_epoch + finalized_state.current_epoch() <= att.data.target_epoch + 1 }) }); } @@ -227,6 +219,7 @@ impl OperationPool { /// Add a deposit to the pool. /// /// No two distinct deposits should be added with the same index. + #[cfg_attr(test, allow(unused_variables))] pub fn insert_deposit( &self, deposit: Deposit, @@ -237,7 +230,9 @@ impl OperationPool { match self.deposits.write().entry(deposit.index) { Entry::Vacant(entry) => { - verify_deposit(state, &deposit, VERIFY_DEPOSIT_PROOFS, spec)?; + // TODO: fix tests to generate valid merkle proofs + #[cfg(not(test))] + verify_deposit_merkle_proof(state, &deposit, spec)?; entry.insert(deposit); Ok(Fresh) } @@ -245,7 +240,9 @@ impl OperationPool { if entry.get() == &deposit { Ok(Duplicate) } else { - verify_deposit(state, &deposit, VERIFY_DEPOSIT_PROOFS, spec)?; + // TODO: fix tests to generate valid merkle proofs + #[cfg(not(test))] + verify_deposit_merkle_proof(state, &deposit, spec)?; Ok(Replaced(Box::new(entry.insert(deposit)))) } } @@ -256,6 +253,7 @@ impl OperationPool { /// /// Take at most the maximum number of deposits, beginning from the current deposit index. pub fn get_deposits(&self, state: &BeaconState, spec: &ChainSpec) -> Vec { + // TODO: might want to re-check the Merkle proof to account for Eth1 forking let start_idx = state.deposit_index; (start_idx..start_idx + spec.max_deposits) .map(|idx| self.deposits.read().get(&idx).cloned()) @@ -300,8 +298,8 @@ impl OperationPool { spec: &ChainSpec, ) -> (AttestationId, AttestationId) { ( - AttestationId::from_data(&slashing.slashable_attestation_1.data, state, spec), - AttestationId::from_data(&slashing.slashable_attestation_2.data, state, spec), + AttestationId::from_data(&slashing.attestation_1.data, state, spec), + AttestationId::from_data(&slashing.attestation_2.data, state, spec), ) } @@ -356,12 +354,10 @@ impl OperationPool { }) .filter(|(_, slashing)| { // Take all slashings that will slash 1 or more validators. - let slashed_validators = gather_attester_slashing_indices_modular( - state, - slashing, - |index, validator| validator.slashed || to_be_slashed.contains(&index), - spec, - ); + let slashed_validators = + get_slashable_indices_modular(state, slashing, |index, validator| { + validator.slashed || to_be_slashed.contains(&index) + }); // Extend the `to_be_slashed` set so subsequent iterations don't try to include // useless slashings. @@ -380,12 +376,11 @@ impl OperationPool { } /// Prune proposer slashings for all slashed or withdrawn validators. - pub fn prune_proposer_slashings(&self, finalized_state: &BeaconState, spec: &ChainSpec) { + pub fn prune_proposer_slashings(&self, finalized_state: &BeaconState) { prune_validator_hash_map( &mut self.proposer_slashings.write(), |validator| { - validator.slashed - || validator.is_withdrawable_at(finalized_state.current_epoch(spec)) + validator.slashed || validator.is_withdrawable_at(finalized_state.current_epoch()) }, finalized_state, ); @@ -396,14 +391,12 @@ impl OperationPool { pub fn prune_attester_slashings(&self, finalized_state: &BeaconState, spec: &ChainSpec) { self.attester_slashings.write().retain(|id, slashing| { let fork_ok = &Self::attester_slashing_id(slashing, finalized_state, spec) == id; - let curr_epoch = finalized_state.current_epoch(spec); - let slashing_ok = gather_attester_slashing_indices_modular( - finalized_state, - slashing, - |_, validator| validator.slashed || validator.is_withdrawable_at(curr_epoch), - spec, - ) - .is_ok(); + let curr_epoch = finalized_state.current_epoch(); + let slashing_ok = + get_slashable_indices_modular(finalized_state, slashing, |_, validator| { + validator.slashed || validator.is_withdrawable_at(curr_epoch) + }) + .is_ok(); fork_ok && slashing_ok }); } @@ -436,10 +429,10 @@ impl OperationPool { } /// Prune if validator has already exited at the last finalized state. - pub fn prune_voluntary_exits(&self, finalized_state: &BeaconState, spec: &ChainSpec) { + pub fn prune_voluntary_exits(&self, finalized_state: &BeaconState) { prune_validator_hash_map( &mut self.voluntary_exits.write(), - |validator| validator.is_exited_at(finalized_state.current_epoch(spec)), + |validator| validator.is_exited_at(finalized_state.current_epoch()), finalized_state, ); } @@ -482,11 +475,11 @@ impl OperationPool { /// Prune all types of transactions given the latest finalized state. pub fn prune_all(&self, finalized_state: &BeaconState, spec: &ChainSpec) { - self.prune_attestations(finalized_state, spec); + self.prune_attestations(finalized_state); self.prune_deposits(finalized_state); - self.prune_proposer_slashings(finalized_state, spec); + self.prune_proposer_slashings(finalized_state); self.prune_attester_slashings(finalized_state, spec); - self.prune_voluntary_exits(finalized_state, spec); + self.prune_voluntary_exits(finalized_state); self.prune_transfers(finalized_state); } } @@ -566,8 +559,8 @@ mod tests { let rng = &mut XorShiftRng::from_seed([42; 16]); let (ref spec, ref state) = test_state(rng); let op_pool = OperationPool::new(); - let deposit1 = make_deposit(rng, state, spec); - let mut deposit2 = make_deposit(rng, state, spec); + let deposit1 = make_deposit(rng); + let mut deposit2 = make_deposit(rng); deposit2.index = deposit1.index; assert_eq!( @@ -595,7 +588,7 @@ mod tests { let offset = 1; assert!(offset <= extra); - let deposits = dummy_deposits(rng, &state, &spec, start, max_deposits + extra); + let deposits = dummy_deposits(rng, start, max_deposits + extra); for deposit in &deposits { assert_eq!( @@ -626,8 +619,8 @@ mod tests { let gap = 25; let start2 = start1 + count + gap; - let deposits1 = dummy_deposits(rng, &state, &spec, start1, count); - let deposits2 = dummy_deposits(rng, &state, &spec, start2, count); + let deposits1 = dummy_deposits(rng, start1, count); + let deposits2 = dummy_deposits(rng, start2, count); for d in deposits1.into_iter().chain(deposits2) { assert!(op_pool.insert_deposit(d, &state, &spec).is_ok()); @@ -665,38 +658,14 @@ mod tests { assert_eq!(op_pool.num_deposits(), 0); } - // Create a random deposit (with a valid proof of posession) - fn make_deposit( - rng: &mut XorShiftRng, - state: &BeaconState, - spec: &ChainSpec, - ) -> Deposit { - let keypair = Keypair::random(); - let mut deposit = Deposit::random_for_test(rng); - let mut deposit_input = DepositInput { - pubkey: keypair.pk.clone(), - withdrawal_credentials: Hash256::zero(), - proof_of_possession: Signature::empty_signature(), - }; - deposit_input.proof_of_possession = deposit_input.create_proof_of_possession( - &keypair.sk, - state.slot.epoch(spec.slots_per_epoch), - &state.fork, - spec, - ); - deposit.deposit_data.deposit_input = deposit_input; - deposit + // Create a random deposit + fn make_deposit(rng: &mut XorShiftRng) -> Deposit { + Deposit::random_for_test(rng) } // Create `count` dummy deposits with sequential deposit IDs beginning from `start`. - fn dummy_deposits( - rng: &mut XorShiftRng, - state: &BeaconState, - spec: &ChainSpec, - start: u64, - count: u64, - ) -> Vec { - let proto_deposit = make_deposit(rng, state, spec); + fn dummy_deposits(rng: &mut XorShiftRng, start: u64, count: u64) -> Vec { + let proto_deposit = make_deposit(rng); (start..start + count) .map(|index| { let mut deposit = proto_deposit.clone(); @@ -723,7 +692,8 @@ mod tests { /// Create a signed attestation for use in tests. /// Signed by all validators in `committee[signing_range]` and `committee[extra_signer]`. fn signed_attestation, E: EthSpec>( - committee: &CrosslinkCommittee, + committee: &[usize], + shard: u64, keypairs: &[Keypair], signing_range: R, slot: Slot, @@ -731,18 +701,12 @@ mod tests { spec: &ChainSpec, extra_signer: Option, ) -> Attestation { - let mut builder = TestingAttestationBuilder::new( - state, - &committee.committee, - slot, - committee.shard, - spec, - ); - let signers = &committee.committee[signing_range]; + let mut builder = TestingAttestationBuilder::new(state, committee, slot, shard, spec); + let signers = &committee[signing_range]; let committee_keys = signers.iter().map(|&i| &keypairs[i].sk).collect::>(); builder.sign(signers, &committee_keys, &state.fork, spec); extra_signer.map(|c_idx| { - let validator_index = committee.committee[c_idx]; + let validator_index = committee[c_idx]; builder.sign( &[validator_index], &[&keypairs[validator_index].sk], @@ -760,7 +724,7 @@ mod tests { let spec = E::spec(); let num_validators = - num_committees * (spec.slots_per_epoch * spec.target_committee_size) as usize; + num_committees * spec.slots_per_epoch as usize * spec.target_committee_size; let mut state_builder = TestingBeaconStateBuilder::from_default_keypairs_file_if_exists( num_validators, &spec, @@ -774,18 +738,6 @@ mod tests { (state, keypairs, FoundationEthSpec::spec()) } - /// Set the latest crosslink in the state to match the attestation. - fn fake_latest_crosslink( - att: &Attestation, - state: &mut BeaconState, - spec: &ChainSpec, - ) { - state.latest_crosslinks[att.data.shard as usize] = Crosslink { - crosslink_data_root: att.data.crosslink_data_root, - epoch: att.data.slot.epoch(spec.slots_per_epoch), - }; - } - #[test] fn test_attestation_score() { let (ref mut state, ref keypairs, ref spec) = @@ -793,27 +745,47 @@ mod tests { let slot = state.slot - 1; let committees = state - .get_crosslink_committees_at_slot(slot, spec) + .get_crosslink_committees_at_slot(slot) .unwrap() - .clone(); + .into_iter() + .map(CrosslinkCommittee::into_owned) + .collect::>(); - for committee in committees { - let att1 = signed_attestation(&committee, keypairs, ..2, slot, state, spec, None); - let att2 = signed_attestation(&committee, keypairs, .., slot, state, spec, None); + for cc in committees { + let att1 = signed_attestation( + &cc.committee, + cc.shard, + keypairs, + ..2, + slot, + state, + spec, + None, + ); + let att2 = signed_attestation( + &cc.committee, + cc.shard, + keypairs, + .., + slot, + state, + spec, + None, + ); assert_eq!( att1.aggregation_bitfield.num_set_bits(), - attestation_score(&att1, state, spec) + attestation_score(&att1, state) ); - state - .current_epoch_attestations - .push(PendingAttestation::from_attestation(&att1, state.slot)); + state.current_epoch_attestations.push(PendingAttestation { + aggregation_bitfield: att1.aggregation_bitfield.clone(), + data: att1.data.clone(), + inclusion_delay: 0, + proposer_index: 0, + }); - assert_eq!( - committee.committee.len() - 2, - attestation_score(&att2, state, spec) - ); + assert_eq!(cc.committee.len() - 2, attestation_score(&att2, state)); } } @@ -827,9 +799,11 @@ mod tests { let slot = state.slot - 1; let committees = state - .get_crosslink_committees_at_slot(slot, spec) + .get_crosslink_committees_at_slot(slot) .unwrap() - .clone(); + .into_iter() + .map(CrosslinkCommittee::into_owned) + .collect::>(); assert_eq!( committees.len(), @@ -837,11 +811,12 @@ mod tests { "we expect just one committee with this many validators" ); - for committee in &committees { + for cc in &committees { let step_size = 2; - for i in (0..committee.committee.len()).step_by(step_size) { + for i in (0..cc.committee.len()).step_by(step_size) { let att = signed_attestation( - committee, + &cc.committee, + cc.shard, keypairs, i..i + step_size, slot, @@ -849,7 +824,6 @@ mod tests { spec, None, ); - fake_latest_crosslink(&att, state, spec); op_pool.insert_attestation(att, state, spec).unwrap(); } } @@ -873,13 +847,13 @@ mod tests { ); // Prune attestations shouldn't do anything at this point. - op_pool.prune_attestations(state, spec); + op_pool.prune_attestations(state); assert_eq!(op_pool.num_attestations(), committees.len()); - // But once we advance to an epoch after the attestation, it should prune it out of - // existence. - state.slot = slot + spec.slots_per_epoch; - op_pool.prune_attestations(state, spec); + // But once we advance to more than an epoch after the attestation, it should prune it + // out of existence. + state.slot += 2 * spec.slots_per_epoch; + op_pool.prune_attestations(state); assert_eq!(op_pool.num_attestations(), 0); } @@ -893,13 +867,23 @@ mod tests { let slot = state.slot - 1; let committees = state - .get_crosslink_committees_at_slot(slot, spec) + .get_crosslink_committees_at_slot(slot) .unwrap() - .clone(); + .into_iter() + .map(CrosslinkCommittee::into_owned) + .collect::>(); - for committee in &committees { - let att = signed_attestation(committee, keypairs, .., slot, state, spec, None); - fake_latest_crosslink(&att, state, spec); + for cc in &committees { + let att = signed_attestation( + &cc.committee, + cc.shard, + keypairs, + .., + slot, + state, + spec, + None, + ); op_pool .insert_attestation(att.clone(), state, spec) .unwrap(); @@ -920,17 +904,20 @@ mod tests { let slot = state.slot - 1; let committees = state - .get_crosslink_committees_at_slot(slot, spec) + .get_crosslink_committees_at_slot(slot) .unwrap() - .clone(); + .into_iter() + .map(CrosslinkCommittee::into_owned) + .collect::>(); let step_size = 2; - for committee in &committees { + for cc in &committees { // Create attestations that overlap on `step_size` validators, like: // {0,1,2,3}, {2,3,4,5}, {4,5,6,7}, ... - for i in (0..committee.committee.len() - step_size).step_by(step_size) { + for i in (0..cc.committee.len() - step_size).step_by(step_size) { let att = signed_attestation( - committee, + &cc.committee, + cc.shard, keypairs, i..i + 2 * step_size, slot, @@ -938,7 +925,6 @@ mod tests { spec, None, ); - fake_latest_crosslink(&att, state, spec); op_pool.insert_attestation(att, state, spec).unwrap(); } } @@ -966,17 +952,20 @@ mod tests { let slot = state.slot - 1; let committees = state - .get_crosslink_committees_at_slot(slot, spec) + .get_crosslink_committees_at_slot(slot) .unwrap() - .clone(); + .into_iter() + .map(CrosslinkCommittee::into_owned) + .collect::>(); let max_attestations = spec.max_attestations as usize; let target_committee_size = spec.target_committee_size as usize; - let mut insert_attestations = |committee, step_size| { + let insert_attestations = |cc: &OwnedCrosslinkCommittee, step_size| { for i in (0..target_committee_size).step_by(step_size) { let att = signed_attestation( - committee, + &cc.committee, + cc.shard, keypairs, i..i + step_size, slot, @@ -984,7 +973,6 @@ mod tests { spec, if i == 0 { None } else { Some(0) }, ); - fake_latest_crosslink(&att, state, spec); op_pool.insert_attestation(att, state, spec).unwrap(); } }; diff --git a/eth2/state_processing/Cargo.toml b/eth2/state_processing/Cargo.toml index 0fc7910c8..fa42671d9 100644 --- a/eth2/state_processing/Cargo.toml +++ b/eth2/state_processing/Cargo.toml @@ -21,6 +21,7 @@ fnv = "1.0" hashing = { path = "../utils/hashing" } int_to_bytes = { path = "../utils/int_to_bytes" } integer-sqrt = "0.1" +itertools = "0.8" log = "0.4" merkle_proof = { path = "../utils/merkle_proof" } ssz = { path = "../utils/ssz" } diff --git a/eth2/state_processing/benches/bench_block_processing.rs b/eth2/state_processing/benches/bench_block_processing.rs index 80be1828f..978d532f1 100644 --- a/eth2/state_processing/benches/bench_block_processing.rs +++ b/eth2/state_processing/benches/bench_block_processing.rs @@ -207,12 +207,12 @@ pub fn bench_block_processing( let spec = initial_spec.clone(); c.bench( &format!("{}/block_processing", desc), - Benchmark::new("build_previous_state_epoch_cache", move |b| { + Benchmark::new("build_previous_state_committee_cache", move |b| { b.iter_batched( || state.clone(), |mut state| { state - .build_epoch_cache(RelativeEpoch::Previous, &spec) + .build_committee_cache(RelativeEpoch::Previous, &spec) .unwrap(); state }, @@ -227,12 +227,12 @@ pub fn bench_block_processing( let spec = initial_spec.clone(); c.bench( &format!("{}/block_processing", desc), - Benchmark::new("build_current_state_epoch_cache", move |b| { + Benchmark::new("build_current_state_committee_cache", move |b| { b.iter_batched( || state.clone(), |mut state| { state - .build_epoch_cache(RelativeEpoch::Current, &spec) + .build_committee_cache(RelativeEpoch::Current, &spec) .unwrap(); state }, diff --git a/eth2/state_processing/src/common/convert_to_indexed.rs b/eth2/state_processing/src/common/convert_to_indexed.rs new file mode 100644 index 000000000..5492252f7 --- /dev/null +++ b/eth2/state_processing/src/common/convert_to_indexed.rs @@ -0,0 +1,29 @@ +use super::get_attesting_indices; +use itertools::{Either, Itertools}; +use types::*; + +/// Convert `attestation` to (almost) indexed-verifiable form. +/// +/// Spec v0.6.1 +pub fn convert_to_indexed( + state: &BeaconState, + attestation: &Attestation, +) -> Result { + let attesting_indices = + get_attesting_indices(state, &attestation.data, &attestation.aggregation_bitfield)?; + + let (custody_bit_0_indices, custody_bit_1_indices) = + attesting_indices.into_iter().enumerate().partition_map( + |(committee_idx, validator_idx)| match attestation.custody_bitfield.get(committee_idx) { + Ok(true) => Either::Right(validator_idx as u64), + _ => Either::Left(validator_idx as u64), + }, + ); + + Ok(IndexedAttestation { + custody_bit_0_indices, + custody_bit_1_indices, + data: attestation.data.clone(), + signature: attestation.signature.clone(), + }) +} diff --git a/eth2/state_processing/src/common/exit_validator.rs b/eth2/state_processing/src/common/exit_validator.rs deleted file mode 100644 index 529f5e161..000000000 --- a/eth2/state_processing/src/common/exit_validator.rs +++ /dev/null @@ -1,22 +0,0 @@ -use types::{BeaconStateError as Error, *}; - -/// Exit the validator of the given `index`. -/// -/// Spec v0.5.1 -pub fn exit_validator( - state: &mut BeaconState, - validator_index: usize, - spec: &ChainSpec, -) -> Result<(), Error> { - if validator_index >= state.validator_registry.len() { - return Err(Error::UnknownValidator); - } - - let delayed_epoch = state.get_delayed_activation_exit_epoch(state.current_epoch(spec), spec); - - if state.validator_registry[validator_index].exit_epoch > delayed_epoch { - state.validator_registry[validator_index].exit_epoch = delayed_epoch; - } - - Ok(()) -} diff --git a/eth2/state_processing/src/common/get_attesting_indices.rs b/eth2/state_processing/src/common/get_attesting_indices.rs new file mode 100644 index 000000000..a45eeb881 --- /dev/null +++ b/eth2/state_processing/src/common/get_attesting_indices.rs @@ -0,0 +1,46 @@ +use crate::common::verify_bitfield_length; +use types::*; + +/// Returns validator indices which participated in the attestation, sorted by increasing index. +/// +/// Spec v0.6.1 +pub fn get_attesting_indices( + state: &BeaconState, + attestation_data: &AttestationData, + bitfield: &Bitfield, +) -> Result, BeaconStateError> { + get_attesting_indices_unsorted(state, attestation_data, bitfield).map(|mut indices| { + // Fast unstable sort is safe because validator indices are unique + indices.sort_unstable(); + indices + }) +} + +/// Returns validator indices which participated in the attestation, unsorted. +/// +/// Spec v0.6.1 +pub fn get_attesting_indices_unsorted( + state: &BeaconState, + attestation_data: &AttestationData, + bitfield: &Bitfield, +) -> Result, BeaconStateError> { + let target_relative_epoch = + RelativeEpoch::from_epoch(state.current_epoch(), attestation_data.target_epoch)?; + + let committee = + state.get_crosslink_committee_for_shard(attestation_data.shard, target_relative_epoch)?; + + if !verify_bitfield_length(&bitfield, committee.committee.len()) { + return Err(BeaconStateError::InvalidBitfield); + } + + Ok(committee + .committee + .iter() + .enumerate() + .filter_map(|(i, validator_index)| match bitfield.get(i) { + Ok(true) => Some(*validator_index), + _ => None, + }) + .collect()) +} diff --git a/eth2/state_processing/src/common/initiate_validator_exit.rs b/eth2/state_processing/src/common/initiate_validator_exit.rs new file mode 100644 index 000000000..996dcdb2a --- /dev/null +++ b/eth2/state_processing/src/common/initiate_validator_exit.rs @@ -0,0 +1,39 @@ +use std::cmp::max; +use types::{BeaconStateError as Error, *}; + +/// Initiate the exit of the validator of the given `index`. +/// +/// Spec v0.6.1 +pub fn initiate_validator_exit( + state: &mut BeaconState, + index: usize, + spec: &ChainSpec, +) -> Result<(), Error> { + if index >= state.validator_registry.len() { + return Err(Error::UnknownValidator); + } + + // Return if the validator already initiated exit + if state.validator_registry[index].exit_epoch != spec.far_future_epoch { + return Ok(()); + } + + // Compute exit queue epoch + let delayed_epoch = state.get_delayed_activation_exit_epoch(state.current_epoch(), spec); + let mut exit_queue_epoch = state + .exit_cache + .max_epoch() + .map_or(delayed_epoch, |epoch| max(epoch, delayed_epoch)); + let exit_queue_churn = state.exit_cache.get_churn_at(exit_queue_epoch); + + if exit_queue_churn >= state.get_churn_limit(spec)? { + exit_queue_epoch += 1; + } + + state.exit_cache.record_validator_exit(exit_queue_epoch); + state.validator_registry[index].exit_epoch = exit_queue_epoch; + state.validator_registry[index].withdrawable_epoch = + exit_queue_epoch + spec.min_validator_withdrawability_delay; + + Ok(()) +} diff --git a/eth2/state_processing/src/common/mod.rs b/eth2/state_processing/src/common/mod.rs index 49898d10f..26302fed0 100644 --- a/eth2/state_processing/src/common/mod.rs +++ b/eth2/state_processing/src/common/mod.rs @@ -1,7 +1,11 @@ -mod exit_validator; +mod convert_to_indexed; +mod get_attesting_indices; +mod initiate_validator_exit; mod slash_validator; mod verify_bitfield; -pub use exit_validator::exit_validator; +pub use convert_to_indexed::convert_to_indexed; +pub use get_attesting_indices::{get_attesting_indices, get_attesting_indices_unsorted}; +pub use initiate_validator_exit::initiate_validator_exit; pub use slash_validator::slash_validator; pub use verify_bitfield::verify_bitfield_length; diff --git a/eth2/state_processing/src/common/slash_validator.rs b/eth2/state_processing/src/common/slash_validator.rs index 63c1e89ad..2a2db1a77 100644 --- a/eth2/state_processing/src/common/slash_validator.rs +++ b/eth2/state_processing/src/common/slash_validator.rs @@ -1,61 +1,45 @@ -use crate::common::exit_validator; +use crate::common::initiate_validator_exit; use types::{BeaconStateError as Error, *}; /// Slash the validator with index ``index``. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn slash_validator( state: &mut BeaconState, - validator_index: usize, + slashed_index: usize, + opt_whistleblower_index: Option, spec: &ChainSpec, ) -> Result<(), Error> { - let current_epoch = state.current_epoch(spec); - - if (validator_index >= state.validator_registry.len()) - | (validator_index >= state.validator_balances.len()) - { + if slashed_index >= state.validator_registry.len() || slashed_index >= state.balances.len() { return Err(BeaconStateError::UnknownValidator); } - let validator = &state.validator_registry[validator_index]; + let current_epoch = state.current_epoch(); - let effective_balance = state.get_effective_balance(validator_index, spec)?; + initiate_validator_exit(state, slashed_index, spec)?; - // A validator that is withdrawn cannot be slashed. - // - // This constraint will be lifted in Phase 0. - if state.slot - >= validator - .withdrawable_epoch - .start_slot(spec.slots_per_epoch) - { - return Err(Error::ValidatorIsWithdrawable); - } - - exit_validator(state, validator_index, spec)?; + state.validator_registry[slashed_index].slashed = true; + state.validator_registry[slashed_index].withdrawable_epoch = + current_epoch + Epoch::from(T::latest_slashed_exit_length()); + let slashed_balance = state.get_effective_balance(slashed_index, spec)?; state.set_slashed_balance( current_epoch, - state.get_slashed_balance(current_epoch)? + effective_balance, + state.get_slashed_balance(current_epoch)? + slashed_balance, )?; - let whistleblower_index = + let proposer_index = state.get_beacon_proposer_index(state.slot, RelativeEpoch::Current, spec)?; - let whistleblower_reward = effective_balance / spec.whistleblower_reward_quotient; + let whistleblower_index = opt_whistleblower_index.unwrap_or(proposer_index); + let whistleblowing_reward = slashed_balance / spec.whistleblowing_reward_quotient; + let proposer_reward = whistleblowing_reward / spec.proposer_reward_quotient; + safe_add_assign!(state.balances[proposer_index], proposer_reward); safe_add_assign!( - state.validator_balances[whistleblower_index as usize], - whistleblower_reward + state.balances[whistleblower_index], + whistleblowing_reward.saturating_sub(proposer_reward) ); - safe_sub_assign!( - state.validator_balances[validator_index], - whistleblower_reward - ); - - state.validator_registry[validator_index].slashed = true; - - state.validator_registry[validator_index].withdrawable_epoch = - current_epoch + Epoch::from(T::LatestSlashedExitLength::to_usize()); + safe_sub_assign!(state.balances[slashed_index], whistleblowing_reward); Ok(()) } diff --git a/eth2/state_processing/src/common/verify_bitfield.rs b/eth2/state_processing/src/common/verify_bitfield.rs index 570a240f1..886269a54 100644 --- a/eth2/state_processing/src/common/verify_bitfield.rs +++ b/eth2/state_processing/src/common/verify_bitfield.rs @@ -4,7 +4,7 @@ use types::*; /// /// Is title `verify_bitfield` in spec. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn verify_bitfield_length(bitfield: &Bitfield, committee_size: usize) -> bool { if bitfield.num_bytes() != ((committee_size + 7) / 8) { return false; diff --git a/eth2/state_processing/src/get_genesis_state.rs b/eth2/state_processing/src/get_genesis_state.rs index 6638b5246..0fe78c1ed 100644 --- a/eth2/state_processing/src/get_genesis_state.rs +++ b/eth2/state_processing/src/get_genesis_state.rs @@ -9,8 +9,8 @@ pub enum GenesisError { /// Returns the genesis `BeaconState` /// -/// Spec v0.5.1 -pub fn get_genesis_state( +/// Spec v0.6.1 +pub fn get_genesis_beacon_state( genesis_validator_deposits: &[Deposit], genesis_time: u64, genesis_eth1_data: Eth1Data, @@ -23,25 +23,23 @@ pub fn get_genesis_state( process_deposits(&mut state, genesis_validator_deposits, spec)?; // Process genesis activations. - for i in 0..state.validator_registry.len() { - if state.get_effective_balance(i, spec)? >= spec.max_deposit_amount { - state.validator_registry[i].activation_epoch = spec.genesis_epoch; + for validator in &mut state.validator_registry { + if validator.effective_balance >= spec.max_effective_balance { + validator.activation_eligibility_epoch = spec.genesis_epoch; + validator.activation_epoch = spec.genesis_epoch; } } // Ensure the current epoch cache is built. - state.build_epoch_cache(RelativeEpoch::Current, spec)?; + state.build_committee_cache(RelativeEpoch::Current, spec)?; // Set all the active index roots to be the genesis active index root. let active_validator_indices = state - .get_cached_active_validator_indices(RelativeEpoch::Current, spec)? + .get_cached_active_validator_indices(RelativeEpoch::Current)? .to_vec(); let genesis_active_index_root = Hash256::from_slice(&active_validator_indices.tree_hash_root()); state.fill_active_index_roots_with(genesis_active_index_root); - // Generate the current shuffling seed. - state.current_shuffling_seed = state.generate_seed(spec.genesis_epoch, spec)?; - Ok(state) } diff --git a/eth2/state_processing/src/lib.rs b/eth2/state_processing/src/lib.rs index 6757b5dbd..e040c1525 100644 --- a/eth2/state_processing/src/lib.rs +++ b/eth2/state_processing/src/lib.rs @@ -7,7 +7,7 @@ pub mod per_block_processing; pub mod per_epoch_processing; pub mod per_slot_processing; -pub use get_genesis_state::get_genesis_state; +pub use get_genesis_state::get_genesis_beacon_state; pub use per_block_processing::{ errors::{BlockInvalid, BlockProcessingError}, per_block_processing, per_block_processing_without_verifying_block_signature, diff --git a/eth2/state_processing/src/per_block_processing.rs b/eth2/state_processing/src/per_block_processing.rs index 251d7cd91..56238f9c2 100644 --- a/eth2/state_processing/src/per_block_processing.rs +++ b/eth2/state_processing/src/per_block_processing.rs @@ -1,21 +1,25 @@ -use crate::common::slash_validator; +use crate::common::{initiate_validator_exit, slash_validator}; use errors::{BlockInvalid as Invalid, BlockProcessingError as Error, IntoWithIndex}; use rayon::prelude::*; use tree_hash::{SignedRoot, TreeHash}; use types::*; pub use self::verify_attester_slashing::{ - gather_attester_slashing_indices, gather_attester_slashing_indices_modular, - verify_attester_slashing, + get_slashable_indices, get_slashable_indices_modular, verify_attester_slashing, }; pub use self::verify_proposer_slashing::verify_proposer_slashing; pub use validate_attestation::{ validate_attestation, validate_attestation_time_independent_only, validate_attestation_without_signature, }; -pub use verify_deposit::{get_existing_validator_index, verify_deposit, verify_deposit_index}; +pub use verify_deposit::{ + get_existing_validator_index, verify_deposit_index, verify_deposit_merkle_proof, + verify_deposit_signature, +}; pub use verify_exit::{verify_exit, verify_exit_time_independent_only}; -pub use verify_slashable_attestation::verify_slashable_attestation; +pub use verify_indexed_attestation::{ + verify_indexed_attestation, verify_indexed_attestation_without_signature, +}; pub use verify_transfer::{ execute_transfer, verify_transfer, verify_transfer_time_independent_only, }; @@ -27,21 +31,16 @@ mod validate_attestation; mod verify_attester_slashing; mod verify_deposit; mod verify_exit; +mod verify_indexed_attestation; mod verify_proposer_slashing; -mod verify_slashable_attestation; mod verify_transfer; -// Set to `true` to check the merkle proof that a deposit is in the eth1 deposit root. -// -// Presently disabled to make testing easier. -const VERIFY_DEPOSIT_MERKLE_PROOFS: bool = false; - /// Updates the state for a new block, whilst validating that the block is valid. /// /// Returns `Ok(())` if the block is valid and the state was successfully updated. Otherwise /// returns an error describing why the block was invalid or how the function failed to execute. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn per_block_processing( state: &mut BeaconState, block: &BeaconBlock, @@ -56,7 +55,7 @@ pub fn per_block_processing( /// Returns `Ok(())` if the block is valid and the state was successfully updated. Otherwise /// returns an error describing why the block was invalid or how the function failed to execute. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn per_block_processing_without_verifying_block_signature( state: &mut BeaconState, block: &BeaconBlock, @@ -71,7 +70,7 @@ pub fn per_block_processing_without_verifying_block_signature( /// Returns `Ok(())` if the block is valid and the state was successfully updated. Otherwise /// returns an error describing why the block was invalid or how the function failed to execute. /// -/// Spec v0.5.1 +/// Spec v0.6.1 fn per_block_processing_signature_optional( mut state: &mut BeaconState, block: &BeaconBlock, @@ -80,15 +79,15 @@ fn per_block_processing_signature_optional( ) -> Result<(), Error> { process_block_header(state, block, spec)?; - // Ensure the current and previous epoch cache is built. - state.build_epoch_cache(RelativeEpoch::Previous, spec)?; - state.build_epoch_cache(RelativeEpoch::Current, spec)?; + // Ensure the current and previous epoch caches are built. + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + state.build_committee_cache(RelativeEpoch::Current, spec)?; if should_verify_block_signature { verify_block_signature(&state, &block, &spec)?; } process_randao(&mut state, &block, &spec)?; - process_eth1_data(&mut state, &block.body.eth1_data)?; + process_eth1_data(&mut state, &block.body.eth1_data, spec)?; process_proposer_slashings(&mut state, &block.body.proposer_slashings, spec)?; process_attester_slashings(&mut state, &block.body.attester_slashings, spec)?; process_attestations(&mut state, &block.body.attestations, spec)?; @@ -101,7 +100,7 @@ fn per_block_processing_signature_optional( /// Processes the block header. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_block_header( state: &mut BeaconState, block: &BeaconBlock, @@ -121,12 +120,17 @@ pub fn process_block_header( state.latest_block_header = block.temporary_block_header(spec); + // Verify proposer is not slashed + let proposer_idx = state.get_beacon_proposer_index(block.slot, RelativeEpoch::Current, spec)?; + let proposer = &state.validator_registry[proposer_idx]; + verify!(!proposer.slashed, Invalid::ProposerSlashed(proposer_idx)); + Ok(()) } /// Verifies the signature of a block. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn verify_block_signature( state: &BeaconState, block: &BeaconBlock, @@ -137,7 +141,7 @@ pub fn verify_block_signature( let domain = spec.get_domain( block.slot.epoch(spec.slots_per_epoch), - Domain::BeaconBlock, + Domain::BeaconProposer, &state.fork, ); @@ -154,7 +158,7 @@ pub fn verify_block_signature( /// Verifies the `randao_reveal` against the block's proposer pubkey and updates /// `state.latest_randao_mixes`. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_randao( state: &mut BeaconState, block: &BeaconBlock, @@ -166,7 +170,7 @@ pub fn process_randao( // Verify the RANDAO is a valid signature of the proposer. verify!( block.body.randao_reveal.verify( - &state.current_epoch(spec).tree_hash_root()[..], + &state.current_epoch().tree_hash_root()[..], spec.get_domain( block.slot.epoch(spec.slots_per_epoch), Domain::Randao, @@ -178,32 +182,29 @@ pub fn process_randao( ); // Update the current epoch RANDAO mix. - state.update_randao_mix(state.current_epoch(spec), &block.body.randao_reveal, spec)?; + state.update_randao_mix(state.current_epoch(), &block.body.randao_reveal)?; Ok(()) } /// Update the `state.eth1_data_votes` based upon the `eth1_data` provided. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_eth1_data( state: &mut BeaconState, eth1_data: &Eth1Data, + spec: &ChainSpec, ) -> Result<(), Error> { - // Attempt to find a `Eth1DataVote` with matching `Eth1Data`. - let matching_eth1_vote_index = state + state.eth1_data_votes.push(eth1_data.clone()); + + let num_votes = state .eth1_data_votes .iter() - .position(|vote| vote.eth1_data == *eth1_data); + .filter(|vote| *vote == eth1_data) + .count() as u64; - // If a vote exists, increment it's `vote_count`. Otherwise, create a new `Eth1DataVote`. - if let Some(index) = matching_eth1_vote_index { - state.eth1_data_votes[index].vote_count += 1; - } else { - state.eth1_data_votes.push(Eth1DataVote { - eth1_data: eth1_data.clone(), - vote_count: 1, - }); + if num_votes * 2 > spec.slots_per_eth1_voting_period { + state.latest_eth1_data = eth1_data.clone(); } Ok(()) @@ -214,7 +215,7 @@ pub fn process_eth1_data( /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_proposer_slashings( state: &mut BeaconState, proposer_slashings: &[ProposerSlashing], @@ -236,18 +237,18 @@ pub fn process_proposer_slashings( // Update the state. for proposer_slashing in proposer_slashings { - slash_validator(state, proposer_slashing.proposer_index as usize, spec)?; + slash_validator(state, proposer_slashing.proposer_index as usize, None, spec)?; } Ok(()) } -/// Validates each `AttesterSlsashing` and updates the state, short-circuiting on an invalid object. +/// Validates each `AttesterSlashing` and updates the state, short-circuiting on an invalid object. /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_attester_slashings( state: &mut BeaconState, attester_slashings: &[AttesterSlashing], @@ -258,42 +259,42 @@ pub fn process_attester_slashings( Invalid::MaxAttesterSlashingsExceed ); - // Verify the `SlashableAttestation`s in parallel (these are the resource-consuming objects, not + // Verify the `IndexedAttestation`s in parallel (these are the resource-consuming objects, not // the `AttesterSlashing`s themselves). - let mut slashable_attestations: Vec<&SlashableAttestation> = + let mut indexed_attestations: Vec<&IndexedAttestation> = Vec::with_capacity(attester_slashings.len() * 2); for attester_slashing in attester_slashings { - slashable_attestations.push(&attester_slashing.slashable_attestation_1); - slashable_attestations.push(&attester_slashing.slashable_attestation_2); + indexed_attestations.push(&attester_slashing.attestation_1); + indexed_attestations.push(&attester_slashing.attestation_2); } - // Verify slashable attestations in parallel. - slashable_attestations + // Verify indexed attestations in parallel. + indexed_attestations .par_iter() .enumerate() - .try_for_each(|(i, slashable_attestation)| { - verify_slashable_attestation(&state, slashable_attestation, spec) + .try_for_each(|(i, indexed_attestation)| { + verify_indexed_attestation(&state, indexed_attestation, spec) .map_err(|e| e.into_with_index(i)) })?; - let all_slashable_attestations_have_been_checked = true; + let all_indexed_attestations_have_been_checked = true; - // Gather the slashable indices and preform the final verification and update the state in series. + // Gather the indexed indices and preform the final verification and update the state in series. for (i, attester_slashing) in attester_slashings.iter().enumerate() { - let should_verify_slashable_attestations = !all_slashable_attestations_have_been_checked; + let should_verify_indexed_attestations = !all_indexed_attestations_have_been_checked; verify_attester_slashing( &state, &attester_slashing, - should_verify_slashable_attestations, + should_verify_indexed_attestations, spec, ) .map_err(|e| e.into_with_index(i))?; - let slashable_indices = gather_attester_slashing_indices(&state, &attester_slashing, spec) - .map_err(|e| e.into_with_index(i))?; + let slashable_indices = + get_slashable_indices(&state, &attester_slashing).map_err(|e| e.into_with_index(i))?; for i in slashable_indices { - slash_validator(state, i as usize, spec)?; + slash_validator(state, i as usize, None, spec)?; } } @@ -305,7 +306,7 @@ pub fn process_attester_slashings( /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_attestations( state: &mut BeaconState, attestations: &[Attestation], @@ -317,7 +318,7 @@ pub fn process_attestations( ); // Ensure the previous epoch cache exists. - state.build_epoch_cache(RelativeEpoch::Previous, spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; // Verify attestations in parallel. attestations @@ -328,13 +329,20 @@ pub fn process_attestations( })?; // Update the state in series. + let proposer_index = + state.get_beacon_proposer_index(state.slot, RelativeEpoch::Current, spec)? as u64; for attestation in attestations { - let pending_attestation = PendingAttestation::from_attestation(attestation, state.slot); - let attestation_epoch = attestation.data.slot.epoch(spec.slots_per_epoch); + let attestation_slot = state.get_attestation_slot(&attestation.data)?; + let pending_attestation = PendingAttestation { + aggregation_bitfield: attestation.aggregation_bitfield.clone(), + data: attestation.data.clone(), + inclusion_delay: (state.slot - attestation_slot).as_u64(), + proposer_index, + }; - if attestation_epoch == state.current_epoch(spec) { + if attestation.data.target_epoch == state.current_epoch() { state.current_epoch_attestations.push(pending_attestation) - } else if attestation_epoch == state.previous_epoch(spec) { + } else { state.previous_epoch_attestations.push(pending_attestation) } } @@ -347,15 +355,19 @@ pub fn process_attestations( /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_deposits( state: &mut BeaconState, deposits: &[Deposit], spec: &ChainSpec, ) -> Result<(), Error> { verify!( - deposits.len() as u64 <= spec.max_deposits, - Invalid::MaxDepositsExceeded + deposits.len() as u64 + == std::cmp::min( + spec.max_deposits, + state.latest_eth1_data.deposit_count - state.deposit_index + ), + Invalid::DepositCountInvalid ); // Verify deposits in parallel. @@ -363,50 +375,53 @@ pub fn process_deposits( .par_iter() .enumerate() .try_for_each(|(i, deposit)| { - verify_deposit(state, deposit, VERIFY_DEPOSIT_MERKLE_PROOFS, spec) - .map_err(|e| e.into_with_index(i)) + verify_deposit_merkle_proof(state, deposit, spec).map_err(|e| e.into_with_index(i)) })?; // Check `state.deposit_index` and update the state in series. for (i, deposit) in deposits.iter().enumerate() { verify_deposit_index(state, deposit).map_err(|e| e.into_with_index(i))?; + state.deposit_index += 1; + // Ensure the state's pubkey cache is fully up-to-date, it will be used to check to see if the // depositing validator already exists in the registry. state.update_pubkey_cache()?; // Get an `Option` where `u64` is the validator index if this deposit public key // already exists in the beacon_state. - // - // This function also verifies the withdrawal credentials. let validator_index = get_existing_validator_index(state, deposit).map_err(|e| e.into_with_index(i))?; - let deposit_data = &deposit.deposit_data; - let deposit_input = &deposit.deposit_data.deposit_input; + let amount = deposit.data.amount; if let Some(index) = validator_index { // Update the existing validator balance. - safe_add_assign!( - state.validator_balances[index as usize], - deposit_data.amount - ); + safe_add_assign!(state.balances[index as usize], amount); } else { + // The signature should be checked for new validators. Return early for a bad + // signature. + if verify_deposit_signature(state, deposit, spec).is_err() { + return Ok(()); + } + // Create a new validator. let validator = Validator { - pubkey: deposit_input.pubkey.clone(), - withdrawal_credentials: deposit_input.withdrawal_credentials, + pubkey: deposit.data.pubkey.clone(), + withdrawal_credentials: deposit.data.withdrawal_credentials, + activation_eligibility_epoch: spec.far_future_epoch, activation_epoch: spec.far_future_epoch, exit_epoch: spec.far_future_epoch, withdrawable_epoch: spec.far_future_epoch, - initiated_exit: false, + effective_balance: std::cmp::min( + amount - amount % spec.effective_balance_increment, + spec.max_effective_balance, + ), slashed: false, }; state.validator_registry.push(validator); - state.validator_balances.push(deposit_data.amount); + state.balances.push(deposit.data.amount); } - - state.deposit_index += 1; } Ok(()) @@ -417,7 +432,7 @@ pub fn process_deposits( /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_exits( state: &mut BeaconState, voluntary_exits: &[VoluntaryExit], @@ -438,7 +453,7 @@ pub fn process_exits( // Update the state in series. for exit in voluntary_exits { - state.initiate_validator_exit(exit.validator_index as usize); + initiate_validator_exit(state, exit.validator_index as usize, spec)?; } Ok(()) @@ -449,7 +464,7 @@ pub fn process_exits( /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_transfers( state: &mut BeaconState, transfers: &[Transfer], diff --git a/eth2/state_processing/src/per_block_processing/errors.rs b/eth2/state_processing/src/per_block_processing/errors.rs index d8627d359..41e6410be 100644 --- a/eth2/state_processing/src/per_block_processing/errors.rs +++ b/eth2/state_processing/src/per_block_processing/errors.rs @@ -71,19 +71,20 @@ pub enum BlockInvalid { state: Hash256, block: Hash256, }, + ProposerSlashed(usize), BadSignature, BadRandaoSignature, MaxAttestationsExceeded, MaxAttesterSlashingsExceed, MaxProposerSlashingsExceeded, - MaxDepositsExceeded, + DepositCountInvalid, MaxExitsExceeded, MaxTransfersExceed, AttestationInvalid(usize, AttestationInvalid), - /// A `SlashableAttestation` inside an `AttesterSlashing` was invalid. + /// A `IndexedAttestation` inside an `AttesterSlashing` was invalid. /// /// To determine the offending `AttesterSlashing` index, divide the error message `usize` by two. - SlashableAttestationInvalid(usize, SlashableAttestationInvalid), + IndexedAttestationInvalid(usize, IndexedAttestationInvalid), AttesterSlashingInvalid(usize, AttesterSlashingInvalid), ProposerSlashingInvalid(usize, ProposerSlashingInvalid), DepositInvalid(usize, DepositInvalid), @@ -125,6 +126,8 @@ pub enum AttestationInvalid { }, /// Attestation slot is too far in the past to be included in a block. IncludedTooLate { state: Slot, attestation: Slot }, + /// Attestation target epoch does not match the current or previous epoch. + BadTargetEpoch, /// Attestation justified epoch does not match the states current or previous justified epoch. /// /// `is_current` is `true` if the attestation was compared to the @@ -169,11 +172,20 @@ pub enum AttestationInvalid { BadSignature, /// The shard block root was not set to zero. This is a phase 0 requirement. ShardBlockRootNotZero, + /// The indexed attestation created from this attestation was found to be invalid. + BadIndexedAttestation(IndexedAttestationInvalid), } impl_from_beacon_state_error!(AttestationValidationError); impl_into_with_index_with_beacon_error!(AttestationValidationError, AttestationInvalid); +impl From for AttestationValidationError { + fn from(err: IndexedAttestationValidationError) -> Self { + let IndexedAttestationValidationError::Invalid(e) = err; + AttestationValidationError::Invalid(AttestationInvalid::BadIndexedAttestation(e)) + } +} + /* * `AttesterSlashing` Validation */ @@ -194,10 +206,10 @@ pub enum AttesterSlashingInvalid { AttestationDataIdentical, /// The attestations were not in conflict. NotSlashable, - /// The first `SlashableAttestation` was invalid. - SlashableAttestation1Invalid(SlashableAttestationInvalid), - /// The second `SlashableAttestation` was invalid. - SlashableAttestation2Invalid(SlashableAttestationInvalid), + /// The first `IndexedAttestation` was invalid. + IndexedAttestation1Invalid(IndexedAttestationInvalid), + /// The second `IndexedAttestation` was invalid. + IndexedAttestation2Invalid(IndexedAttestationInvalid), /// The validator index is unknown. One cannot slash one who does not exist. UnknownValidator(u64), /// The specified validator has already been withdrawn. @@ -210,52 +222,50 @@ impl_from_beacon_state_error!(AttesterSlashingValidationError); impl_into_with_index_with_beacon_error!(AttesterSlashingValidationError, AttesterSlashingInvalid); /* - * `SlashableAttestation` Validation + * `IndexedAttestation` Validation */ /// The object is invalid or validation failed. #[derive(Debug, PartialEq)] -pub enum SlashableAttestationValidationError { +pub enum IndexedAttestationValidationError { /// Validation completed successfully and the object is invalid. - Invalid(SlashableAttestationInvalid), + Invalid(IndexedAttestationInvalid), } /// Describes why an object is invalid. #[derive(Debug, PartialEq)] -pub enum SlashableAttestationInvalid { +pub enum IndexedAttestationInvalid { + /// The custody bit 0 validators intersect with the bit 1 validators. + CustodyBitValidatorsIntersect, /// The custody bitfield has some bits set `true`. This is not allowed in phase 0. CustodyBitfieldHasSetBits, /// No validator indices were specified. NoValidatorIndices, + /// The number of indices exceeds the global maximum. + /// + /// (max_indices, indices_given) + MaxIndicesExceed(u64, usize), /// The validator indices were not in increasing order. /// /// The error occured between the given `index` and `index + 1` BadValidatorIndicesOrdering(usize), - /// The custody bitfield length is not the smallest possible size to represent the validators. - /// - /// (validators_len, bitfield_len) - BadCustodyBitfieldLength(usize, usize), - /// The number of slashable indices exceed the global maximum. - /// - /// (max_indices, indices_given) - MaxIndicesExceed(usize, usize), /// The validator index is unknown. One cannot slash one who does not exist. UnknownValidator(u64), - /// The slashable attestation aggregate signature was not valid. + /// The indexed attestation aggregate signature was not valid. BadSignature, } -impl Into for SlashableAttestationValidationError { - fn into(self) -> SlashableAttestationInvalid { +impl Into for IndexedAttestationValidationError { + fn into(self) -> IndexedAttestationInvalid { match self { - SlashableAttestationValidationError::Invalid(e) => e, + IndexedAttestationValidationError::Invalid(e) => e, } } } impl_into_with_index_without_beacon_error!( - SlashableAttestationValidationError, - SlashableAttestationInvalid + IndexedAttestationValidationError, + IndexedAttestationInvalid ); /* @@ -280,10 +290,8 @@ pub enum ProposerSlashingInvalid { ProposalEpochMismatch(Slot, Slot), /// The proposals are identical and therefore not slashable. ProposalsIdentical, - /// The specified proposer has already been slashed. - ProposerAlreadySlashed, - /// The specified proposer has already been withdrawn. - ProposerAlreadyWithdrawn(u64), + /// The specified proposer cannot be slashed because they are already slashed, or not active. + ProposerNotSlashable(u64), /// The first proposal signature was invalid. BadProposal1Signature, /// The second proposal signature was invalid. @@ -313,11 +321,8 @@ pub enum DepositValidationError { pub enum DepositInvalid { /// The deposit index does not match the state index. BadIndex { state: u64, deposit: u64 }, - /// The proof-of-possession does not match the given pubkey. - BadProofOfPossession, - /// The withdrawal credentials for the depositing validator did not match the withdrawal - /// credentials of an existing validator with the same public key. - BadWithdrawalCredentials, + /// The signature (proof-of-possession) does not match the given pubkey. + BadSignature, /// The specified `branch` and `index` did not form a valid proof that the deposit is included /// in the eth1 deposit root. BadMerkleProof, @@ -340,6 +345,8 @@ pub enum ExitValidationError { /// Describes why an object is invalid. #[derive(Debug, PartialEq)] pub enum ExitInvalid { + /// The specified validator is not active. + NotActive(u64), /// The specified validator is not in the state's validator registry. ValidatorUnknown(u64), /// The specified validator has a non-maximum exit epoch. @@ -388,7 +395,12 @@ pub enum TransferInvalid { /// min_deposit_amount` /// /// (resulting_amount, min_deposit_amount) - InvalidResultingFromBalance(u64, u64), + SenderDust(u64, u64), + /// This transfer would result in the `transfer.to` account to have `0 < balance < + /// min_deposit_amount` + /// + /// (resulting_amount, min_deposit_amount) + RecipientDust(u64, u64), /// The state slot does not match `transfer.slot`. /// /// (state_slot, transfer_slot) diff --git a/eth2/state_processing/src/per_block_processing/tests.rs b/eth2/state_processing/src/per_block_processing/tests.rs index 19418aba1..28ed9c4f0 100644 --- a/eth2/state_processing/src/per_block_processing/tests.rs +++ b/eth2/state_processing/src/per_block_processing/tests.rs @@ -67,7 +67,7 @@ fn invalid_block_signature() { let keypair = Keypair::random(); let message = block.signed_root(); let epoch = block.slot.epoch(spec.slots_per_epoch); - let domain = spec.get_domain(epoch, Domain::BeaconBlock, &state.fork); + let domain = spec.get_domain(epoch, Domain::BeaconProposer, &state.fork); block.signature = Signature::new(&message, domain, &keypair.sk); // process block with invalid block signature diff --git a/eth2/state_processing/src/per_block_processing/validate_attestation.rs b/eth2/state_processing/src/per_block_processing/validate_attestation.rs index cb26389df..1058c0d21 100644 --- a/eth2/state_processing/src/per_block_processing/validate_attestation.rs +++ b/eth2/state_processing/src/per_block_processing/validate_attestation.rs @@ -1,5 +1,8 @@ use super::errors::{AttestationInvalid as Invalid, AttestationValidationError as Error}; -use crate::common::verify_bitfield_length; +use crate::common::convert_to_indexed; +use crate::per_block_processing::{ + verify_indexed_attestation, verify_indexed_attestation_without_signature, +}; use tree_hash::TreeHash; use types::*; @@ -8,7 +11,7 @@ use types::*; /// /// Returns `Ok(())` if the `Attestation` is valid, otherwise indicates the reason for invalidity. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn validate_attestation( state: &BeaconState, attestation: &Attestation, @@ -31,7 +34,7 @@ pub fn validate_attestation_time_independent_only( /// /// Returns `Ok(())` if the `Attestation` is valid, otherwise indicates the reason for invalidity. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn validate_attestation_without_signature( state: &BeaconState, attestation: &Attestation, @@ -44,7 +47,7 @@ pub fn validate_attestation_without_signature( /// given state, optionally validating the aggregate signature. /// /// -/// Spec v0.5.1 +/// Spec v0.6.1 fn validate_attestation_parametric( state: &BeaconState, attestation: &Attestation, @@ -52,107 +55,29 @@ fn validate_attestation_parametric( verify_signature: bool, time_independent_only: bool, ) -> Result<(), Error> { - // Can't submit pre-historic attestations. - verify!( - attestation.data.slot >= spec.genesis_slot, - Invalid::PreGenesis { - genesis: spec.genesis_slot, - attestation: attestation.data.slot - } - ); + let attestation_slot = state.get_attestation_slot(&attestation.data)?; - // Can't submit attestations too far in history. - verify!( - state.slot <= attestation.data.slot + spec.slots_per_epoch, - Invalid::IncludedTooLate { - state: spec.genesis_slot, - attestation: attestation.data.slot - } - ); - - // Can't submit attestation too quickly. + // Check attestation slot. verify!( time_independent_only - || attestation.data.slot + spec.min_attestation_inclusion_delay <= state.slot, + || attestation_slot + spec.min_attestation_inclusion_delay <= state.slot, Invalid::IncludedTooEarly { state: state.slot, delay: spec.min_attestation_inclusion_delay, - attestation: attestation.data.slot + attestation: attestation_slot + } + ); + verify!( + state.slot <= attestation_slot + spec.slots_per_epoch, + Invalid::IncludedTooLate { + state: state.slot, + attestation: attestation_slot } ); - // Verify the justified epoch and root is correct. + // Verify the Casper FFG vote. if !time_independent_only { - verify_justified_epoch_and_root(attestation, state, spec)?; - } - - // Check that the crosslink data is valid. - // - // Verify that either: - // - // (i)`state.latest_crosslinks[attestation.data.shard] == attestation.data.latest_crosslink`, - // - // (ii) `state.latest_crosslinks[attestation.data.shard] == - // Crosslink(crosslink_data_root=attestation.data.crosslink_data_root, - // epoch=slot_to_epoch(attestation.data.slot))`. - let potential_crosslink = Crosslink { - crosslink_data_root: attestation.data.crosslink_data_root, - epoch: attestation.data.slot.epoch(spec.slots_per_epoch), - }; - verify!( - (attestation.data.previous_crosslink - == state.latest_crosslinks[attestation.data.shard as usize]) - | (state.latest_crosslinks[attestation.data.shard as usize] == potential_crosslink), - Invalid::BadPreviousCrosslink - ); - - // Attestation must be non-empty! - verify!( - attestation.aggregation_bitfield.num_set_bits() != 0, - Invalid::AggregationBitfieldIsEmpty - ); - // Custody bitfield must be empty (be be removed in phase 1) - verify!( - attestation.custody_bitfield.num_set_bits() == 0, - Invalid::CustodyBitfieldHasSetBits - ); - - // Get the committee for the specific shard that this attestation is for. - let crosslink_committee = state - .get_crosslink_committees_at_slot(attestation.data.slot, spec)? - .iter() - .find(|c| c.shard == attestation.data.shard) - .ok_or_else(|| { - Error::Invalid(Invalid::NoCommitteeForShard { - shard: attestation.data.shard, - slot: attestation.data.slot, - }) - })?; - let committee = &crosslink_committee.committee; - - // Custody bitfield length is correct. - // - // This is not directly in the spec, but it is inferred. - verify!( - verify_bitfield_length(&attestation.custody_bitfield, committee.len()), - Invalid::BadCustodyBitfieldLength { - committee_len: committee.len(), - bitfield_len: attestation.custody_bitfield.len() - } - ); - // Aggregation bitfield length is correct. - // - // This is not directly in the spec, but it is inferred. - verify!( - verify_bitfield_length(&attestation.aggregation_bitfield, committee.len()), - Invalid::BadAggregationBitfieldLength { - committee_len: committee.len(), - bitfield_len: attestation.custody_bitfield.len() - } - ); - - if verify_signature { - verify_attestation_signature(state, committee, attestation, spec)?; + verify_casper_ffg_vote(attestation, state)?; } // Crosslink data root is zero (to be removed in phase 1). @@ -161,145 +86,71 @@ fn validate_attestation_parametric( Invalid::ShardBlockRootNotZero ); + // Check signature and bitfields + let indexed_attestation = convert_to_indexed(state, attestation)?; + if verify_signature { + verify_indexed_attestation(state, &indexed_attestation, spec)?; + } else { + verify_indexed_attestation_without_signature(state, &indexed_attestation, spec)?; + } + Ok(()) } -/// Verify that the `source_epoch` and `source_root` of an `Attestation` correctly -/// match the current (or previous) justified epoch and root from the state. +/// Check target epoch, source epoch, source root, and source crosslink. /// -/// Spec v0.5.1 -fn verify_justified_epoch_and_root( +/// Spec v0.6.1 +fn verify_casper_ffg_vote( attestation: &Attestation, state: &BeaconState, - spec: &ChainSpec, ) -> Result<(), Error> { - let state_epoch = state.slot.epoch(spec.slots_per_epoch); - let attestation_epoch = attestation.data.slot.epoch(spec.slots_per_epoch); - - if attestation_epoch >= state_epoch { + let data = &attestation.data; + if data.target_epoch == state.current_epoch() { verify!( - attestation.data.source_epoch == state.current_justified_epoch, + data.source_epoch == state.current_justified_epoch, Invalid::WrongJustifiedEpoch { state: state.current_justified_epoch, - attestation: attestation.data.source_epoch, + attestation: data.source_epoch, is_current: true, } ); verify!( - attestation.data.source_root == state.current_justified_root, + data.source_root == state.current_justified_root, Invalid::WrongJustifiedRoot { state: state.current_justified_root, - attestation: attestation.data.source_root, + attestation: data.source_root, is_current: true, } ); - } else { verify!( - attestation.data.source_epoch == state.previous_justified_epoch, + data.previous_crosslink_root + == Hash256::from_slice(&state.get_current_crosslink(data.shard)?.tree_hash_root()), + Invalid::BadPreviousCrosslink + ); + } else if data.target_epoch == state.previous_epoch() { + verify!( + data.source_epoch == state.previous_justified_epoch, Invalid::WrongJustifiedEpoch { state: state.previous_justified_epoch, - attestation: attestation.data.source_epoch, + attestation: data.source_epoch, is_current: false, } ); verify!( - attestation.data.source_root == state.previous_justified_root, + data.source_root == state.previous_justified_root, Invalid::WrongJustifiedRoot { state: state.previous_justified_root, - attestation: attestation.data.source_root, - is_current: true, + attestation: data.source_root, + is_current: false, } ); + verify!( + data.previous_crosslink_root + == Hash256::from_slice(&state.get_previous_crosslink(data.shard)?.tree_hash_root()), + Invalid::BadPreviousCrosslink + ); + } else { + invalid!(Invalid::BadTargetEpoch) } Ok(()) } - -/// Verifies an aggregate signature for some given `AttestationData`, returning `true` if the -/// `aggregate_signature` is valid. -/// -/// Returns `false` if: -/// - `aggregate_signature` was not signed correctly. -/// - `custody_bitfield` does not have a bit for each index of `committee`. -/// - A `validator_index` in `committee` is not in `state.validator_registry`. -/// -/// Spec v0.5.1 -fn verify_attestation_signature( - state: &BeaconState, - committee: &[usize], - a: &Attestation, - spec: &ChainSpec, -) -> Result<(), Error> { - let mut aggregate_pubs = vec![AggregatePublicKey::new(); 2]; - let mut message_exists = vec![false; 2]; - let attestation_epoch = a.data.slot.epoch(spec.slots_per_epoch); - - for (i, v) in committee.iter().enumerate() { - let validator_signed = a.aggregation_bitfield.get(i).map_err(|_| { - Error::Invalid(Invalid::BadAggregationBitfieldLength { - committee_len: committee.len(), - bitfield_len: a.aggregation_bitfield.len(), - }) - })?; - - if validator_signed { - let custody_bit: bool = match a.custody_bitfield.get(i) { - Ok(bit) => bit, - // Invalidate signature if custody_bitfield.len() < committee - Err(_) => { - return Err(Error::Invalid(Invalid::BadCustodyBitfieldLength { - committee_len: committee.len(), - bitfield_len: a.aggregation_bitfield.len(), - })); - } - }; - - message_exists[custody_bit as usize] = true; - - match state.validator_registry.get(*v as usize) { - Some(validator) => { - aggregate_pubs[custody_bit as usize].add(&validator.pubkey); - } - // Return error if validator index is unknown. - None => return Err(Error::BeaconStateError(BeaconStateError::UnknownValidator)), - }; - } - } - - // Message when custody bitfield is `false` - let message_0 = AttestationDataAndCustodyBit { - data: a.data.clone(), - custody_bit: false, - } - .tree_hash_root(); - - // Message when custody bitfield is `true` - let message_1 = AttestationDataAndCustodyBit { - data: a.data.clone(), - custody_bit: true, - } - .tree_hash_root(); - - let mut messages = vec![]; - let mut keys = vec![]; - - // If any validator signed a message with a `false` custody bit. - if message_exists[0] { - messages.push(&message_0[..]); - keys.push(&aggregate_pubs[0]); - } - // If any validator signed a message with a `true` custody bit. - if message_exists[1] { - messages.push(&message_1[..]); - keys.push(&aggregate_pubs[1]); - } - - let domain = spec.get_domain(attestation_epoch, Domain::Attestation, &state.fork); - - verify!( - a.aggregate_signature - .verify_multiple(&messages[..], domain, &keys[..]), - Invalid::BadSignature - ); - - Ok(()) -} diff --git a/eth2/state_processing/src/per_block_processing/verify_attester_slashing.rs b/eth2/state_processing/src/per_block_processing/verify_attester_slashing.rs index 5ac62221a..7e1fa5e66 100644 --- a/eth2/state_processing/src/per_block_processing/verify_attester_slashing.rs +++ b/eth2/state_processing/src/per_block_processing/verify_attester_slashing.rs @@ -1,5 +1,6 @@ use super::errors::{AttesterSlashingInvalid as Invalid, AttesterSlashingValidationError as Error}; -use super::verify_slashable_attestation::verify_slashable_attestation; +use super::verify_indexed_attestation::verify_indexed_attestation; +use std::collections::BTreeSet; use types::*; /// Indicates if an `AttesterSlashing` is valid to be included in a block in the current epoch of the given @@ -7,90 +8,87 @@ use types::*; /// /// Returns `Ok(())` if the `AttesterSlashing` is valid, otherwise indicates the reason for invalidity. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn verify_attester_slashing( state: &BeaconState, attester_slashing: &AttesterSlashing, - should_verify_slashable_attestations: bool, + should_verify_indexed_attestations: bool, spec: &ChainSpec, ) -> Result<(), Error> { - let slashable_attestation_1 = &attester_slashing.slashable_attestation_1; - let slashable_attestation_2 = &attester_slashing.slashable_attestation_2; + let attestation_1 = &attester_slashing.attestation_1; + let attestation_2 = &attester_slashing.attestation_2; + // Spec: is_slashable_attestation_data verify!( - slashable_attestation_1.data != slashable_attestation_2.data, - Invalid::AttestationDataIdentical - ); - verify!( - slashable_attestation_1.is_double_vote(slashable_attestation_2, spec) - | slashable_attestation_1.is_surround_vote(slashable_attestation_2, spec), + attestation_1.is_double_vote(attestation_2) + || attestation_1.is_surround_vote(attestation_2), Invalid::NotSlashable ); - if should_verify_slashable_attestations { - verify_slashable_attestation(state, &slashable_attestation_1, spec) - .map_err(|e| Error::Invalid(Invalid::SlashableAttestation1Invalid(e.into())))?; - verify_slashable_attestation(state, &slashable_attestation_2, spec) - .map_err(|e| Error::Invalid(Invalid::SlashableAttestation2Invalid(e.into())))?; + if should_verify_indexed_attestations { + verify_indexed_attestation(state, &attestation_1, spec) + .map_err(|e| Error::Invalid(Invalid::IndexedAttestation1Invalid(e.into())))?; + verify_indexed_attestation(state, &attestation_2, spec) + .map_err(|e| Error::Invalid(Invalid::IndexedAttestation2Invalid(e.into())))?; } Ok(()) } -/// For a given attester slashing, return the indices able to be slashed. +/// For a given attester slashing, return the indices able to be slashed in ascending order. /// /// Returns Ok(indices) if `indices.len() > 0`. /// -/// Spec v0.5.1 -pub fn gather_attester_slashing_indices( +/// Spec v0.6.1 +pub fn get_slashable_indices( state: &BeaconState, attester_slashing: &AttesterSlashing, - spec: &ChainSpec, ) -> Result, Error> { - gather_attester_slashing_indices_modular( - state, - attester_slashing, - |_, validator| validator.slashed, - spec, - ) + get_slashable_indices_modular(state, attester_slashing, |_, validator| { + validator.is_slashable_at(state.current_epoch()) + }) } /// Same as `gather_attester_slashing_indices` but allows the caller to specify the criteria -/// for determining whether a given validator should be considered slashed. -pub fn gather_attester_slashing_indices_modular( +/// for determining whether a given validator should be considered slashable. +pub fn get_slashable_indices_modular( state: &BeaconState, attester_slashing: &AttesterSlashing, - is_slashed: F, - spec: &ChainSpec, + is_slashable: F, ) -> Result, Error> where F: Fn(u64, &Validator) -> bool, { - let slashable_attestation_1 = &attester_slashing.slashable_attestation_1; - let slashable_attestation_2 = &attester_slashing.slashable_attestation_2; + let attestation_1 = &attester_slashing.attestation_1; + let attestation_2 = &attester_slashing.attestation_2; - let mut slashable_indices = Vec::with_capacity(spec.max_indices_per_slashable_vote); - for i in &slashable_attestation_1.validator_indices { + let attesting_indices_1 = attestation_1 + .custody_bit_0_indices + .iter() + .chain(&attestation_1.custody_bit_1_indices) + .cloned() + .collect::>(); + let attesting_indices_2 = attestation_2 + .custody_bit_0_indices + .iter() + .chain(&attestation_2.custody_bit_1_indices) + .cloned() + .collect::>(); + + let mut slashable_indices = vec![]; + + for index in &attesting_indices_1 & &attesting_indices_2 { let validator = state .validator_registry - .get(*i as usize) - .ok_or_else(|| Error::Invalid(Invalid::UnknownValidator(*i)))?; + .get(index as usize) + .ok_or_else(|| Error::Invalid(Invalid::UnknownValidator(index)))?; - if slashable_attestation_2.validator_indices.contains(&i) & !is_slashed(*i, validator) { - // TODO: verify that we should reject any slashable attestation which includes a - // withdrawn validator. PH has asked the question on gitter, awaiting response. - verify!( - validator.withdrawable_epoch > state.slot.epoch(spec.slots_per_epoch), - Invalid::ValidatorAlreadyWithdrawn(*i) - ); - - slashable_indices.push(*i); + if is_slashable(index, validator) { + slashable_indices.push(index); } } verify!(!slashable_indices.is_empty(), Invalid::NoSlashableIndices); - slashable_indices.shrink_to_fit(); - Ok(slashable_indices) } diff --git a/eth2/state_processing/src/per_block_processing/verify_deposit.rs b/eth2/state_processing/src/per_block_processing/verify_deposit.rs index e2868a1b6..da2353bb9 100644 --- a/eth2/state_processing/src/per_block_processing/verify_deposit.rs +++ b/eth2/state_processing/src/per_block_processing/verify_deposit.rs @@ -1,52 +1,31 @@ use super::errors::{DepositInvalid as Invalid, DepositValidationError as Error}; -use hashing::hash; use merkle_proof::verify_merkle_proof; -use ssz::ssz_encode; -use ssz_derive::Encode; +use tree_hash::{SignedRoot, TreeHash}; use types::*; -/// Indicates if a `Deposit` is valid to be included in a block in the current epoch of the given -/// state. +/// Verify `Deposit.pubkey` signed `Deposit.signature`. /// -/// Returns `Ok(())` if the `Deposit` is valid, otherwise indicates the reason for invalidity. -/// -/// This function _does not_ check `state.deposit_index` so this function may be run in parallel. -/// See the `verify_deposit_index` function for this. -/// -/// Note: this function is incomplete. -/// -/// Spec v0.5.1 -pub fn verify_deposit( +/// Spec v0.6.1 +pub fn verify_deposit_signature( state: &BeaconState, deposit: &Deposit, - verify_merkle_branch: bool, spec: &ChainSpec, ) -> Result<(), Error> { verify!( - deposit - .deposit_data - .deposit_input - .validate_proof_of_possession( - state.slot.epoch(spec.slots_per_epoch), - &state.fork, - spec - ), - Invalid::BadProofOfPossession + deposit.data.signature.verify( + &deposit.data.signed_root(), + spec.get_domain(state.current_epoch(), Domain::Deposit, &state.fork), + &deposit.data.pubkey, + ), + Invalid::BadSignature ); - if verify_merkle_branch { - verify!( - verify_deposit_merkle_proof(state, deposit, spec), - Invalid::BadMerkleProof - ); - } - Ok(()) } /// Verify that the `Deposit` index is correct. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn verify_deposit_index( state: &BeaconState, deposit: &Deposit, @@ -72,61 +51,30 @@ pub fn get_existing_validator_index( state: &BeaconState, deposit: &Deposit, ) -> Result, Error> { - let deposit_input = &deposit.deposit_data.deposit_input; - - let validator_index = state.get_validator_index(&deposit_input.pubkey)?; - - match validator_index { - None => Ok(None), - Some(index) => { - verify!( - deposit_input.withdrawal_credentials - == state.validator_registry[index as usize].withdrawal_credentials, - Invalid::BadWithdrawalCredentials - ); - Ok(Some(index as u64)) - } - } + let validator_index = state.get_validator_index(&deposit.data.pubkey)?; + Ok(validator_index.map(|idx| idx as u64)) } /// Verify that a deposit is included in the state's eth1 deposit root. /// -/// Spec v0.5.1 -fn verify_deposit_merkle_proof( +/// Spec v0.6.1 +pub fn verify_deposit_merkle_proof( state: &BeaconState, deposit: &Deposit, spec: &ChainSpec, -) -> bool { - let leaf = hash(&get_serialized_deposit_data(deposit)); - verify_merkle_proof( - Hash256::from_slice(&leaf), - &deposit.proof[..], - spec.deposit_contract_tree_depth as usize, - deposit.index as usize, - state.latest_eth1_data.deposit_root, - ) -} +) -> Result<(), Error> { + let leaf = deposit.data.tree_hash_root(); -/// Helper struct for easily getting the serialized data generated by the deposit contract. -/// -/// Spec v0.5.1 -#[derive(Encode)] -struct SerializedDepositData { - amount: u64, - timestamp: u64, - input: DepositInput, -} + verify!( + verify_merkle_proof( + Hash256::from_slice(&leaf), + &deposit.proof[..], + spec.deposit_contract_tree_depth as usize, + deposit.index as usize, + state.latest_eth1_data.deposit_root, + ), + Invalid::BadMerkleProof + ); -/// Return the serialized data generated by the deposit contract that is used to generate the -/// merkle proof. -/// -/// Spec v0.5.1 -fn get_serialized_deposit_data(deposit: &Deposit) -> Vec { - let serialized_deposit_data = SerializedDepositData { - amount: deposit.deposit_data.amount, - timestamp: deposit.deposit_data.timestamp, - input: deposit.deposit_data.deposit_input.clone(), - }; - - ssz_encode(&serialized_deposit_data) + Ok(()) } diff --git a/eth2/state_processing/src/per_block_processing/verify_exit.rs b/eth2/state_processing/src/per_block_processing/verify_exit.rs index 333638a8d..2a3a29b3f 100644 --- a/eth2/state_processing/src/per_block_processing/verify_exit.rs +++ b/eth2/state_processing/src/per_block_processing/verify_exit.rs @@ -7,7 +7,7 @@ use types::*; /// /// Returns `Ok(())` if the `Exit` is valid, otherwise indicates the reason for invalidity. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn verify_exit( state: &BeaconState, exit: &VoluntaryExit, @@ -17,6 +17,8 @@ pub fn verify_exit( } /// Like `verify_exit` but doesn't run checks which may become true in future states. +/// +/// Spec v0.6.1 pub fn verify_exit_time_independent_only( state: &BeaconState, exit: &VoluntaryExit, @@ -26,6 +28,8 @@ pub fn verify_exit_time_independent_only( } /// Parametric version of `verify_exit` that skips some checks if `time_independent_only` is true. +/// +/// Spec v0.6.1 fn verify_exit_parametric( state: &BeaconState, exit: &VoluntaryExit, @@ -37,29 +41,29 @@ fn verify_exit_parametric( .get(exit.validator_index as usize) .ok_or_else(|| Error::Invalid(Invalid::ValidatorUnknown(exit.validator_index)))?; + // Verify the validator is active. + verify!( + validator.is_active_at(state.current_epoch()), + Invalid::NotActive(exit.validator_index) + ); + // Verify that the validator has not yet exited. verify!( validator.exit_epoch == spec.far_future_epoch, Invalid::AlreadyExited(exit.validator_index) ); - // Verify that the validator has not yet initiated. - verify!( - !validator.initiated_exit, - Invalid::AlreadyInitiatedExited(exit.validator_index) - ); - // Exits must specify an epoch when they become valid; they are not valid before then. verify!( - time_independent_only || state.current_epoch(spec) >= exit.epoch, + time_independent_only || state.current_epoch() >= exit.epoch, Invalid::FutureEpoch { - state: state.current_epoch(spec), + state: state.current_epoch(), exit: exit.epoch } ); - // Must have been in the validator set long enough. - let lifespan = state.slot.epoch(spec.slots_per_epoch) - validator.activation_epoch; + // Verify the validator has been active long enough. + let lifespan = state.current_epoch() - validator.activation_epoch; verify!( lifespan >= spec.persistent_committee_period, Invalid::TooYoungToLeave { @@ -68,9 +72,9 @@ fn verify_exit_parametric( } ); + // Verify signature. let message = exit.signed_root(); - let domain = spec.get_domain(exit.epoch, Domain::Exit, &state.fork); - + let domain = spec.get_domain(exit.epoch, Domain::VoluntaryExit, &state.fork); verify!( exit.signature .verify(&message[..], domain, &validator.pubkey), diff --git a/eth2/state_processing/src/per_block_processing/verify_indexed_attestation.rs b/eth2/state_processing/src/per_block_processing/verify_indexed_attestation.rs new file mode 100644 index 000000000..6581e516d --- /dev/null +++ b/eth2/state_processing/src/per_block_processing/verify_indexed_attestation.rs @@ -0,0 +1,156 @@ +use super::errors::{ + IndexedAttestationInvalid as Invalid, IndexedAttestationValidationError as Error, +}; +use std::collections::HashSet; +use std::iter::FromIterator; +use tree_hash::TreeHash; +use types::*; + +/// Verify an `IndexedAttestation`. +/// +/// Spec v0.6.1 +pub fn verify_indexed_attestation( + state: &BeaconState, + indexed_attestation: &IndexedAttestation, + spec: &ChainSpec, +) -> Result<(), Error> { + verify_indexed_attestation_parametric(state, indexed_attestation, spec, true) +} + +/// Verify but don't check the signature. +/// +/// Spec v0.6.1 +pub fn verify_indexed_attestation_without_signature( + state: &BeaconState, + indexed_attestation: &IndexedAttestation, + spec: &ChainSpec, +) -> Result<(), Error> { + verify_indexed_attestation_parametric(state, indexed_attestation, spec, false) +} + +/// Optionally check the signature. +/// +/// Spec v0.6.1 +fn verify_indexed_attestation_parametric( + state: &BeaconState, + indexed_attestation: &IndexedAttestation, + spec: &ChainSpec, + verify_signature: bool, +) -> Result<(), Error> { + let custody_bit_0_indices = &indexed_attestation.custody_bit_0_indices; + let custody_bit_1_indices = &indexed_attestation.custody_bit_1_indices; + + // Ensure no duplicate indices across custody bits + let custody_bit_intersection: HashSet<&u64> = + &HashSet::from_iter(custody_bit_0_indices) & &HashSet::from_iter(custody_bit_1_indices); + verify!( + custody_bit_intersection.is_empty(), + Invalid::CustodyBitValidatorsIntersect + ); + + // Check that nobody signed with custody bit 1 (to be removed in phase 1) + if custody_bit_1_indices.len() > 0 { + invalid!(Invalid::CustodyBitfieldHasSetBits); + } + + let total_indices = custody_bit_0_indices.len() + custody_bit_1_indices.len(); + verify!(1 <= total_indices, Invalid::NoValidatorIndices); + verify!( + total_indices as u64 <= spec.max_indices_per_attestation, + Invalid::MaxIndicesExceed(spec.max_indices_per_attestation, total_indices) + ); + + // Check that both vectors of indices are sorted + let check_sorted = |list: &Vec| { + list.windows(2).enumerate().try_for_each(|(i, pair)| { + if pair[0] >= pair[1] { + invalid!(Invalid::BadValidatorIndicesOrdering(i)); + } else { + Ok(()) + } + })?; + Ok(()) + }; + check_sorted(custody_bit_0_indices)?; + check_sorted(custody_bit_1_indices)?; + + if verify_signature { + verify_indexed_attestation_signature(state, indexed_attestation, spec)?; + } + + Ok(()) +} + +/// Create an aggregate public key for a list of validators, failing if any key can't be found. +fn create_aggregate_pubkey<'a, T, I>( + state: &BeaconState, + validator_indices: I, +) -> Result +where + I: IntoIterator, + T: EthSpec, +{ + validator_indices.into_iter().try_fold( + AggregatePublicKey::new(), + |mut aggregate_pubkey, &validator_idx| { + state + .validator_registry + .get(validator_idx as usize) + .ok_or(Error::Invalid(Invalid::UnknownValidator(validator_idx))) + .map(|validator| { + aggregate_pubkey.add(&validator.pubkey); + aggregate_pubkey + }) + }, + ) +} + +/// Verify the signature of an IndexedAttestation. +/// +/// Spec v0.6.1 +fn verify_indexed_attestation_signature( + state: &BeaconState, + indexed_attestation: &IndexedAttestation, + spec: &ChainSpec, +) -> Result<(), Error> { + let bit_0_pubkey = create_aggregate_pubkey(state, &indexed_attestation.custody_bit_0_indices)?; + let bit_1_pubkey = create_aggregate_pubkey(state, &indexed_attestation.custody_bit_1_indices)?; + + let message_0 = AttestationDataAndCustodyBit { + data: indexed_attestation.data.clone(), + custody_bit: false, + } + .tree_hash_root(); + let message_1 = AttestationDataAndCustodyBit { + data: indexed_attestation.data.clone(), + custody_bit: true, + } + .tree_hash_root(); + + let mut messages = vec![]; + let mut keys = vec![]; + + if !indexed_attestation.custody_bit_0_indices.is_empty() { + messages.push(&message_0[..]); + keys.push(&bit_0_pubkey); + } + if !indexed_attestation.custody_bit_1_indices.is_empty() { + messages.push(&message_1[..]); + keys.push(&bit_1_pubkey); + } + + let domain = spec.get_domain( + indexed_attestation.data.target_epoch, + Domain::Attestation, + &state.fork, + ); + + verify!( + indexed_attestation + .signature + .verify_multiple(&messages[..], domain, &keys[..]), + Invalid::BadSignature + ); + + Ok(()) +} diff --git a/eth2/state_processing/src/per_block_processing/verify_proposer_slashing.rs b/eth2/state_processing/src/per_block_processing/verify_proposer_slashing.rs index 0c66a9b15..98a9a248c 100644 --- a/eth2/state_processing/src/per_block_processing/verify_proposer_slashing.rs +++ b/eth2/state_processing/src/per_block_processing/verify_proposer_slashing.rs @@ -7,7 +7,7 @@ use types::*; /// /// Returns `Ok(())` if the `ProposerSlashing` is valid, otherwise indicates the reason for invalidity. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn verify_proposer_slashing( proposer_slashing: &ProposerSlashing, state: &BeaconState, @@ -34,11 +34,9 @@ pub fn verify_proposer_slashing( Invalid::ProposalsIdentical ); - verify!(!proposer.slashed, Invalid::ProposerAlreadySlashed); - verify!( - proposer.withdrawable_epoch > state.slot.epoch(spec.slots_per_epoch), - Invalid::ProposerAlreadyWithdrawn(proposer_slashing.proposer_index) + proposer.is_slashable_at(state.current_epoch()), + Invalid::ProposerNotSlashable(proposer_slashing.proposer_index) ); verify!( @@ -67,7 +65,7 @@ pub fn verify_proposer_slashing( /// /// Returns `true` if the signature is valid. /// -/// Spec v0.5.1 +/// Spec v0.6.1 fn verify_header_signature( header: &BeaconBlockHeader, pubkey: &PublicKey, @@ -77,7 +75,7 @@ fn verify_header_signature( let message = header.signed_root(); let domain = spec.get_domain( header.slot.epoch(spec.slots_per_epoch), - Domain::BeaconBlock, + Domain::BeaconProposer, fork, ); header.signature.verify(&message[..], domain, pubkey) diff --git a/eth2/state_processing/src/per_block_processing/verify_slashable_attestation.rs b/eth2/state_processing/src/per_block_processing/verify_slashable_attestation.rs deleted file mode 100644 index 4d440332a..000000000 --- a/eth2/state_processing/src/per_block_processing/verify_slashable_attestation.rs +++ /dev/null @@ -1,112 +0,0 @@ -use super::errors::{ - SlashableAttestationInvalid as Invalid, SlashableAttestationValidationError as Error, -}; -use crate::common::verify_bitfield_length; -use tree_hash::TreeHash; -use types::*; - -/// Indicates if a `SlashableAttestation` is valid to be included in a block in the current epoch of the given -/// state. -/// -/// Returns `Ok(())` if the `SlashableAttestation` is valid, otherwise indicates the reason for invalidity. -/// -/// Spec v0.5.1 -pub fn verify_slashable_attestation( - state: &BeaconState, - slashable_attestation: &SlashableAttestation, - spec: &ChainSpec, -) -> Result<(), Error> { - if slashable_attestation.custody_bitfield.num_set_bits() > 0 { - invalid!(Invalid::CustodyBitfieldHasSetBits); - } - - if slashable_attestation.validator_indices.is_empty() { - invalid!(Invalid::NoValidatorIndices); - } - - for i in 0..(slashable_attestation.validator_indices.len() - 1) { - if slashable_attestation.validator_indices[i] - >= slashable_attestation.validator_indices[i + 1] - { - invalid!(Invalid::BadValidatorIndicesOrdering(i)); - } - } - - if !verify_bitfield_length( - &slashable_attestation.custody_bitfield, - slashable_attestation.validator_indices.len(), - ) { - invalid!(Invalid::BadCustodyBitfieldLength( - slashable_attestation.validator_indices.len(), - slashable_attestation.custody_bitfield.len() - )); - } - - if slashable_attestation.validator_indices.len() > spec.max_indices_per_slashable_vote as usize - { - invalid!(Invalid::MaxIndicesExceed( - spec.max_indices_per_slashable_vote as usize, - slashable_attestation.validator_indices.len() - )); - } - - // TODO: this signature verification could likely be replaced with: - // - // super::validate_attestation::validate_attestation_signature(..) - - let mut aggregate_pubs = vec![AggregatePublicKey::new(); 2]; - let mut message_exists = vec![false; 2]; - - for (i, v) in slashable_attestation.validator_indices.iter().enumerate() { - let custody_bit = match slashable_attestation.custody_bitfield.get(i) { - Ok(bit) => bit, - Err(_) => unreachable!(), - }; - - message_exists[custody_bit as usize] = true; - - match state.validator_registry.get(*v as usize) { - Some(validator) => { - aggregate_pubs[custody_bit as usize].add(&validator.pubkey); - } - None => invalid!(Invalid::UnknownValidator(*v)), - }; - } - - let message_0 = AttestationDataAndCustodyBit { - data: slashable_attestation.data.clone(), - custody_bit: false, - } - .tree_hash_root(); - let message_1 = AttestationDataAndCustodyBit { - data: slashable_attestation.data.clone(), - custody_bit: true, - } - .tree_hash_root(); - - let mut messages = vec![]; - let mut keys = vec![]; - - if message_exists[0] { - messages.push(&message_0[..]); - keys.push(&aggregate_pubs[0]); - } - if message_exists[1] { - messages.push(&message_1[..]); - keys.push(&aggregate_pubs[1]); - } - - let domain = { - let epoch = slashable_attestation.data.slot.epoch(spec.slots_per_epoch); - spec.get_domain(epoch, Domain::Attestation, &state.fork) - }; - - verify!( - slashable_attestation - .aggregate_signature - .verify_multiple(&messages[..], domain, &keys[..]), - Invalid::BadSignature - ); - - Ok(()) -} diff --git a/eth2/state_processing/src/per_block_processing/verify_transfer.rs b/eth2/state_processing/src/per_block_processing/verify_transfer.rs index c6388bebe..15c142d90 100644 --- a/eth2/state_processing/src/per_block_processing/verify_transfer.rs +++ b/eth2/state_processing/src/per_block_processing/verify_transfer.rs @@ -8,9 +8,7 @@ use types::*; /// /// Returns `Ok(())` if the `Transfer` is valid, otherwise indicates the reason for invalidity. /// -/// Note: this function is incomplete. -/// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn verify_transfer( state: &BeaconState, transfer: &Transfer, @@ -20,6 +18,8 @@ pub fn verify_transfer( } /// Like `verify_transfer` but doesn't run checks which may become true in future states. +/// +/// Spec v0.6.1 pub fn verify_transfer_time_independent_only( state: &BeaconState, transfer: &Transfer, @@ -29,6 +29,15 @@ pub fn verify_transfer_time_independent_only( } /// Parametric version of `verify_transfer` that allows some checks to be skipped. +/// +/// When `time_independent_only == true`, time-specific parameters are ignored, including: +/// +/// - Balance considerations (e.g., adequate balance, not dust, etc). +/// - `transfer.slot` does not have to exactly match `state.slot`, it just needs to be in the +/// present or future. +/// - Validator transfer eligibility (e.g., is withdrawable) +/// +/// Spec v0.6.1 fn verify_transfer_parametric( state: &BeaconState, transfer: &Transfer, @@ -36,35 +45,44 @@ fn verify_transfer_parametric( time_independent_only: bool, ) -> Result<(), Error> { let sender_balance = *state - .validator_balances + .balances .get(transfer.sender as usize) .ok_or_else(|| Error::Invalid(Invalid::FromValidatorUnknown(transfer.sender)))?; + let recipient_balance = *state + .balances + .get(transfer.recipient as usize) + .ok_or_else(|| Error::Invalid(Invalid::FromValidatorUnknown(transfer.recipient)))?; + + // Safely determine `amount + fee`. let total_amount = transfer .amount .checked_add(transfer.fee) .ok_or_else(|| Error::Invalid(Invalid::FeeOverflow(transfer.amount, transfer.fee)))?; + // Verify the sender has adequate balance. verify!( time_independent_only || sender_balance >= transfer.amount, Invalid::FromBalanceInsufficient(transfer.amount, sender_balance) ); - verify!( - time_independent_only || sender_balance >= transfer.fee, - Invalid::FromBalanceInsufficient(transfer.fee, sender_balance) - ); - + // Verify sender balance will not be "dust" (i.e., greater than zero but less than the minimum deposit + // amount). verify!( time_independent_only || (sender_balance == total_amount) || (sender_balance >= (total_amount + spec.min_deposit_amount)), - Invalid::InvalidResultingFromBalance( - sender_balance - total_amount, - spec.min_deposit_amount - ) + Invalid::SenderDust(sender_balance - total_amount, spec.min_deposit_amount) ); + // Verify the recipient balance will not be dust. + verify!( + time_independent_only || ((recipient_balance + transfer.amount) >= spec.min_deposit_amount), + Invalid::RecipientDust(sender_balance - total_amount, spec.min_deposit_amount) + ); + + // If loosely enforcing `transfer.slot`, ensure the slot is not in the past. Otherwise, ensure + // the transfer slot equals the state slot. if time_independent_only { verify!( state.slot <= transfer.slot, @@ -77,19 +95,33 @@ fn verify_transfer_parametric( ); } + // Load the sender `Validator` record from the state. let sender_validator = state .validator_registry .get(transfer.sender as usize) .ok_or_else(|| Error::Invalid(Invalid::FromValidatorUnknown(transfer.sender)))?; + let epoch = state.slot.epoch(spec.slots_per_epoch); + // Ensure one of the following is met: + // + // - Time independent checks are being ignored. + // - The sender has not been activated. + // - The sender is withdrawable at the state's epoch. + // - The transfer will not reduce the sender below the max effective balance. verify!( time_independent_only + || sender_validator.activation_eligibility_epoch == spec.far_future_epoch || sender_validator.is_withdrawable_at(epoch) - || sender_validator.activation_epoch == spec.far_future_epoch, + || total_amount + spec.max_effective_balance <= sender_balance, Invalid::FromValidatorIneligableForTransfer(transfer.sender) ); + // Ensure the withdrawal credentials generated from the sender's pubkey match those stored in + // the validator registry. + // + // This ensures the validator can only perform a transfer when they are in control of the + // withdrawal address. let transfer_withdrawal_credentials = Hash256::from_slice( &get_withdrawal_credentials(&transfer.pubkey, spec.bls_withdrawal_prefix_byte)[..], ); @@ -101,13 +133,13 @@ fn verify_transfer_parametric( ) ); + // Verify the transfer signature. let message = transfer.signed_root(); let domain = spec.get_domain( transfer.slot.epoch(spec.slots_per_epoch), Domain::Transfer, &state.fork, ); - verify!( transfer .signature @@ -122,31 +154,31 @@ fn verify_transfer_parametric( /// /// Does not check that the transfer is valid, however checks for overflow in all actions. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn execute_transfer( state: &mut BeaconState, transfer: &Transfer, spec: &ChainSpec, ) -> Result<(), Error> { let sender_balance = *state - .validator_balances + .balances .get(transfer.sender as usize) .ok_or_else(|| Error::Invalid(Invalid::FromValidatorUnknown(transfer.sender)))?; let recipient_balance = *state - .validator_balances + .balances .get(transfer.recipient as usize) .ok_or_else(|| Error::Invalid(Invalid::ToValidatorUnknown(transfer.recipient)))?; let proposer_index = state.get_beacon_proposer_index(state.slot, RelativeEpoch::Current, spec)?; - let proposer_balance = state.validator_balances[proposer_index]; + let proposer_balance = state.balances[proposer_index]; let total_amount = transfer .amount .checked_add(transfer.fee) .ok_or_else(|| Error::Invalid(Invalid::FeeOverflow(transfer.amount, transfer.fee)))?; - state.validator_balances[transfer.sender as usize] = + state.balances[transfer.sender as usize] = sender_balance.checked_sub(total_amount).ok_or_else(|| { Error::Invalid(Invalid::FromBalanceInsufficient( total_amount, @@ -154,7 +186,7 @@ pub fn execute_transfer( )) })?; - state.validator_balances[transfer.recipient as usize] = recipient_balance + state.balances[transfer.recipient as usize] = recipient_balance .checked_add(transfer.amount) .ok_or_else(|| { Error::Invalid(Invalid::ToBalanceOverflow( @@ -163,7 +195,7 @@ pub fn execute_transfer( )) })?; - state.validator_balances[proposer_index] = + state.balances[proposer_index] = proposer_balance.checked_add(transfer.fee).ok_or_else(|| { Error::Invalid(Invalid::ProposerBalanceOverflow( proposer_balance, diff --git a/eth2/state_processing/src/per_epoch_processing.rs b/eth2/state_processing/src/per_epoch_processing.rs index 3f8eb5112..d261b8b47 100644 --- a/eth2/state_processing/src/per_epoch_processing.rs +++ b/eth2/state_processing/src/per_epoch_processing.rs @@ -1,24 +1,18 @@ -use apply_rewards::apply_rewards; +use apply_rewards::process_rewards_and_penalties; use errors::EpochProcessingError as Error; -use process_ejections::process_ejections; -use process_exit_queue::process_exit_queue; use process_slashings::process_slashings; +use registry_updates::process_registry_updates; use std::collections::HashMap; use tree_hash::TreeHash; use types::*; -use update_registry_and_shuffling_data::update_registry_and_shuffling_data; use validator_statuses::{TotalBalances, ValidatorStatuses}; use winning_root::{winning_root, WinningRoot}; pub mod apply_rewards; pub mod errors; -pub mod get_attestation_participants; -pub mod inclusion_distance; -pub mod process_ejections; -pub mod process_exit_queue; pub mod process_slashings; +pub mod registry_updates; pub mod tests; -pub mod update_registry_and_shuffling_data; pub mod validator_statuses; pub mod winning_root; @@ -32,14 +26,14 @@ pub type WinningRootHashSet = HashMap; /// Mutates the given `BeaconState`, returning early if an error is encountered. If an error is /// returned, a state might be "half-processed" and therefore in an invalid state. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn per_epoch_processing( state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { // Ensure the previous and next epoch caches are built. - state.build_epoch_cache(RelativeEpoch::Previous, spec)?; - state.build_epoch_cache(RelativeEpoch::Current, spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + state.build_committee_cache(RelativeEpoch::Current, spec)?; // Load the struct we use to assign validators into sets based on their participation. // @@ -47,39 +41,28 @@ pub fn per_epoch_processing( let mut validator_statuses = ValidatorStatuses::new(state, spec)?; validator_statuses.process_attestations(&state, spec)?; - // Justification. - update_justification_and_finalization(state, &validator_statuses.total_balances, spec)?; + // Justification and finalization. + process_justification_and_finalization(state, &validator_statuses.total_balances, spec)?; // Crosslinks. let winning_root_for_shards = process_crosslinks(state, spec)?; - // Eth1 data. - maybe_reset_eth1_period(state, spec); - // Rewards and Penalities. - apply_rewards( + process_rewards_and_penalties( state, &mut validator_statuses, &winning_root_for_shards, spec, )?; - // Ejections. - process_ejections(state, spec)?; + // Registry Updates. + process_registry_updates(state, spec)?; - // Validator Registry. - update_registry_and_shuffling_data( - state, - validator_statuses.total_balances.current_epoch, - spec, - )?; - - // Slashings and exit queue. + // Slashings. process_slashings(state, validator_statuses.total_balances.current_epoch, spec)?; - process_exit_queue(state, spec); // Final updates. - finish_epoch_update(state, spec)?; + process_final_updates(state, spec)?; // Rotate the epoch caches to suit the epoch transition. state.advance_caches(); @@ -87,89 +70,72 @@ pub fn per_epoch_processing( Ok(()) } -/// Maybe resets the eth1 period. -/// -/// Spec v0.5.1 -pub fn maybe_reset_eth1_period(state: &mut BeaconState, spec: &ChainSpec) { - let next_epoch = state.next_epoch(spec); - let voting_period = spec.epochs_per_eth1_voting_period; - - if next_epoch % voting_period == 0 { - for eth1_data_vote in &state.eth1_data_votes { - if eth1_data_vote.vote_count * 2 > voting_period * spec.slots_per_epoch { - state.latest_eth1_data = eth1_data_vote.eth1_data.clone(); - } - } - state.eth1_data_votes = vec![]; - } -} - /// Update the following fields on the `BeaconState`: /// /// - `justification_bitfield`. -/// - `finalized_epoch` -/// - `justified_epoch` /// - `previous_justified_epoch` +/// - `previous_justified_root` +/// - `current_justified_epoch` +/// - `current_justified_root` +/// - `finalized_epoch` +/// - `finalized_root` /// -/// Spec v0.5.1 -pub fn update_justification_and_finalization( +/// Spec v0.6.1 +pub fn process_justification_and_finalization( state: &mut BeaconState, total_balances: &TotalBalances, spec: &ChainSpec, ) -> Result<(), Error> { - let previous_epoch = state.previous_epoch(spec); - let current_epoch = state.current_epoch(spec); + if state.current_epoch() == spec.genesis_epoch { + return Ok(()); + } - let mut new_justified_epoch = state.current_justified_epoch; - let mut new_finalized_epoch = state.finalized_epoch; + let previous_epoch = state.previous_epoch(); + let current_epoch = state.current_epoch(); - // Rotate the justification bitfield up one epoch to make room for the current epoch. + let old_previous_justified_epoch = state.previous_justified_epoch; + let old_current_justified_epoch = state.current_justified_epoch; + + // Process justifications + state.previous_justified_epoch = state.current_justified_epoch; + state.previous_justified_root = state.current_justified_root; state.justification_bitfield <<= 1; - // If the previous epoch gets justified, full the second last bit. - if (total_balances.previous_epoch_boundary_attesters * 3) >= (total_balances.previous_epoch * 2) - { - new_justified_epoch = previous_epoch; + if total_balances.previous_epoch_target_attesters * 3 >= total_balances.previous_epoch * 2 { + state.current_justified_epoch = previous_epoch; + state.current_justified_root = + *state.get_block_root_at_epoch(state.current_justified_epoch, spec)?; state.justification_bitfield |= 2; } // If the current epoch gets justified, fill the last bit. - if (total_balances.current_epoch_boundary_attesters * 3) >= (total_balances.current_epoch * 2) { - new_justified_epoch = current_epoch; + if total_balances.current_epoch_target_attesters * 3 >= total_balances.current_epoch * 2 { + state.current_justified_epoch = current_epoch; + state.current_justified_root = + *state.get_block_root_at_epoch(state.current_justified_epoch, spec)?; state.justification_bitfield |= 1; } let bitfield = state.justification_bitfield; // The 2nd/3rd/4th most recent epochs are all justified, the 2nd using the 4th as source. - if ((bitfield >> 1) % 8 == 0b111) & (state.previous_justified_epoch == current_epoch - 3) { - new_finalized_epoch = state.previous_justified_epoch; + if (bitfield >> 1) % 8 == 0b111 && old_previous_justified_epoch == current_epoch - 3 { + state.finalized_epoch = old_previous_justified_epoch; + state.finalized_root = *state.get_block_root_at_epoch(state.finalized_epoch, spec)?; } // The 2nd/3rd most recent epochs are both justified, the 2nd using the 3rd as source. - if ((bitfield >> 1) % 4 == 0b11) & (state.previous_justified_epoch == current_epoch - 2) { - new_finalized_epoch = state.previous_justified_epoch; + if (bitfield >> 1) % 4 == 0b11 && state.previous_justified_epoch == current_epoch - 2 { + state.finalized_epoch = old_previous_justified_epoch; + state.finalized_root = *state.get_block_root_at_epoch(state.finalized_epoch, spec)?; } // The 1st/2nd/3rd most recent epochs are all justified, the 1st using the 2nd as source. - if (bitfield % 8 == 0b111) & (state.current_justified_epoch == current_epoch - 2) { - new_finalized_epoch = state.current_justified_epoch; + if bitfield % 8 == 0b111 && state.current_justified_epoch == current_epoch - 2 { + state.finalized_epoch = old_current_justified_epoch; + state.finalized_root = *state.get_block_root_at_epoch(state.finalized_epoch, spec)?; } // The 1st/2nd most recent epochs are both justified, the 1st using the 2nd as source. - if (bitfield % 4 == 0b11) & (state.current_justified_epoch == current_epoch - 1) { - new_finalized_epoch = state.current_justified_epoch; - } - - state.previous_justified_epoch = state.current_justified_epoch; - state.previous_justified_root = state.current_justified_root; - - if new_justified_epoch != state.current_justified_epoch { - state.current_justified_epoch = new_justified_epoch; - state.current_justified_root = - *state.get_block_root(new_justified_epoch.start_slot(spec.slots_per_epoch))?; - } - - if new_finalized_epoch != state.finalized_epoch { - state.finalized_epoch = new_finalized_epoch; - state.finalized_root = - *state.get_block_root(new_finalized_epoch.start_slot(spec.slots_per_epoch))?; + if bitfield % 4 == 0b11 && state.current_justified_epoch == current_epoch - 1 { + state.finalized_epoch = old_current_justified_epoch; + state.finalized_root = *state.get_block_root_at_epoch(state.finalized_epoch, spec)?; } Ok(()) @@ -177,42 +143,36 @@ pub fn update_justification_and_finalization( /// Updates the following fields on the `BeaconState`: /// -/// - `latest_crosslinks` +/// - `previous_crosslinks` +/// - `current_crosslinks` /// /// Also returns a `WinningRootHashSet` for later use during epoch processing. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_crosslinks( state: &mut BeaconState, spec: &ChainSpec, ) -> Result { let mut winning_root_for_shards: WinningRootHashSet = HashMap::new(); - let previous_and_current_epoch_slots: Vec = state - .previous_epoch(spec) - .slot_iter(spec.slots_per_epoch) - .chain(state.current_epoch(spec).slot_iter(spec.slots_per_epoch)) - .collect(); + state.previous_crosslinks = state.current_crosslinks.clone(); - for slot in previous_and_current_epoch_slots { - // Clone removes the borrow which becomes an issue when mutating `state.balances`. - let crosslink_committees_at_slot = - state.get_crosslink_committees_at_slot(slot, spec)?.clone(); + for relative_epoch in vec![RelativeEpoch::Previous, RelativeEpoch::Current] { + let epoch = relative_epoch.into_epoch(state.current_epoch()); + for offset in 0..state.get_epoch_committee_count(relative_epoch)? { + let shard = + (state.get_epoch_start_shard(relative_epoch)? + offset) % T::ShardCount::to_u64(); + let crosslink_committee = + state.get_crosslink_committee_for_shard(shard, relative_epoch)?; - for c in crosslink_committees_at_slot { - let shard = c.shard as u64; - - let winning_root = winning_root(state, shard, spec)?; + let winning_root = winning_root(state, shard, epoch, spec)?; if let Some(winning_root) = winning_root { - let total_committee_balance = state.get_total_balance(&c.committee, spec)?; + let total_committee_balance = + state.get_total_balance(&crosslink_committee.committee, spec)?; - // TODO: I think this has a bug. - if (3 * winning_root.total_attesting_balance) >= (2 * total_committee_balance) { - state.latest_crosslinks[shard as usize] = Crosslink { - epoch: slot.epoch(spec.slots_per_epoch), - crosslink_data_root: winning_root.crosslink_data_root, - } + if 3 * winning_root.total_attesting_balance >= 2 * total_committee_balance { + state.current_crosslinks[shard as usize] = winning_root.crosslink.clone(); } winning_root_for_shards.insert(shard, winning_root); } @@ -224,13 +184,35 @@ pub fn process_crosslinks( /// Finish up an epoch update. /// -/// Spec v0.5.1 -pub fn finish_epoch_update( +/// Spec v0.6.1 +pub fn process_final_updates( state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { - let current_epoch = state.current_epoch(spec); - let next_epoch = state.next_epoch(spec); + let current_epoch = state.current_epoch(); + let next_epoch = state.next_epoch(); + + // Reset eth1 data votes. + if (state.slot + 1) % spec.slots_per_eth1_voting_period == 0 { + state.eth1_data_votes = vec![]; + } + + // Update effective balances with hysteresis (lag). + for (index, validator) in state.validator_registry.iter_mut().enumerate() { + let balance = state.balances[index]; + let half_increment = spec.effective_balance_increment / 2; + if balance < validator.effective_balance + || validator.effective_balance + 3 * half_increment < balance + { + validator.effective_balance = std::cmp::min( + balance - balance % spec.effective_balance_increment, + spec.max_effective_balance, + ); + } + } + + // Update start shard. + state.latest_start_shard = state.next_epoch_start_shard()?; // This is a hack to allow us to update index roots and slashed balances for the next epoch. // @@ -250,11 +232,7 @@ pub fn finish_epoch_update( state.set_slashed_balance(next_epoch, state.get_slashed_balance(current_epoch)?)?; // Set randao mix - state.set_randao_mix( - next_epoch, - *state.get_randao_mix(current_epoch, spec)?, - spec, - )?; + state.set_randao_mix(next_epoch, *state.get_randao_mix(current_epoch)?)?; state.slot -= 1; } @@ -266,8 +244,9 @@ pub fn finish_epoch_update( .push(Hash256::from_slice(&historical_batch.tree_hash_root()[..])); } - state.previous_epoch_attestations = state.current_epoch_attestations.clone(); - state.current_epoch_attestations = vec![]; + // Rotate current/previous epoch attestations + state.previous_epoch_attestations = + std::mem::replace(&mut state.current_epoch_attestations, vec![]); Ok(()) } diff --git a/eth2/state_processing/src/per_epoch_processing/apply_rewards.rs b/eth2/state_processing/src/per_epoch_processing/apply_rewards.rs index f529523fb..7f98f3ae5 100644 --- a/eth2/state_processing/src/per_epoch_processing/apply_rewards.rs +++ b/eth2/state_processing/src/per_epoch_processing/apply_rewards.rs @@ -32,57 +32,52 @@ impl std::ops::AddAssign for Delta { /// Apply attester and proposer rewards. /// -/// Spec v0.5.1 -pub fn apply_rewards( +/// Spec v0.6.1 +pub fn process_rewards_and_penalties( state: &mut BeaconState, validator_statuses: &mut ValidatorStatuses, winning_root_for_shards: &WinningRootHashSet, spec: &ChainSpec, ) -> Result<(), Error> { + if state.current_epoch() == spec.genesis_epoch { + return Ok(()); + } + // Guard against an out-of-bounds during the validator balance update. - if validator_statuses.statuses.len() != state.validator_balances.len() { - return Err(Error::ValidatorStatusesInconsistent); - } - // Guard against an out-of-bounds during the attester inclusion balance update. - if validator_statuses.statuses.len() != state.validator_registry.len() { + if validator_statuses.statuses.len() != state.balances.len() + || validator_statuses.statuses.len() != state.validator_registry.len() + { return Err(Error::ValidatorStatusesInconsistent); } - let mut deltas = vec![Delta::default(); state.validator_balances.len()]; + let mut deltas = vec![Delta::default(); state.balances.len()]; - get_justification_and_finalization_deltas(&mut deltas, state, &validator_statuses, spec)?; + get_attestation_deltas(&mut deltas, state, &validator_statuses, spec)?; get_crosslink_deltas(&mut deltas, state, &validator_statuses, spec)?; - // Apply the proposer deltas if we are finalizing normally. - // - // This is executed slightly differently to the spec because of the way our functions are - // structured. It should be functionally equivalent. - if epochs_since_finality(state, spec) <= 4 { - get_proposer_deltas( - &mut deltas, - state, - validator_statuses, - winning_root_for_shards, - spec, - )?; - } + get_proposer_deltas( + &mut deltas, + state, + validator_statuses, + winning_root_for_shards, + spec, + )?; // Apply the deltas, over-flowing but not under-flowing (saturating at 0 instead). for (i, delta) in deltas.iter().enumerate() { - state.validator_balances[i] += delta.rewards; - state.validator_balances[i] = state.validator_balances[i].saturating_sub(delta.penalties); + state.balances[i] += delta.rewards; + state.balances[i] = state.balances[i].saturating_sub(delta.penalties); } Ok(()) } -/// Applies the attestation inclusion reward to each proposer for every validator who included an -/// attestation in the previous epoch. +/// For each attesting validator, reward the proposer who was first to include their attestation. /// -/// Spec v0.5.1 +/// Spec v0.6.1 fn get_proposer_deltas( deltas: &mut Vec, - state: &mut BeaconState, + state: &BeaconState, validator_statuses: &mut ValidatorStatuses, winning_root_for_shards: &WinningRootHashSet, spec: &ChainSpec, @@ -90,9 +85,7 @@ fn get_proposer_deltas( // Update statuses with the information from winning roots. validator_statuses.process_winning_roots(state, winning_root_for_shards, spec)?; - for (index, validator) in validator_statuses.statuses.iter().enumerate() { - let mut delta = Delta::default(); - + for validator in &validator_statuses.statuses { if validator.is_previous_epoch_attester { let inclusion = validator .inclusion_info @@ -101,7 +94,7 @@ fn get_proposer_deltas( let base_reward = get_base_reward( state, inclusion.proposer_index, - validator_statuses.total_balances.previous_epoch, + validator_statuses.total_balances.current_epoch, spec, )?; @@ -109,10 +102,8 @@ fn get_proposer_deltas( return Err(Error::ValidatorStatusesInconsistent); } - delta.reward(base_reward / spec.attestation_inclusion_reward_quotient); + deltas[inclusion.proposer_index].reward(base_reward / spec.proposer_reward_quotient); } - - deltas[index] += delta; } Ok(()) @@ -120,40 +111,30 @@ fn get_proposer_deltas( /// Apply rewards for participation in attestations during the previous epoch. /// -/// Spec v0.5.1 -fn get_justification_and_finalization_deltas( +/// Spec v0.6.1 +fn get_attestation_deltas( deltas: &mut Vec, state: &BeaconState, validator_statuses: &ValidatorStatuses, spec: &ChainSpec, ) -> Result<(), Error> { - let epochs_since_finality = epochs_since_finality(state, spec); + let finality_delay = (state.previous_epoch() - state.finalized_epoch).as_u64(); for (index, validator) in validator_statuses.statuses.iter().enumerate() { let base_reward = get_base_reward( state, index, - validator_statuses.total_balances.previous_epoch, - spec, - )?; - let inactivity_penalty = get_inactivity_penalty( - state, - index, - epochs_since_finality.as_u64(), - validator_statuses.total_balances.previous_epoch, + validator_statuses.total_balances.current_epoch, spec, )?; - let delta = if epochs_since_finality <= 4 { - compute_normal_justification_and_finalization_delta( - &validator, - &validator_statuses.total_balances, - base_reward, - spec, - ) - } else { - compute_inactivity_leak_delta(&validator, base_reward, inactivity_penalty, spec) - }; + let delta = get_attestation_delta( + &validator, + &validator_statuses.total_balances, + base_reward, + finality_delay, + spec, + ); deltas[index] += delta; } @@ -161,51 +142,79 @@ fn get_justification_and_finalization_deltas( Ok(()) } -/// Determine the delta for a single validator, if the chain is finalizing normally. +/// Determine the delta for a single validator, sans proposer rewards. /// -/// Spec v0.5.1 -fn compute_normal_justification_and_finalization_delta( +/// Spec v0.6.1 +fn get_attestation_delta( validator: &ValidatorStatus, total_balances: &TotalBalances, base_reward: u64, + finality_delay: u64, spec: &ChainSpec, ) -> Delta { let mut delta = Delta::default(); - let boundary_attesting_balance = total_balances.previous_epoch_boundary_attesters; - let total_balance = total_balances.previous_epoch; + // Is this validator eligible to be rewarded or penalized? + // Spec: validator index in `eligible_validator_indices` + let is_eligible = validator.is_active_in_previous_epoch + || (validator.is_slashed && !validator.is_withdrawable_in_current_epoch); + + if !is_eligible { + return delta; + } + + let total_balance = total_balances.current_epoch; let total_attesting_balance = total_balances.previous_epoch_attesters; - let matching_head_balance = total_balances.previous_epoch_boundary_attesters; + let matching_target_balance = total_balances.previous_epoch_target_attesters; + let matching_head_balance = total_balances.previous_epoch_head_attesters; // Expected FFG source. - if validator.is_previous_epoch_attester { + // Spec: + // - validator index in `get_unslashed_attesting_indices(state, matching_source_attestations)` + if validator.is_previous_epoch_attester && !validator.is_slashed { delta.reward(base_reward * total_attesting_balance / total_balance); // Inclusion speed bonus let inclusion = validator .inclusion_info .expect("It is a logic error for an attester not to have an inclusion distance."); - delta.reward( - base_reward * spec.min_attestation_inclusion_delay / inclusion.distance.as_u64(), - ); - } else if validator.is_active_in_previous_epoch { + delta.reward(base_reward * spec.min_attestation_inclusion_delay / inclusion.distance); + } else { delta.penalize(base_reward); } // Expected FFG target. - if validator.is_previous_epoch_boundary_attester { - delta.reward(base_reward / boundary_attesting_balance / total_balance); - } else if validator.is_active_in_previous_epoch { + // Spec: + // - validator index in `get_unslashed_attesting_indices(state, matching_target_attestations)` + if validator.is_previous_epoch_target_attester && !validator.is_slashed { + delta.reward(base_reward * matching_target_balance / total_balance); + } else { delta.penalize(base_reward); } // Expected head. - if validator.is_previous_epoch_head_attester { + // Spec: + // - validator index in `get_unslashed_attesting_indices(state, matching_head_attestations)` + if validator.is_previous_epoch_head_attester && !validator.is_slashed { delta.reward(base_reward * matching_head_balance / total_balance); - } else if validator.is_active_in_previous_epoch { + } else { delta.penalize(base_reward); - }; + } - // Proposer bonus is handled in `apply_proposer_deltas`. + // Inactivity penalty + if finality_delay > spec.min_epochs_to_inactivity_penalty { + // All eligible validators are penalized + delta.penalize(spec.base_rewards_per_epoch * base_reward); + + // Additionally, all validators whose FFG target didn't match are penalized extra + if !validator.is_previous_epoch_target_attester { + delta.penalize( + validator.current_epoch_effective_balance * finality_delay + / spec.inactivity_penalty_quotient, + ); + } + } + + // Proposer bonus is handled in `get_proposer_deltas`. // // This function only computes the delta for a single validator, so it cannot also return a // delta for a validator. @@ -213,55 +222,9 @@ fn compute_normal_justification_and_finalization_delta( delta } -/// Determine the delta for a single delta, assuming the chain is _not_ finalizing normally. -/// -/// Spec v0.5.1 -fn compute_inactivity_leak_delta( - validator: &ValidatorStatus, - base_reward: u64, - inactivity_penalty: u64, - spec: &ChainSpec, -) -> Delta { - let mut delta = Delta::default(); - - if validator.is_active_in_previous_epoch { - if !validator.is_previous_epoch_attester { - delta.penalize(inactivity_penalty); - } else { - // If a validator did attest, apply a small penalty for getting attestations included - // late. - let inclusion = validator - .inclusion_info - .expect("It is a logic error for an attester not to have an inclusion distance."); - delta.reward( - base_reward * spec.min_attestation_inclusion_delay / inclusion.distance.as_u64(), - ); - delta.penalize(base_reward); - } - - if !validator.is_previous_epoch_boundary_attester { - delta.reward(inactivity_penalty); - } - - if !validator.is_previous_epoch_head_attester { - delta.penalize(inactivity_penalty); - } - } - - // Penalize slashed-but-inactive validators as though they were active but offline. - if !validator.is_active_in_previous_epoch - & validator.is_slashed - & !validator.is_withdrawable_in_current_epoch - { - delta.penalize(2 * inactivity_penalty + base_reward); - } - - delta -} - /// Calculate the deltas based upon the winning roots for attestations during the previous epoch. /// -/// Spec v0.5.1 +/// Spec v0.6.1 fn get_crosslink_deltas( deltas: &mut Vec, state: &BeaconState, @@ -274,7 +237,7 @@ fn get_crosslink_deltas( let base_reward = get_base_reward( state, index, - validator_statuses.total_balances.previous_epoch, + validator_statuses.total_balances.current_epoch, spec, )?; @@ -295,40 +258,20 @@ fn get_crosslink_deltas( /// Returns the base reward for some validator. /// -/// Spec v0.5.1 +/// Spec v0.6.1 fn get_base_reward( state: &BeaconState, index: usize, - previous_total_balance: u64, + // Should be == get_total_active_balance(state, spec) + total_active_balance: u64, spec: &ChainSpec, ) -> Result { - if previous_total_balance == 0 { + if total_active_balance == 0 { Ok(0) } else { - let adjusted_quotient = previous_total_balance.integer_sqrt() / spec.base_reward_quotient; - Ok(state.get_effective_balance(index, spec)? / adjusted_quotient / 5) + let adjusted_quotient = total_active_balance.integer_sqrt() / spec.base_reward_quotient; + Ok(state.get_effective_balance(index, spec)? + / adjusted_quotient + / spec.base_rewards_per_epoch) } } - -/// Returns the inactivity penalty for some validator. -/// -/// Spec v0.5.1 -fn get_inactivity_penalty( - state: &BeaconState, - index: usize, - epochs_since_finality: u64, - previous_total_balance: u64, - spec: &ChainSpec, -) -> Result { - Ok(get_base_reward(state, index, previous_total_balance, spec)? - + state.get_effective_balance(index, spec)? * epochs_since_finality - / spec.inactivity_penalty_quotient - / 2) -} - -/// Returns the epochs since the last finalized epoch. -/// -/// Spec v0.5.1 -fn epochs_since_finality(state: &BeaconState, spec: &ChainSpec) -> Epoch { - state.current_epoch(spec) + 1 - state.finalized_epoch -} diff --git a/eth2/state_processing/src/per_epoch_processing/get_attestation_participants.rs b/eth2/state_processing/src/per_epoch_processing/get_attestation_participants.rs deleted file mode 100644 index 7f5504c56..000000000 --- a/eth2/state_processing/src/per_epoch_processing/get_attestation_participants.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::common::verify_bitfield_length; -use types::*; - -/// Returns validator indices which participated in the attestation. -/// -/// Spec v0.5.1 -pub fn get_attestation_participants( - state: &BeaconState, - attestation_data: &AttestationData, - bitfield: &Bitfield, - spec: &ChainSpec, -) -> Result, BeaconStateError> { - let epoch = attestation_data.slot.epoch(spec.slots_per_epoch); - - let crosslink_committee = - state.get_crosslink_committee_for_shard(epoch, attestation_data.shard, spec)?; - - if crosslink_committee.slot != attestation_data.slot { - return Err(BeaconStateError::NoCommitteeForShard); - } - - let committee = &crosslink_committee.committee; - - if !verify_bitfield_length(&bitfield, committee.len()) { - return Err(BeaconStateError::InvalidBitfield); - } - - let mut participants = Vec::with_capacity(committee.len()); - for (i, validator_index) in committee.iter().enumerate() { - match bitfield.get(i) { - Ok(bit) if bit => participants.push(*validator_index), - _ => {} - } - } - participants.shrink_to_fit(); - - Ok(participants) -} diff --git a/eth2/state_processing/src/per_epoch_processing/inclusion_distance.rs b/eth2/state_processing/src/per_epoch_processing/inclusion_distance.rs deleted file mode 100644 index e82d810ba..000000000 --- a/eth2/state_processing/src/per_epoch_processing/inclusion_distance.rs +++ /dev/null @@ -1,56 +0,0 @@ -use super::errors::InclusionError; -use super::get_attestation_participants::get_attestation_participants; -use types::*; - -/// Returns the distance between the first included attestation for some validator and this -/// slot. -/// -/// Spec v0.5.1 -pub fn inclusion_distance( - state: &BeaconState, - attestations: &[&PendingAttestation], - validator_index: usize, - spec: &ChainSpec, -) -> Result { - let attestation = earliest_included_attestation(state, attestations, validator_index, spec)?; - Ok((attestation.inclusion_slot - attestation.data.slot).as_u64()) -} - -/// Returns the slot of the earliest included attestation for some validator. -/// -/// Spec v0.5.1 -pub fn inclusion_slot( - state: &BeaconState, - attestations: &[&PendingAttestation], - validator_index: usize, - spec: &ChainSpec, -) -> Result { - let attestation = earliest_included_attestation(state, attestations, validator_index, spec)?; - Ok(attestation.inclusion_slot) -} - -/// Finds the earliest included attestation for some validator. -/// -/// Spec v0.5.1 -fn earliest_included_attestation( - state: &BeaconState, - attestations: &[&PendingAttestation], - validator_index: usize, - spec: &ChainSpec, -) -> Result { - let mut included_attestations = vec![]; - - for (i, a) in attestations.iter().enumerate() { - let participants = - get_attestation_participants(state, &a.data, &a.aggregation_bitfield, spec)?; - if participants.iter().any(|i| *i == validator_index) { - included_attestations.push(i); - } - } - - let earliest_attestation_index = included_attestations - .iter() - .min_by_key(|i| attestations[**i].inclusion_slot) - .ok_or_else(|| InclusionError::NoAttestationsForValidator)?; - Ok(attestations[*earliest_attestation_index].clone()) -} diff --git a/eth2/state_processing/src/per_epoch_processing/process_ejections.rs b/eth2/state_processing/src/per_epoch_processing/process_ejections.rs deleted file mode 100644 index e32241e24..000000000 --- a/eth2/state_processing/src/per_epoch_processing/process_ejections.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::common::exit_validator; -use types::{BeaconStateError as Error, *}; - -/// Iterate through the validator registry and eject active validators with balance below -/// ``EJECTION_BALANCE``. -/// -/// Spec v0.5.1 -pub fn process_ejections( - state: &mut BeaconState, - spec: &ChainSpec, -) -> Result<(), Error> { - // There is an awkward double (triple?) loop here because we can't loop across the borrowed - // active validator indices and mutate state in the one loop. - let exitable: Vec = state - .get_cached_active_validator_indices(RelativeEpoch::Current, spec)? - .iter() - .filter_map(|&i| { - if state.validator_balances[i as usize] < spec.ejection_balance { - Some(i) - } else { - None - } - }) - .collect(); - - for validator_index in exitable { - exit_validator(state, validator_index, spec)? - } - - Ok(()) -} diff --git a/eth2/state_processing/src/per_epoch_processing/process_exit_queue.rs b/eth2/state_processing/src/per_epoch_processing/process_exit_queue.rs deleted file mode 100644 index eafe4541e..000000000 --- a/eth2/state_processing/src/per_epoch_processing/process_exit_queue.rs +++ /dev/null @@ -1,42 +0,0 @@ -use types::*; - -/// Process the exit queue. -/// -/// Spec v0.5.1 -pub fn process_exit_queue(state: &mut BeaconState, spec: &ChainSpec) { - let current_epoch = state.current_epoch(spec); - - let eligible = |index: usize| { - let validator = &state.validator_registry[index]; - - if validator.withdrawable_epoch != spec.far_future_epoch { - false - } else { - current_epoch >= validator.exit_epoch + spec.min_validator_withdrawability_delay - } - }; - - let mut eligable_indices: Vec = (0..state.validator_registry.len()) - .filter(|i| eligible(*i)) - .collect(); - eligable_indices.sort_by_key(|i| state.validator_registry[*i].exit_epoch); - - for (dequeues, index) in eligable_indices.iter().enumerate() { - if dequeues as u64 >= spec.max_exit_dequeues_per_epoch { - break; - } - prepare_validator_for_withdrawal(state, *index, spec); - } -} - -/// Initiate an exit for the validator of the given `index`. -/// -/// Spec v0.5.1 -fn prepare_validator_for_withdrawal( - state: &mut BeaconState, - validator_index: usize, - spec: &ChainSpec, -) { - state.validator_registry[validator_index].withdrawable_epoch = - state.current_epoch(spec) + spec.min_validator_withdrawability_delay; -} diff --git a/eth2/state_processing/src/per_epoch_processing/process_slashings.rs b/eth2/state_processing/src/per_epoch_processing/process_slashings.rs index a70720cce..020534f80 100644 --- a/eth2/state_processing/src/per_epoch_processing/process_slashings.rs +++ b/eth2/state_processing/src/per_epoch_processing/process_slashings.rs @@ -2,13 +2,13 @@ use types::{BeaconStateError as Error, *}; /// Process slashings. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn process_slashings( state: &mut BeaconState, current_total_balance: u64, spec: &ChainSpec, ) -> Result<(), Error> { - let current_epoch = state.current_epoch(spec); + let current_epoch = state.current_epoch(); let total_at_start = state.get_slashed_balance(current_epoch + 1)?; let total_at_end = state.get_slashed_balance(current_epoch)?; @@ -24,10 +24,10 @@ pub fn process_slashings( let penalty = std::cmp::max( effective_balance * std::cmp::min(total_penalities * 3, current_total_balance) / current_total_balance, - effective_balance / spec.min_penalty_quotient, + effective_balance / spec.min_slashing_penalty_quotient, ); - state.validator_balances[index] -= penalty; + safe_sub_assign!(state.balances[index], penalty); } } diff --git a/eth2/state_processing/src/per_epoch_processing/registry_updates.rs b/eth2/state_processing/src/per_epoch_processing/registry_updates.rs new file mode 100644 index 000000000..469a14fdd --- /dev/null +++ b/eth2/state_processing/src/per_epoch_processing/registry_updates.rs @@ -0,0 +1,69 @@ +use super::super::common::initiate_validator_exit; +use super::Error; +use itertools::{Either, Itertools}; +use types::*; + +/// Peforms a validator registry update, if required. +/// +/// Spec v0.6.1 +pub fn process_registry_updates( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + // Process activation eligibility and ejections. + // Collect eligible and exiting validators (we need to avoid mutating the state while iterating). + // We assume it's safe to re-order the change in eligibility and `initiate_validator_exit`. + // Rest assured exiting validators will still be exited in the same order as in the spec. + let current_epoch = state.current_epoch(); + let is_eligible = |validator: &Validator| { + validator.activation_eligibility_epoch == spec.far_future_epoch + && validator.effective_balance >= spec.max_effective_balance + }; + let is_exiting_validator = |validator: &Validator| { + validator.is_active_at(current_epoch) + && validator.effective_balance <= spec.ejection_balance + }; + let (eligible_validators, exiting_validators): (Vec<_>, Vec<_>) = state + .validator_registry + .iter() + .enumerate() + .filter(|(_, validator)| is_eligible(validator) || is_exiting_validator(validator)) + .partition_map(|(index, validator)| { + if is_eligible(validator) { + Either::Left(index) + } else { + Either::Right(index) + } + }); + for index in eligible_validators { + state.validator_registry[index].activation_eligibility_epoch = current_epoch; + } + for index in exiting_validators { + initiate_validator_exit(state, index, spec)?; + } + + // Queue validators eligible for activation and not dequeued for activation prior to finalized epoch + let activation_queue = state + .validator_registry + .iter() + .enumerate() + .filter(|(_, validator)| { + validator.activation_eligibility_epoch != spec.far_future_epoch + && validator.activation_epoch + >= state.get_delayed_activation_exit_epoch(state.finalized_epoch, spec) + }) + .sorted_by_key(|(_, validator)| validator.activation_eligibility_epoch) + .map(|(index, _)| index) + .collect_vec(); + + let churn_limit = state.get_churn_limit(spec)? as usize; + let delayed_activation_epoch = state.get_delayed_activation_exit_epoch(current_epoch, spec); + for index in activation_queue.into_iter().take(churn_limit) { + let validator = &mut state.validator_registry[index]; + if validator.activation_epoch == spec.far_future_epoch { + validator.activation_epoch = delayed_activation_epoch; + } + } + + Ok(()) +} diff --git a/eth2/state_processing/src/per_epoch_processing/tests.rs b/eth2/state_processing/src/per_epoch_processing/tests.rs index b075c5cd4..9841a5551 100644 --- a/eth2/state_processing/src/per_epoch_processing/tests.rs +++ b/eth2/state_processing/src/per_epoch_processing/tests.rs @@ -1,5 +1,5 @@ #![cfg(test)] -use crate::per_epoch_processing; +use crate::per_epoch_processing::per_epoch_processing; use env_logger::{Builder, Env}; use types::test_utils::TestingBeaconStateBuilder; use types::*; diff --git a/eth2/state_processing/src/per_epoch_processing/update_registry_and_shuffling_data.rs b/eth2/state_processing/src/per_epoch_processing/update_registry_and_shuffling_data.rs deleted file mode 100644 index f6548fb67..000000000 --- a/eth2/state_processing/src/per_epoch_processing/update_registry_and_shuffling_data.rs +++ /dev/null @@ -1,150 +0,0 @@ -use super::super::common::exit_validator; -use super::Error; -use types::*; - -/// Peforms a validator registry update, if required. -/// -/// Spec v0.5.1 -pub fn update_registry_and_shuffling_data( - state: &mut BeaconState, - current_total_balance: u64, - spec: &ChainSpec, -) -> Result<(), Error> { - // First set previous shuffling data to current shuffling data. - state.previous_shuffling_epoch = state.current_shuffling_epoch; - state.previous_shuffling_start_shard = state.previous_shuffling_start_shard; - state.previous_shuffling_seed = state.previous_shuffling_seed; - - let current_epoch = state.current_epoch(spec); - let next_epoch = current_epoch + 1; - - // Check we should update, and if so, update. - if should_update_validator_registry(state, spec)? { - update_validator_registry(state, current_total_balance, spec)?; - - // If we update the registry, update the shuffling data and shards as well. - state.current_shuffling_epoch = next_epoch; - state.current_shuffling_start_shard = { - let active_validators = - state.get_cached_active_validator_indices(RelativeEpoch::Current, spec)?; - let epoch_committee_count = spec.get_epoch_committee_count(active_validators.len()); - - (state.current_shuffling_start_shard + epoch_committee_count) % spec.shard_count - }; - state.current_shuffling_seed = state.generate_seed(state.current_shuffling_epoch, spec)?; - } else { - // If processing at least on crosslink keeps failing, the reshuffle every power of two, but - // don't update the current_shuffling_start_shard. - let epochs_since_last_update = current_epoch - state.validator_registry_update_epoch; - - if epochs_since_last_update > 1 && epochs_since_last_update.is_power_of_two() { - state.current_shuffling_epoch = next_epoch; - state.current_shuffling_seed = - state.generate_seed(state.current_shuffling_epoch, spec)?; - } - } - - Ok(()) -} - -/// Returns `true` if the validator registry should be updated during an epoch processing. -/// -/// Spec v0.5.1 -pub fn should_update_validator_registry( - state: &BeaconState, - spec: &ChainSpec, -) -> Result { - if state.finalized_epoch <= state.validator_registry_update_epoch { - return Ok(false); - } - - let num_active_validators = state - .get_cached_active_validator_indices(RelativeEpoch::Current, spec)? - .len(); - let current_epoch_committee_count = spec.get_epoch_committee_count(num_active_validators); - - for shard in (0..current_epoch_committee_count) - .map(|i| (state.current_shuffling_start_shard + i as u64) % spec.shard_count) - { - if state.latest_crosslinks[shard as usize].epoch <= state.validator_registry_update_epoch { - return Ok(false); - } - } - - Ok(true) -} - -/// Update validator registry, activating/exiting validators if possible. -/// -/// Note: Utilizes the cache and will fail if the appropriate cache is not initialized. -/// -/// Spec v0.5.1 -pub fn update_validator_registry( - state: &mut BeaconState, - current_total_balance: u64, - spec: &ChainSpec, -) -> Result<(), Error> { - let current_epoch = state.current_epoch(spec); - - let max_balance_churn = std::cmp::max( - spec.max_deposit_amount, - current_total_balance / (2 * spec.max_balance_churn_quotient), - ); - - // Activate validators within the allowable balance churn. - let mut balance_churn = 0; - for index in 0..state.validator_registry.len() { - let not_activated = - state.validator_registry[index].activation_epoch == spec.far_future_epoch; - let has_enough_balance = state.validator_balances[index] >= spec.max_deposit_amount; - - if not_activated && has_enough_balance { - // Check the balance churn would be within the allowance. - balance_churn += state.get_effective_balance(index, spec)?; - if balance_churn > max_balance_churn { - break; - } - - activate_validator(state, index, false, spec); - } - } - - // Exit validators within the allowable balance churn. - let mut balance_churn = 0; - for index in 0..state.validator_registry.len() { - let not_exited = state.validator_registry[index].exit_epoch == spec.far_future_epoch; - let has_initiated_exit = state.validator_registry[index].initiated_exit; - - if not_exited && has_initiated_exit { - // Check the balance churn would be within the allowance. - balance_churn += state.get_effective_balance(index, spec)?; - if balance_churn > max_balance_churn { - break; - } - - exit_validator(state, index, spec)?; - } - } - - state.validator_registry_update_epoch = current_epoch; - - Ok(()) -} - -/// Activate the validator of the given ``index``. -/// -/// Spec v0.5.1 -pub fn activate_validator( - state: &mut BeaconState, - validator_index: usize, - is_genesis: bool, - spec: &ChainSpec, -) { - let current_epoch = state.current_epoch(spec); - - state.validator_registry[validator_index].activation_epoch = if is_genesis { - spec.genesis_epoch - } else { - state.get_delayed_activation_exit_epoch(current_epoch, spec) - } -} diff --git a/eth2/state_processing/src/per_epoch_processing/validator_statuses.rs b/eth2/state_processing/src/per_epoch_processing/validator_statuses.rs index ee0079eee..a2ce292eb 100644 --- a/eth2/state_processing/src/per_epoch_processing/validator_statuses.rs +++ b/eth2/state_processing/src/per_epoch_processing/validator_statuses.rs @@ -1,5 +1,5 @@ -use super::get_attestation_participants::get_attestation_participants; use super::WinningRootHashSet; +use crate::common::get_attesting_indices_unsorted; use types::*; /// Sets the boolean `var` on `self` to be true if it is true on `other`. Otherwise leaves `self` @@ -29,7 +29,7 @@ pub struct InclusionInfo { pub slot: Slot, /// The distance between the attestation slot and the slot that attestation was included in a /// block. - pub distance: Slot, + pub distance: u64, /// The index of the proposer at the slot where the attestation was included. pub proposer_index: usize, } @@ -39,7 +39,7 @@ impl Default for InclusionInfo { fn default() -> Self { Self { slot: Slot::max_value(), - distance: Slot::max_value(), + distance: u64::max_value(), proposer_index: 0, } } @@ -68,17 +68,19 @@ pub struct ValidatorStatus { pub is_active_in_current_epoch: bool, /// True if the validator was active in the state's _previous_ epoch. pub is_active_in_previous_epoch: bool, + /// The validator's effective balance in the _current_ epoch. + pub current_epoch_effective_balance: u64, /// True if the validator had an attestation included in the _current_ epoch. pub is_current_epoch_attester: bool, /// True if the validator's beacon block root attestation for the first slot of the _current_ /// epoch matches the block root known to the state. - pub is_current_epoch_boundary_attester: bool, + pub is_current_epoch_target_attester: bool, /// True if the validator had an attestation included in the _previous_ epoch. pub is_previous_epoch_attester: bool, /// True if the validator's beacon block root attestation for the first slot of the _previous_ /// epoch matches the block root known to the state. - pub is_previous_epoch_boundary_attester: bool, + pub is_previous_epoch_target_attester: bool, /// True if the validator's beacon block root attestation in the _previous_ epoch at the /// attestation's slot (`attestation_data.slot`) matches the block root known to the state. pub is_previous_epoch_head_attester: bool, @@ -106,9 +108,9 @@ impl ValidatorStatus { set_self_if_other_is_true!(self, other, is_active_in_current_epoch); set_self_if_other_is_true!(self, other, is_active_in_previous_epoch); set_self_if_other_is_true!(self, other, is_current_epoch_attester); - set_self_if_other_is_true!(self, other, is_current_epoch_boundary_attester); + set_self_if_other_is_true!(self, other, is_current_epoch_target_attester); set_self_if_other_is_true!(self, other, is_previous_epoch_attester); - set_self_if_other_is_true!(self, other, is_previous_epoch_boundary_attester); + set_self_if_other_is_true!(self, other, is_previous_epoch_target_attester); set_self_if_other_is_true!(self, other, is_previous_epoch_head_attester); if let Some(other_info) = other.inclusion_info { @@ -133,12 +135,12 @@ pub struct TotalBalances { pub current_epoch_attesters: u64, /// The total effective balance of all validators who attested during the _current_ epoch and /// agreed with the state about the beacon block at the first slot of the _current_ epoch. - pub current_epoch_boundary_attesters: u64, + pub current_epoch_target_attesters: u64, /// The total effective balance of all validators who attested during the _previous_ epoch. pub previous_epoch_attesters: u64, /// The total effective balance of all validators who attested during the _previous_ epoch and /// agreed with the state about the beacon block at the first slot of the _previous_ epoch. - pub previous_epoch_boundary_attesters: u64, + pub previous_epoch_target_attesters: u64, /// The total effective balance of all validators who attested during the _previous_ epoch and /// agreed with the state about the beacon block at the time of attestation. pub previous_epoch_head_attesters: u64, @@ -160,7 +162,7 @@ impl ValidatorStatuses { /// - Active validators /// - Total balances for the current and previous epochs. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn new( state: &BeaconState, spec: &ChainSpec, @@ -169,21 +171,23 @@ impl ValidatorStatuses { let mut total_balances = TotalBalances::default(); for (i, validator) in state.validator_registry.iter().enumerate() { + let effective_balance = state.get_effective_balance(i, spec)?; let mut status = ValidatorStatus { is_slashed: validator.slashed, is_withdrawable_in_current_epoch: validator - .is_withdrawable_at(state.current_epoch(spec)), + .is_withdrawable_at(state.current_epoch()), + current_epoch_effective_balance: effective_balance, ..ValidatorStatus::default() }; - if validator.is_active_at(state.current_epoch(spec)) { + if validator.is_active_at(state.current_epoch()) { status.is_active_in_current_epoch = true; - total_balances.current_epoch += state.get_effective_balance(i, spec)?; + total_balances.current_epoch += effective_balance; } - if validator.is_active_at(state.previous_epoch(spec)) { + if validator.is_active_at(state.previous_epoch()) { status.is_active_in_previous_epoch = true; - total_balances.previous_epoch += state.get_effective_balance(i, spec)?; + total_balances.previous_epoch += effective_balance; } statuses.push(status); @@ -198,7 +202,7 @@ impl ValidatorStatuses { /// Process some attestations from the given `state` updating the `statuses` and /// `total_balances` fields. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn process_attestations( &mut self, state: &BeaconState, @@ -210,44 +214,41 @@ impl ValidatorStatuses { .chain(state.current_epoch_attestations.iter()) { let attesting_indices = - get_attestation_participants(state, &a.data, &a.aggregation_bitfield, spec)?; - let attesting_balance = state.get_total_balance(&attesting_indices, spec)?; + get_attesting_indices_unsorted(state, &a.data, &a.aggregation_bitfield)?; let mut status = ValidatorStatus::default(); // Profile this attestation, updating the total balances and generating an // `ValidatorStatus` object that applies to all participants in the attestation. - if is_from_epoch(a, state.current_epoch(spec), spec) { - self.total_balances.current_epoch_attesters += attesting_balance; + if is_from_epoch(a, state.current_epoch()) { status.is_current_epoch_attester = true; - if has_common_epoch_boundary_root(a, state, state.current_epoch(spec), spec)? { - self.total_balances.current_epoch_boundary_attesters += attesting_balance; - status.is_current_epoch_boundary_attester = true; + if target_matches_epoch_start_block(a, state, state.current_epoch(), spec)? { + status.is_current_epoch_target_attester = true; } - } else if is_from_epoch(a, state.previous_epoch(spec), spec) { - self.total_balances.previous_epoch_attesters += attesting_balance; + } else if is_from_epoch(a, state.previous_epoch()) { status.is_previous_epoch_attester = true; // The inclusion slot and distance are only required for previous epoch attesters. - let relative_epoch = RelativeEpoch::from_slot(state.slot, a.inclusion_slot, spec)?; + let attestation_slot = state.get_attestation_slot(&a.data)?; + let inclusion_slot = attestation_slot + a.inclusion_delay; + let relative_epoch = + RelativeEpoch::from_slot(state.slot, inclusion_slot, spec.slots_per_epoch)?; status.inclusion_info = Some(InclusionInfo { - slot: a.inclusion_slot, - distance: inclusion_distance(a), + slot: inclusion_slot, + distance: a.inclusion_delay, proposer_index: state.get_beacon_proposer_index( - a.inclusion_slot, + attestation_slot, relative_epoch, spec, )?, }); - if has_common_epoch_boundary_root(a, state, state.previous_epoch(spec), spec)? { - self.total_balances.previous_epoch_boundary_attesters += attesting_balance; - status.is_previous_epoch_boundary_attester = true; + if target_matches_epoch_start_block(a, state, state.previous_epoch(), spec)? { + status.is_previous_epoch_target_attester = true; } if has_common_beacon_block_root(a, state)? { - self.total_balances.previous_epoch_head_attesters += attesting_balance; status.is_previous_epoch_head_attester = true; } } @@ -258,13 +259,37 @@ impl ValidatorStatuses { } } + // Compute the total balances + for (index, v) in self.statuses.iter().enumerate() { + // According to the spec, we only count unslashed validators towards the totals. + if !v.is_slashed { + let validator_balance = state.get_effective_balance(index, spec)?; + + if v.is_current_epoch_attester { + self.total_balances.current_epoch_attesters += validator_balance; + } + if v.is_current_epoch_target_attester { + self.total_balances.current_epoch_target_attesters += validator_balance; + } + if v.is_previous_epoch_attester { + self.total_balances.previous_epoch_attesters += validator_balance; + } + if v.is_previous_epoch_target_attester { + self.total_balances.previous_epoch_target_attesters += validator_balance; + } + if v.is_previous_epoch_head_attester { + self.total_balances.previous_epoch_head_attesters += validator_balance; + } + } + } + Ok(()) } /// Update the `statuses` for each validator based upon whether or not they attested to the /// "winning" shard block root for the previous epoch. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn process_winning_roots( &mut self, state: &BeaconState, @@ -272,9 +297,8 @@ impl ValidatorStatuses { spec: &ChainSpec, ) -> Result<(), BeaconStateError> { // Loop through each slot in the previous epoch. - for slot in state.previous_epoch(spec).slot_iter(spec.slots_per_epoch) { - let crosslink_committees_at_slot = - state.get_crosslink_committees_at_slot(slot, spec)?; + for slot in state.previous_epoch().slot_iter(spec.slots_per_epoch) { + let crosslink_committees_at_slot = state.get_crosslink_committees_at_slot(slot)?; // Loop through each committee in the slot. for c in crosslink_committees_at_slot { @@ -297,26 +321,18 @@ impl ValidatorStatuses { } } -/// Returns the distance between when the attestation was created and when it was included in a -/// block. -/// -/// Spec v0.5.1 -fn inclusion_distance(a: &PendingAttestation) -> Slot { - a.inclusion_slot - a.data.slot -} - /// Returns `true` if some `PendingAttestation` is from the supplied `epoch`. /// -/// Spec v0.5.1 -fn is_from_epoch(a: &PendingAttestation, epoch: Epoch, spec: &ChainSpec) -> bool { - a.data.slot.epoch(spec.slots_per_epoch) == epoch +/// Spec v0.6.1 +fn is_from_epoch(a: &PendingAttestation, epoch: Epoch) -> bool { + a.data.target_epoch == epoch } -/// Returns `true` if a `PendingAttestation` and `BeaconState` share the same beacon block hash for -/// the first slot of the given epoch. +/// Returns `true` if the attestation's FFG target is equal to the hash of the `state`'s first +/// beacon block in the given `epoch`. /// -/// Spec v0.5.1 -fn has_common_epoch_boundary_root( +/// Spec v0.6.1 +fn target_matches_epoch_start_block( a: &PendingAttestation, state: &BeaconState, epoch: Epoch, @@ -331,12 +347,13 @@ fn has_common_epoch_boundary_root( /// Returns `true` if a `PendingAttestation` and `BeaconState` share the same beacon block hash for /// the current slot of the `PendingAttestation`. /// -/// Spec v0.5.1 +/// Spec v0.6.1 fn has_common_beacon_block_root( a: &PendingAttestation, state: &BeaconState, ) -> Result { - let state_block_root = *state.get_block_root(a.data.slot)?; + let attestation_slot = state.get_attestation_slot(&a.data)?; + let state_block_root = *state.get_block_root(attestation_slot)?; Ok(a.data.beacon_block_root == state_block_root) } diff --git a/eth2/state_processing/src/per_epoch_processing/winning_root.rs b/eth2/state_processing/src/per_epoch_processing/winning_root.rs index 8ffa717e8..62917a134 100644 --- a/eth2/state_processing/src/per_epoch_processing/winning_root.rs +++ b/eth2/state_processing/src/per_epoch_processing/winning_root.rs @@ -1,11 +1,11 @@ -use super::get_attestation_participants::get_attestation_participants; -use std::collections::HashSet; -use std::iter::FromIterator; +use crate::common::get_attesting_indices_unsorted; +use std::collections::{HashMap, HashSet}; +use tree_hash::TreeHash; use types::*; #[derive(Clone)] pub struct WinningRoot { - pub crosslink_data_root: Hash256, + pub crosslink: Crosslink, pub attesting_validator_indices: Vec, pub total_attesting_balance: u64, } @@ -16,15 +16,15 @@ impl WinningRoot { /// A winning root is "better" than another if it has a higher `total_attesting_balance`. Ties /// are broken by favouring the higher `crosslink_data_root` value. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn is_better_than(&self, other: &Self) -> bool { - if self.total_attesting_balance > other.total_attesting_balance { - true - } else if self.total_attesting_balance == other.total_attesting_balance { - self.crosslink_data_root > other.crosslink_data_root - } else { - false - } + ( + self.total_attesting_balance, + self.crosslink.crosslink_data_root, + ) > ( + other.total_attesting_balance, + other.crosslink.crosslink_data_root, + ) } } @@ -34,43 +34,56 @@ impl WinningRoot { /// The `WinningRoot` object also contains additional fields that are useful in later stages of /// per-epoch processing. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn winning_root( state: &BeaconState, shard: u64, + epoch: Epoch, spec: &ChainSpec, ) -> Result, BeaconStateError> { - let mut winning_root: Option = None; + let shard_attestations: Vec<&PendingAttestation> = state + .get_matching_source_attestations(epoch)? + .iter() + .filter(|a| a.data.shard == shard) + .collect(); - let crosslink_data_roots: HashSet = HashSet::from_iter( - state - .previous_epoch_attestations - .iter() - .chain(state.current_epoch_attestations.iter()) - .filter_map(|a| { - if is_eligible_for_winning_root(state, a, shard) { - Some(a.data.crosslink_data_root) - } else { - None - } - }), - ); + let mut shard_crosslinks = Vec::with_capacity(shard_attestations.len()); + for att in shard_attestations { + shard_crosslinks.push(( + att, + state.get_crosslink_from_attestation_data(&att.data, spec)?, + )); + } - for crosslink_data_root in crosslink_data_roots { + let current_shard_crosslink_root = state.get_current_crosslink(shard)?.tree_hash_root(); + let candidate_crosslinks = shard_crosslinks.into_iter().filter(|(_, c)| { + c.previous_crosslink_root.as_bytes() == ¤t_shard_crosslink_root[..] + || c.tree_hash_root() == current_shard_crosslink_root + }); + + // Build a map from candidate crosslink to attestations that support that crosslink. + let mut candidate_crosslink_map: HashMap> = HashMap::new(); + + for (attestation, crosslink) in candidate_crosslinks { + let supporting_attestations = candidate_crosslink_map + .entry(crosslink) + .or_insert_with(Vec::new); + supporting_attestations.push(attestation); + } + + if candidate_crosslink_map.is_empty() { + return Ok(None); + } + + let mut winning_root = None; + for (crosslink, attestations) in candidate_crosslink_map { let attesting_validator_indices = - get_attesting_validator_indices(state, shard, &crosslink_data_root, spec)?; - - let total_attesting_balance: u64 = - attesting_validator_indices - .iter() - .try_fold(0_u64, |acc, i| { - state - .get_effective_balance(*i, spec) - .and_then(|bal| Ok(acc + bal)) - })?; + get_unslashed_attesting_indices_unsorted(state, &attestations)?; + let total_attesting_balance = + state.get_total_balance(&attesting_validator_indices, spec)?; let candidate = WinningRoot { - crosslink_data_root, + crosslink, attesting_validator_indices, total_attesting_balance, }; @@ -87,56 +100,27 @@ pub fn winning_root( Ok(winning_root) } -/// Returns `true` if pending attestation `a` is eligible to become a winning root. -/// -/// Spec v0.5.1 -fn is_eligible_for_winning_root( +pub fn get_unslashed_attesting_indices_unsorted( state: &BeaconState, - a: &PendingAttestation, - shard: Shard, -) -> bool { - if shard >= state.latest_crosslinks.len() as u64 { - return false; - } - - a.data.previous_crosslink == state.latest_crosslinks[shard as usize] -} - -/// Returns all indices which voted for a given crosslink. Does not contain duplicates. -/// -/// Spec v0.5.1 -fn get_attesting_validator_indices( - state: &BeaconState, - shard: u64, - crosslink_data_root: &Hash256, - spec: &ChainSpec, + attestations: &[&PendingAttestation], ) -> Result, BeaconStateError> { - let mut indices = vec![]; - - for a in state - .current_epoch_attestations - .iter() - .chain(state.previous_epoch_attestations.iter()) - { - if (a.data.shard == shard) && (a.data.crosslink_data_root == *crosslink_data_root) { - indices.append(&mut get_attestation_participants( - state, - &a.data, - &a.aggregation_bitfield, - spec, - )?); - } + let mut output = HashSet::new(); + for a in attestations { + output.extend(get_attesting_indices_unsorted( + state, + &a.data, + &a.aggregation_bitfield, + )?); } - - // Sort the list (required for dedup). "Unstable" means the sort may re-order equal elements, - // this causes no issue here. - // - // These sort + dedup ops are potentially good CPU time optimisation targets. - indices.sort_unstable(); - // Remove all duplicate indices (requires a sorted list). - indices.dedup(); - - Ok(indices) + Ok(output + .into_iter() + .filter(|index| { + state + .validator_registry + .get(*index) + .map_or(false, |v| !v.slashed) + }) + .collect()) } #[cfg(test)] @@ -146,15 +130,17 @@ mod tests { #[test] fn is_better_than() { let worse = WinningRoot { - crosslink_data_root: Hash256::from_slice(&[1; 32]), + crosslink: Crosslink { + epoch: Epoch::new(0), + previous_crosslink_root: Hash256::from_slice(&[0; 32]), + crosslink_data_root: Hash256::from_slice(&[1; 32]), + }, attesting_validator_indices: vec![], total_attesting_balance: 42, }; - let better = WinningRoot { - crosslink_data_root: Hash256::from_slice(&[2; 32]), - ..worse.clone() - }; + let mut better = worse.clone(); + better.crosslink.crosslink_data_root = Hash256::from_slice(&[2; 32]); assert!(better.is_better_than(&worse)); diff --git a/eth2/state_processing/src/per_slot_processing.rs b/eth2/state_processing/src/per_slot_processing.rs index ebab36ff7..97645ab8a 100644 --- a/eth2/state_processing/src/per_slot_processing.rs +++ b/eth2/state_processing/src/per_slot_processing.rs @@ -10,14 +10,14 @@ pub enum Error { /// Advances a state forward by one slot, performing per-epoch processing if required. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub fn per_slot_processing( state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { cache_state(state, spec)?; - if (state.slot + 1) % spec.slots_per_epoch == 0 { + if (state.slot > spec.genesis_slot) && ((state.slot + 1) % spec.slots_per_epoch == 0) { per_epoch_processing(state, spec)?; } diff --git a/eth2/types/Cargo.toml b/eth2/types/Cargo.toml index 160697edd..fa1fe6a6d 100644 --- a/eth2/types/Cargo.toml +++ b/eth2/types/Cargo.toml @@ -8,6 +8,8 @@ edition = "2018" bls = { path = "../utils/bls" } boolean-bitfield = { path = "../utils/boolean-bitfield" } cached_tree_hash = { path = "../utils/cached_tree_hash" } +compare_fields = { path = "../utils/compare_fields" } +compare_fields_derive = { path = "../utils/compare_fields_derive" } dirs = "1.0" derivative = "1.0" ethereum-types = "0.5" diff --git a/eth2/types/src/attestation.rs b/eth2/types/src/attestation.rs index d6d5d3a22..682716073 100644 --- a/eth2/types/src/attestation.rs +++ b/eth2/types/src/attestation.rs @@ -9,7 +9,7 @@ use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; /// Details an attestation that can be slashable. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, Clone, @@ -28,7 +28,7 @@ pub struct Attestation { pub data: AttestationData, pub custody_bitfield: Bitfield, #[signed_root(skip_hashing)] - pub aggregate_signature: AggregateSignature, + pub signature: AggregateSignature, } impl Attestation { @@ -49,8 +49,7 @@ impl Attestation { self.aggregation_bitfield .union_inplace(&other.aggregation_bitfield); self.custody_bitfield.union_inplace(&other.custody_bitfield); - self.aggregate_signature - .add_aggregate(&other.aggregate_signature); + self.signature.add_aggregate(&other.signature); } } diff --git a/eth2/types/src/attestation_data.rs b/eth2/types/src/attestation_data.rs index 60e539ab8..3b0f529d6 100644 --- a/eth2/types/src/attestation_data.rs +++ b/eth2/types/src/attestation_data.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{Crosslink, Epoch, Hash256, Slot}; +use crate::{Epoch, Hash256}; use serde_derive::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -9,11 +9,12 @@ use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; /// The data upon which an attestation is based. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, Clone, PartialEq, + Eq, Default, Serialize, Deserialize, @@ -27,22 +28,20 @@ use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; )] pub struct AttestationData { // LMD GHOST vote - pub slot: Slot, pub beacon_block_root: Hash256, // FFG Vote pub source_epoch: Epoch, pub source_root: Hash256, + pub target_epoch: Epoch, pub target_root: Hash256, // Crosslink Vote pub shard: u64, - pub previous_crosslink: Crosslink, + pub previous_crosslink_root: Hash256, pub crosslink_data_root: Hash256, } -impl Eq for AttestationData {} - #[cfg(test)] mod tests { use super::*; diff --git a/eth2/types/src/attestation_data_and_custody_bit.rs b/eth2/types/src/attestation_data_and_custody_bit.rs index f1437cb54..e2d949ae8 100644 --- a/eth2/types/src/attestation_data_and_custody_bit.rs +++ b/eth2/types/src/attestation_data_and_custody_bit.rs @@ -1,29 +1,31 @@ use super::AttestationData; use crate::test_utils::TestRandom; - -use rand::RngCore; -use serde_derive::Serialize; +use serde_derive::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; use tree_hash_derive::{CachedTreeHash, TreeHash}; /// Used for pairing an attestation with a proof-of-custody. /// -/// Spec v0.5.1 -#[derive(Debug, Clone, PartialEq, Default, Serialize, Encode, Decode, TreeHash, CachedTreeHash)] +/// Spec v0.6.1 +#[derive( + Debug, + Clone, + PartialEq, + Default, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + CachedTreeHash, + TestRandom, +)] pub struct AttestationDataAndCustodyBit { pub data: AttestationData, pub custody_bit: bool, } -impl TestRandom for AttestationDataAndCustodyBit { - fn random_for_test(rng: &mut impl RngCore) -> Self { - Self { - data: <_>::random_for_test(rng), - custody_bit: <_>::random_for_test(rng), - } - } -} - #[cfg(test)] mod test { use super::*; diff --git a/eth2/types/src/attester_slashing.rs b/eth2/types/src/attester_slashing.rs index d7b5d5942..2b6402fcc 100644 --- a/eth2/types/src/attester_slashing.rs +++ b/eth2/types/src/attester_slashing.rs @@ -1,4 +1,4 @@ -use crate::{test_utils::TestRandom, SlashableAttestation}; +use crate::{test_utils::TestRandom, IndexedAttestation}; use serde_derive::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -7,7 +7,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// Two conflicting attestations. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, @@ -21,8 +21,8 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; TestRandom, )] pub struct AttesterSlashing { - pub slashable_attestation_1: SlashableAttestation, - pub slashable_attestation_2: SlashableAttestation, + pub attestation_1: IndexedAttestation, + pub attestation_2: IndexedAttestation, } #[cfg(test)] diff --git a/eth2/types/src/beacon_block.rs b/eth2/types/src/beacon_block.rs index 33d73db16..2a829fda2 100644 --- a/eth2/types/src/beacon_block.rs +++ b/eth2/types/src/beacon_block.rs @@ -10,7 +10,7 @@ use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; /// A block of the `BeaconChain`. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, @@ -36,18 +36,20 @@ pub struct BeaconBlock { impl BeaconBlock { /// Returns an empty block to be used during genesis. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn empty(spec: &ChainSpec) -> BeaconBlock { BeaconBlock { slot: spec.genesis_slot, previous_block_root: spec.zero_hash, state_root: spec.zero_hash, body: BeaconBlockBody { - randao_reveal: spec.empty_signature.clone(), + randao_reveal: Signature::empty_signature(), eth1_data: Eth1Data { deposit_root: spec.zero_hash, block_hash: spec.zero_hash, + deposit_count: 0, }, + graffiti: [0; 32], proposer_slashings: vec![], attester_slashings: vec![], attestations: vec![], @@ -55,13 +57,13 @@ impl BeaconBlock { voluntary_exits: vec![], transfers: vec![], }, - signature: spec.empty_signature.clone(), + signature: Signature::empty_signature(), } } /// Returns the `tree_hash_root | update` of the block. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn canonical_root(&self) -> Hash256 { Hash256::from_slice(&self.tree_hash_root()[..]) } @@ -73,7 +75,7 @@ impl BeaconBlock { /// /// Note: performs a full tree-hash of `self.body`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn block_header(&self) -> BeaconBlockHeader { BeaconBlockHeader { slot: self.slot, @@ -86,11 +88,11 @@ impl BeaconBlock { /// Returns a "temporary" header, where the `state_root` is `spec.zero_hash`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn temporary_block_header(&self, spec: &ChainSpec) -> BeaconBlockHeader { BeaconBlockHeader { state_root: spec.zero_hash, - signature: spec.empty_signature.clone(), + signature: Signature::empty_signature(), ..self.block_header() } } diff --git a/eth2/types/src/beacon_block_body.rs b/eth2/types/src/beacon_block_body.rs index 867db78c4..e3609d889 100644 --- a/eth2/types/src/beacon_block_body.rs +++ b/eth2/types/src/beacon_block_body.rs @@ -1,4 +1,4 @@ -use crate::test_utils::TestRandom; +use crate::test_utils::{graffiti_from_hex_str, TestRandom}; use crate::*; use serde_derive::{Deserialize, Serialize}; @@ -8,7 +8,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// The body of a `BeaconChain` block, containing operations. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, @@ -24,6 +24,8 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; pub struct BeaconBlockBody { pub randao_reveal: Signature, pub eth1_data: Eth1Data, + #[serde(deserialize_with = "graffiti_from_hex_str")] + pub graffiti: [u8; 32], pub proposer_slashings: Vec, pub attester_slashings: Vec, pub attestations: Vec, diff --git a/eth2/types/src/beacon_block_header.rs b/eth2/types/src/beacon_block_header.rs index 601346db5..f58803c20 100644 --- a/eth2/types/src/beacon_block_header.rs +++ b/eth2/types/src/beacon_block_header.rs @@ -10,7 +10,7 @@ use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; /// A header of a `BeaconBlock`. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, @@ -36,14 +36,14 @@ pub struct BeaconBlockHeader { impl BeaconBlockHeader { /// Returns the `tree_hash_root` of the header. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn canonical_root(&self) -> Hash256 { Hash256::from_slice(&self.signed_root()[..]) } /// Given a `body`, consumes `self` and returns a complete `BeaconBlock`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn into_block(self, body: BeaconBlockBody) -> BeaconBlock { BeaconBlock { slot: self.slot, diff --git a/eth2/types/src/beacon_state.rs b/eth2/types/src/beacon_state.rs index d8eade8bd..4e510940f 100644 --- a/eth2/types/src/beacon_state.rs +++ b/eth2/types/src/beacon_state.rs @@ -1,12 +1,13 @@ -use self::epoch_cache::{get_active_validator_indices, EpochCache, Error as EpochCacheError}; +use self::committee_cache::{get_active_validator_indices, CommitteeCache}; +use self::exit_cache::ExitCache; use crate::test_utils::TestRandom; use crate::*; use cached_tree_hash::{Error as TreeHashCacheError, TreeHashCache}; -use hashing::hash; -use int_to_bytes::int_to_bytes32; -use pubkey_cache::PubkeyCache; - +use compare_fields_derive::CompareFields; use fixed_len_vec::{typenum::Unsigned, FixedLenVec}; +use hashing::hash; +use int_to_bytes::{int_to_bytes32, int_to_bytes8}; +use pubkey_cache::PubkeyCache; use serde_derive::{Deserialize, Serialize}; use ssz::ssz_encode; use ssz_derive::{Decode, Encode}; @@ -17,11 +18,13 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; pub use beacon_state_types::*; mod beacon_state_types; -mod epoch_cache; +mod committee_cache; +mod exit_cache; mod pubkey_cache; mod tests; -pub const CACHED_EPOCHS: usize = 4; +pub const CACHED_EPOCHS: usize = 3; +const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; #[derive(Debug, PartialEq)] pub enum Error { @@ -32,6 +35,9 @@ pub enum Error { UnableToDetermineProducer, InvalidBitfield, ValidatorIsWithdrawable, + UnableToShuffle, + TooManyValidators, + InsufficientValidators, InsufficientRandaoMixes, InsufficientBlockRoots, InsufficientIndexRoots, @@ -40,20 +46,23 @@ pub enum Error { InsufficientSlashedBalances, InsufficientStateRoots, NoCommitteeForShard, + NoCommitteeForSlot, + ZeroSlotsPerEpoch, PubkeyCacheInconsistent, PubkeyCacheIncomplete { cache_len: usize, registry_len: usize, }, - EpochCacheUninitialized(RelativeEpoch), + PreviousCommitteeCacheUninitialized, + CurrentCommitteeCacheUninitialized, RelativeEpochError(RelativeEpochError), - EpochCacheError(EpochCacheError), + CommitteeCacheUninitialized(RelativeEpoch), TreeHashCacheError(TreeHashCacheError), } /// The state of the `BeaconChain` at some slot. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, @@ -65,6 +74,7 @@ pub enum Error { Decode, TreeHash, CachedTreeHash, + CompareFields, )] pub struct BeaconState where @@ -76,18 +86,14 @@ where pub fork: Fork, // Validator registry + #[compare_fields(as_slice)] pub validator_registry: Vec, - pub validator_balances: Vec, - pub validator_registry_update_epoch: Epoch, + #[compare_fields(as_slice)] + pub balances: Vec, // Randomness and committees pub latest_randao_mixes: FixedLenVec, - pub previous_shuffling_start_shard: u64, - pub current_shuffling_start_shard: u64, - pub previous_shuffling_epoch: Epoch, - pub current_shuffling_epoch: Epoch, - pub previous_shuffling_seed: Hash256, - pub current_shuffling_seed: Hash256, + pub latest_start_shard: u64, // Finality pub previous_epoch_attestations: Vec, @@ -101,7 +107,8 @@ where pub finalized_root: Hash256, // Recent state - pub latest_crosslinks: FixedLenVec, + pub current_crosslinks: FixedLenVec, + pub previous_crosslinks: FixedLenVec, pub latest_block_roots: FixedLenVec, latest_state_roots: FixedLenVec, latest_active_index_roots: FixedLenVec, @@ -111,7 +118,7 @@ where // Ethereum 1.0 chain data pub latest_eth1_data: Eth1Data, - pub eth1_data_votes: Vec, + pub eth1_data_votes: Vec, pub deposit_index: u64, // Caching (not in the spec) @@ -120,13 +127,7 @@ where #[ssz(skip_deserializing)] #[tree_hash(skip_hashing)] #[test_random(default)] - pub cache_index_offset: usize, - #[serde(default)] - #[ssz(skip_serializing)] - #[ssz(skip_deserializing)] - #[tree_hash(skip_hashing)] - #[test_random(default)] - pub caches: [EpochCache; CACHED_EPOCHS], + pub committee_caches: [CommitteeCache; CACHED_EPOCHS], #[serde(default)] #[ssz(skip_serializing)] #[ssz(skip_deserializing)] @@ -139,6 +140,12 @@ where #[tree_hash(skip_hashing)] #[test_random(default)] pub tree_hash_cache: TreeHashCache, + #[serde(skip_serializing, skip_deserializing)] + #[ssz(skip_serializing)] + #[ssz(skip_deserializing)] + #[tree_hash(skip_hashing)] + #[test_random(default)] + pub exit_cache: ExitCache, } impl BeaconState { @@ -147,7 +154,7 @@ impl BeaconState { /// This does not fully build a genesis beacon state, it omits processing of initial validator /// deposits. To obtain a full genesis beacon state, use the `BeaconStateBuilder`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn genesis( genesis_time: u64, latest_eth1_data: Eth1Data, @@ -155,6 +162,7 @@ impl BeaconState { ) -> BeaconState { let initial_crosslink = Crosslink { epoch: spec.genesis_epoch, + previous_crosslink_root: spec.zero_hash, crosslink_data_root: spec.zero_hash, }; @@ -166,20 +174,14 @@ impl BeaconState { // Validator registry validator_registry: vec![], // Set later in the function. - validator_balances: vec![], // Set later in the function. - validator_registry_update_epoch: spec.genesis_epoch, + balances: vec![], // Set later in the function. // Randomness and committees latest_randao_mixes: FixedLenVec::from(vec![ spec.zero_hash; T::LatestRandaoMixesLength::to_usize() ]), - previous_shuffling_start_shard: spec.genesis_start_shard, - current_shuffling_start_shard: spec.genesis_start_shard, - previous_shuffling_epoch: spec.genesis_epoch, - current_shuffling_epoch: spec.genesis_epoch, - previous_shuffling_seed: spec.zero_hash, - current_shuffling_seed: spec.zero_hash, + latest_start_shard: 0, // Finality previous_epoch_attestations: vec![], @@ -193,22 +195,16 @@ impl BeaconState { finalized_root: spec.zero_hash, // Recent state - latest_crosslinks: vec![initial_crosslink; spec.shard_count as usize].into(), - latest_block_roots: FixedLenVec::from(vec![ + current_crosslinks: vec![initial_crosslink.clone(); T::ShardCount::to_usize()].into(), + previous_crosslinks: vec![initial_crosslink; T::ShardCount::to_usize()].into(), + latest_block_roots: vec![spec.zero_hash; T::SlotsPerHistoricalRoot::to_usize()].into(), + latest_state_roots: vec![spec.zero_hash; T::SlotsPerHistoricalRoot::to_usize()].into(), + latest_active_index_roots: vec![ spec.zero_hash; - T::SlotsPerHistoricalRoot::to_usize() - ]), - latest_state_roots: FixedLenVec::from(vec![ - spec.zero_hash; - T::SlotsPerHistoricalRoot::to_usize() - ]), - latest_active_index_roots: FixedLenVec::from( - vec![spec.zero_hash; T::LatestActiveIndexRootsLength::to_usize()], - ), - latest_slashed_balances: FixedLenVec::from(vec![ - 0; - T::LatestSlashedExitLength::to_usize() - ]), + T::LatestActiveIndexRootsLength::to_usize() + ] + .into(), + latest_slashed_balances: vec![0; T::LatestSlashedExitLength::to_usize()].into(), latest_block_header: BeaconBlock::empty(spec).temporary_block_header(spec), historical_roots: vec![], @@ -222,21 +218,20 @@ impl BeaconState { /* * Caching (not in spec) */ - cache_index_offset: 0, - caches: [ - EpochCache::default(), - EpochCache::default(), - EpochCache::default(), - EpochCache::default(), + committee_caches: [ + CommitteeCache::default(), + CommitteeCache::default(), + CommitteeCache::default(), ], pubkey_cache: PubkeyCache::default(), tree_hash_cache: TreeHashCache::default(), + exit_cache: ExitCache::default(), } } /// Returns the `tree_hash_root` of the state. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn canonical_root(&self) -> Hash256 { Hash256::from_slice(&self.tree_hash_root()[..]) } @@ -265,44 +260,80 @@ impl BeaconState { /// The epoch corresponding to `self.slot`. /// - /// Spec v0.5.1 - pub fn current_epoch(&self, spec: &ChainSpec) -> Epoch { - self.slot.epoch(spec.slots_per_epoch) + /// Spec v0.6.1 + pub fn current_epoch(&self) -> Epoch { + self.slot.epoch(T::slots_per_epoch()) } /// The epoch prior to `self.current_epoch()`. /// /// If the current epoch is the genesis epoch, the genesis_epoch is returned. /// - /// Spec v0.5.1 - pub fn previous_epoch(&self, spec: &ChainSpec) -> Epoch { - self.current_epoch(&spec) - 1 + /// Spec v0.6.1 + pub fn previous_epoch(&self) -> Epoch { + let current_epoch = self.current_epoch(); + if current_epoch > T::genesis_epoch() { + current_epoch - 1 + } else { + current_epoch + } } /// The epoch following `self.current_epoch()`. /// - /// Spec v0.5.1 - pub fn next_epoch(&self, spec: &ChainSpec) -> Epoch { - self.current_epoch(spec) + 1 + /// Spec v0.6.1 + pub fn next_epoch(&self) -> Epoch { + self.current_epoch() + 1 } - /// Returns the active validator indices for the given epoch, assuming there is no validator - /// registry update in the next epoch. - /// - /// This uses the cache, so it saves an iteration over the validator registry, however it can - /// not return a result for any epoch before the previous epoch. + pub fn get_epoch_committee_count(&self, relative_epoch: RelativeEpoch) -> Result { + let cache = self.cache(relative_epoch)?; + + Ok(cache.epoch_committee_count() as u64) + } + + pub fn get_epoch_start_shard(&self, relative_epoch: RelativeEpoch) -> Result { + let cache = self.cache(relative_epoch)?; + + Ok(cache.epoch_start_shard()) + } + + pub fn next_epoch_start_shard(&self) -> Result { + let cache = self.cache(RelativeEpoch::Current)?; + + Ok( + (cache.epoch_start_shard() + cache.epoch_committee_count() as u64) + & T::shard_count() as u64, + ) + } + + /// Get the slot of an attestation. /// /// Note: Utilizes the cache and will fail if the appropriate cache is not initialized. /// - /// Spec v0.5.1 + /// Spec v0.6.1 + pub fn get_attestation_slot(&self, attestation_data: &AttestationData) -> Result { + let target_relative_epoch = + RelativeEpoch::from_epoch(self.current_epoch(), attestation_data.target_epoch)?; + + let cc = + self.get_crosslink_committee_for_shard(attestation_data.shard, target_relative_epoch)?; + + Ok(cc.slot) + } + + /// Return the cached active validator indices at some epoch. + /// + /// Note: the indices are shuffled (i.e., not in ascending order). + /// + /// Returns an error if that epoch is not cached, or the cache is not initialized. pub fn get_cached_active_validator_indices( &self, relative_epoch: RelativeEpoch, - spec: &ChainSpec, ) -> Result<&[usize], Error> { - let cache = self.cache(relative_epoch, spec)?; + let cache = self.cache(relative_epoch)?; - Ok(&cache.active_validator_indices) + Ok(&cache.active_validator_indices()) } /// Returns the active validator indices for the given epoch. @@ -314,6 +345,17 @@ impl BeaconState { get_active_validator_indices(&self.validator_registry, epoch) } + /// Return the cached active validator indices at some epoch. + /// + /// Note: the indices are shuffled (i.e., not in ascending order). + /// + /// Returns an error if that epoch is not cached, or the cache is not initialized. + pub fn get_shuffling(&self, relative_epoch: RelativeEpoch) -> Result<&[usize], Error> { + let cache = self.cache(relative_epoch)?; + + Ok(cache.shuffling()) + } + /// Returns the crosslink committees for some slot. /// /// Note: Utilizes the cache and will fail if the appropriate cache is not initialized. @@ -322,78 +364,69 @@ impl BeaconState { pub fn get_crosslink_committees_at_slot( &self, slot: Slot, - spec: &ChainSpec, - ) -> Result<&Vec, Error> { - // If the slot is in the next epoch, assume there was no validator registry update. - let relative_epoch = match RelativeEpoch::from_slot(self.slot, slot, spec) { - Err(RelativeEpochError::AmbiguiousNextEpoch) => { - Ok(RelativeEpoch::NextWithoutRegistryChange) - } - e => e, - }?; + ) -> Result, Error> { + let relative_epoch = RelativeEpoch::from_slot(self.slot, slot, T::slots_per_epoch())?; + let cache = self.cache(relative_epoch)?; - let cache = self.cache(relative_epoch, spec)?; - - Ok(cache - .get_crosslink_committees_at_slot(slot, spec) - .ok_or_else(|| Error::SlotOutOfBounds)?) + cache + .get_crosslink_committees_for_slot(slot) + .ok_or_else(|| Error::NoCommitteeForSlot) } - /// Returns the crosslink committees for some shard in an epoch. + /// Returns the crosslink committees for some shard in some cached epoch. /// /// Note: Utilizes the cache and will fail if the appropriate cache is not initialized. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn get_crosslink_committee_for_shard( &self, - epoch: Epoch, - shard: Shard, - spec: &ChainSpec, - ) -> Result<&CrosslinkCommittee, Error> { - // If the slot is in the next epoch, assume there was no validator registry update. - let relative_epoch = match RelativeEpoch::from_epoch(self.current_epoch(spec), epoch) { - Err(RelativeEpochError::AmbiguiousNextEpoch) => { - Ok(RelativeEpoch::NextWithoutRegistryChange) - } - e => e, - }?; + shard: u64, + relative_epoch: RelativeEpoch, + ) -> Result { + let cache = self.cache(relative_epoch)?; - let cache = self.cache(relative_epoch, spec)?; + let committee = cache + .get_crosslink_committee_for_shard(shard) + .ok_or_else(|| Error::NoCommitteeForShard)?; - Ok(cache - .get_crosslink_committee_for_shard(shard, spec) - .ok_or_else(|| Error::NoCommitteeForShard)?) + Ok(committee) } - /// Returns the beacon proposer index for the `slot`. + /// Returns the beacon proposer index for the `slot` in the given `relative_epoch`. /// - /// If the state does not contain an index for a beacon proposer at the requested `slot`, then `None` is returned. - /// - /// Spec v0.5.1 + /// Spec v0.6.1 + // NOTE: be sure to test this bad boy. pub fn get_beacon_proposer_index( &self, slot: Slot, relative_epoch: RelativeEpoch, spec: &ChainSpec, ) -> Result { - let cache = self.cache(relative_epoch, spec)?; + let cache = self.cache(relative_epoch)?; + let epoch = relative_epoch.into_epoch(self.current_epoch()); - let committees = cache - .get_crosslink_committees_at_slot(slot, spec) + let first_committee = cache + .first_committee_at_slot(slot) .ok_or_else(|| Error::SlotOutOfBounds)?; + let seed = self.generate_seed(epoch, spec)?; - let epoch = slot.epoch(spec.slots_per_epoch); - - committees - .first() - .ok_or(Error::UnableToDetermineProducer) - .and_then(|first| { - let index = epoch - .as_usize() - .checked_rem(first.committee.len()) - .ok_or(Error::UnableToDetermineProducer)?; - Ok(first.committee[index]) - }) + let mut i = 0; + Ok(loop { + let candidate_index = first_committee[(epoch.as_usize() + i) % first_committee.len()]; + let random_byte = { + let mut preimage = seed.as_bytes().to_vec(); + preimage.append(&mut int_to_bytes8((i / 32) as u64)); + let hash = hash(&preimage); + hash[i % 32] + }; + let effective_balance = self.validator_registry[candidate_index].effective_balance; + if (effective_balance * MAX_RANDOM_BYTE) + >= (spec.max_effective_balance * random_byte as u64) + { + break candidate_index; + } + i += 1; + }) } /// Safely obtains the index for latest block roots, given some `slot`. @@ -415,6 +448,18 @@ impl BeaconState { Ok(&self.latest_block_roots[i]) } + /// Return the block root at a recent `slot`. + /// + /// Spec v0.6.0 + // FIXME(sproul): name swap with get_block_root + pub fn get_block_root_at_epoch( + &self, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result<&Hash256, BeaconStateError> { + self.get_block_root(epoch.start_slot(spec.slots_per_epoch)) + } + /// Sets the block root for some given slot. /// /// Spec v0.5.1 @@ -431,11 +476,11 @@ impl BeaconState { /// Safely obtains the index for `latest_randao_mixes` /// /// Spec v0.5.1 - fn get_randao_mix_index(&self, epoch: Epoch, spec: &ChainSpec) -> Result { - let current_epoch = self.current_epoch(spec); + fn get_randao_mix_index(&self, epoch: Epoch) -> Result { + let current_epoch = self.current_epoch(); let len = T::LatestRandaoMixesLength::to_u64(); - if (current_epoch - len < epoch) & (epoch <= current_epoch) { + if (epoch + len > current_epoch) & (epoch <= current_epoch) { Ok(epoch.as_usize() % len as usize) } else { Err(Error::EpochOutOfBounds) @@ -448,18 +493,13 @@ impl BeaconState { /// /// See `Self::get_randao_mix`. /// - /// Spec v0.5.1 - pub fn update_randao_mix( - &mut self, - epoch: Epoch, - signature: &Signature, - spec: &ChainSpec, - ) -> Result<(), Error> { + /// Spec v0.6.1 + pub fn update_randao_mix(&mut self, epoch: Epoch, signature: &Signature) -> Result<(), Error> { let i = epoch.as_usize() % T::LatestRandaoMixesLength::to_usize(); let signature_hash = Hash256::from_slice(&hash(&ssz_encode(signature))); - self.latest_randao_mixes[i] = *self.get_randao_mix(epoch, spec)? ^ signature_hash; + self.latest_randao_mixes[i] = *self.get_randao_mix(epoch)? ^ signature_hash; Ok(()) } @@ -467,36 +507,30 @@ impl BeaconState { /// Return the randao mix at a recent ``epoch``. /// /// Spec v0.5.1 - pub fn get_randao_mix(&self, epoch: Epoch, spec: &ChainSpec) -> Result<&Hash256, Error> { - let i = self.get_randao_mix_index(epoch, spec)?; + pub fn get_randao_mix(&self, epoch: Epoch) -> Result<&Hash256, Error> { + let i = self.get_randao_mix_index(epoch)?; Ok(&self.latest_randao_mixes[i]) } /// Set the randao mix at a recent ``epoch``. /// /// Spec v0.5.1 - pub fn set_randao_mix( - &mut self, - epoch: Epoch, - mix: Hash256, - spec: &ChainSpec, - ) -> Result<(), Error> { - let i = self.get_randao_mix_index(epoch, spec)?; + pub fn set_randao_mix(&mut self, epoch: Epoch, mix: Hash256) -> Result<(), Error> { + let i = self.get_randao_mix_index(epoch)?; self.latest_randao_mixes[i] = mix; Ok(()) } /// Safely obtains the index for `latest_active_index_roots`, given some `epoch`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 fn get_active_index_root_index(&self, epoch: Epoch, spec: &ChainSpec) -> Result { - let current_epoch = self.current_epoch(spec); + let current_epoch = self.current_epoch(); - if (current_epoch - self.latest_active_index_roots.len() as u64 - + spec.activation_exit_delay - < epoch) - & (epoch <= current_epoch + spec.activation_exit_delay) - { + let lookahead = spec.activation_exit_delay; + let lookback = self.latest_active_index_roots.len() as u64 - lookahead; + + if (epoch + lookback > current_epoch) && (current_epoch + lookahead >= epoch) { Ok(epoch.as_usize() % self.latest_active_index_roots.len()) } else { Err(Error::EpochOutOfBounds) @@ -505,7 +539,7 @@ impl BeaconState { /// Return the `active_index_root` at a recent `epoch`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn get_active_index_root(&self, epoch: Epoch, spec: &ChainSpec) -> Result { let i = self.get_active_index_root_index(epoch, spec)?; Ok(self.latest_active_index_roots[i]) @@ -513,7 +547,7 @@ impl BeaconState { /// Set the `active_index_root` at a recent `epoch`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn set_active_index_root( &mut self, epoch: Epoch, @@ -527,10 +561,10 @@ impl BeaconState { /// Replace `active_index_roots` with clones of `index_root`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn fill_active_index_roots_with(&mut self, index_root: Hash256) { self.latest_active_index_roots = - vec![index_root; self.latest_active_index_roots.len() as usize].into() + vec![index_root; self.latest_active_index_roots.len()].into() } /// Safely obtains the index for latest state roots, given some `slot`. @@ -563,7 +597,7 @@ impl BeaconState { /// Safely obtains the index for `latest_slashed_balances`, given some `epoch`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 fn get_slashed_balance_index(&self, epoch: Epoch) -> Result { let i = epoch.as_usize() % self.latest_slashed_balances.len(); @@ -578,7 +612,7 @@ impl BeaconState { /// Gets the total slashed balances for some epoch. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn get_slashed_balance(&self, epoch: Epoch) -> Result { let i = self.get_slashed_balance_index(epoch)?; Ok(self.latest_slashed_balances[i]) @@ -586,42 +620,99 @@ impl BeaconState { /// Sets the total slashed balances for some epoch. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn set_slashed_balance(&mut self, epoch: Epoch, balance: u64) -> Result<(), Error> { let i = self.get_slashed_balance_index(epoch)?; self.latest_slashed_balances[i] = balance; Ok(()) } + /// Get the attestations from the current or previous epoch. + /// + /// Spec v0.6.0 + pub fn get_matching_source_attestations( + &self, + epoch: Epoch, + ) -> Result<&[PendingAttestation], Error> { + if epoch == self.current_epoch() { + Ok(&self.current_epoch_attestations) + } else if epoch == self.previous_epoch() { + Ok(&self.previous_epoch_attestations) + } else { + Err(Error::EpochOutOfBounds) + } + } + + /// Get the current crosslink for a shard. + /// + /// Spec v0.6.1 + pub fn get_current_crosslink(&self, shard: u64) -> Result<&Crosslink, Error> { + self.current_crosslinks + .get(shard as usize) + .ok_or(Error::ShardOutOfBounds) + } + + /// Get the previous crosslink for a shard. + /// + /// Spec v0.6.1 + pub fn get_previous_crosslink(&self, shard: u64) -> Result<&Crosslink, Error> { + self.previous_crosslinks + .get(shard as usize) + .ok_or(Error::ShardOutOfBounds) + } + + /// Transform an attestation into the crosslink that it reinforces. + /// + /// Spec v0.6.1 + pub fn get_crosslink_from_attestation_data( + &self, + data: &AttestationData, + spec: &ChainSpec, + ) -> Result { + let current_crosslink_epoch = self.get_current_crosslink(data.shard)?.epoch; + Ok(Crosslink { + epoch: std::cmp::min( + data.target_epoch, + current_crosslink_epoch + spec.max_crosslink_epochs, + ), + previous_crosslink_root: data.previous_crosslink_root, + crosslink_data_root: data.crosslink_data_root, + }) + } + /// Generate a seed for the given `epoch`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn generate_seed(&self, epoch: Epoch, spec: &ChainSpec) -> Result { - let mut input = self - .get_randao_mix(epoch - spec.min_seed_lookahead, spec)? - .as_bytes() - .to_vec(); + // Bypass the safe getter for RANDAO so we can gracefully handle the scenario where `epoch + // == 0`. + let randao = { + let i = epoch + T::latest_randao_mixes_length() as u64 - spec.min_seed_lookahead; + self.latest_randao_mixes[i.as_usize() % self.latest_randao_mixes.len()] + }; + let active_index_root = self.get_active_index_root(epoch, spec)?; + let epoch_bytes = int_to_bytes32(epoch.as_u64()); - input.append(&mut self.get_active_index_root(epoch, spec)?.as_bytes().to_vec()); + let mut preimage = [0; 32 * 3]; + preimage[0..32].copy_from_slice(&randao[..]); + preimage[32..64].copy_from_slice(&active_index_root[..]); + preimage[64..].copy_from_slice(&epoch_bytes); - input.append(&mut int_to_bytes32(epoch.as_u64())); - - Ok(Hash256::from_slice(&hash(&input[..])[..])) + Ok(Hash256::from_slice(&hash(&preimage))) } /// Return the effective balance (also known as "balance at stake") for a validator with the given ``index``. /// - /// Spec v0.5.1 + /// Spec v0.6.0 pub fn get_effective_balance( &self, validator_index: usize, - spec: &ChainSpec, + _spec: &ChainSpec, ) -> Result { - let balance = self - .validator_balances + self.validator_registry .get(validator_index) - .ok_or_else(|| Error::UnknownValidator)?; - Ok(std::cmp::min(*balance, spec.max_deposit_amount)) + .map(|v| v.effective_balance) + .ok_or_else(|| Error::UnknownValidator) } /// Return the epoch at which an activation or exit triggered in ``epoch`` takes effect. @@ -631,37 +722,38 @@ impl BeaconState { epoch + 1 + spec.activation_exit_delay } - /// Initiate an exit for the validator of the given `index`. + /// Return the churn limit for the current epoch (number of validators who can leave per epoch). /// - /// Spec v0.5.1 - pub fn initiate_validator_exit(&mut self, validator_index: usize) { - self.validator_registry[validator_index].initiated_exit = true; + /// Uses the epoch cache, and will error if it isn't initialized. + /// + /// Spec v0.6.1 + pub fn get_churn_limit(&self, spec: &ChainSpec) -> Result { + Ok(std::cmp::max( + spec.min_per_epoch_churn_limit, + self.cache(RelativeEpoch::Current)?.active_validator_count() as u64 + / spec.churn_limit_quotient, + )) } /// Returns the `slot`, `shard` and `committee_index` for which a validator must produce an /// attestation. /// - /// Only reads the current epoch. - /// /// Note: Utilizes the cache and will fail if the appropriate cache is not initialized. /// - /// Spec v0.5.1 + /// Spec v0.6.2 pub fn get_attestation_duties( &self, validator_index: usize, - spec: &ChainSpec, - ) -> Result<&Option, Error> { - let cache = self.cache(RelativeEpoch::Current, spec)?; + relative_epoch: RelativeEpoch, + ) -> Result, Error> { + let cache = self.cache(relative_epoch)?; - Ok(cache - .attestation_duties - .get(validator_index) - .ok_or_else(|| Error::UnknownValidator)?) + Ok(cache.get_attestation_duties(validator_index)) } /// Return the combined effective balance of an array of validators. /// - /// Spec v0.5.1 + /// Spec v0.6.0 pub fn get_total_balance( &self, validator_indices: &[usize], @@ -675,42 +767,52 @@ impl BeaconState { /// Build all the caches, if they need to be built. pub fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { - self.build_epoch_cache(RelativeEpoch::Previous, spec)?; - self.build_epoch_cache(RelativeEpoch::Current, spec)?; - self.build_epoch_cache(RelativeEpoch::NextWithoutRegistryChange, spec)?; - self.build_epoch_cache(RelativeEpoch::NextWithRegistryChange, spec)?; + self.build_committee_cache(RelativeEpoch::Previous, spec)?; + self.build_committee_cache(RelativeEpoch::Current, spec)?; + self.build_committee_cache(RelativeEpoch::Next, spec)?; self.update_pubkey_cache()?; self.update_tree_hash_cache()?; + self.exit_cache + .build_from_registry(&self.validator_registry, spec); Ok(()) } + /// Drop all caches on the state. + pub fn drop_all_caches(&mut self) { + self.drop_committee_cache(RelativeEpoch::Previous); + self.drop_committee_cache(RelativeEpoch::Current); + self.drop_committee_cache(RelativeEpoch::Next); + self.drop_pubkey_cache(); + self.drop_tree_hash_cache(); + self.exit_cache = ExitCache::default(); + } + /// Build an epoch cache, unless it is has already been built. - pub fn build_epoch_cache( + pub fn build_committee_cache( &mut self, relative_epoch: RelativeEpoch, spec: &ChainSpec, ) -> Result<(), Error> { - let cache_index = self.cache_index(relative_epoch); + let i = Self::cache_index(relative_epoch); - if self.caches[cache_index].initialized_epoch == Some(self.slot.epoch(spec.slots_per_epoch)) - { + if self.committee_caches[i].is_initialized_at(self.previous_epoch()) { Ok(()) } else { - self.force_build_epoch_cache(relative_epoch, spec) + self.force_build_committee_cache(relative_epoch, spec) } } - /// Always builds an epoch cache, even if it is already initialized. - pub fn force_build_epoch_cache( + /// Always builds the previous epoch cache, even if it is already initialized. + pub fn force_build_committee_cache( &mut self, relative_epoch: RelativeEpoch, spec: &ChainSpec, ) -> Result<(), Error> { - let cache_index = self.cache_index(relative_epoch); - - self.caches[cache_index] = EpochCache::initialized(&self, relative_epoch, spec)?; + let epoch = relative_epoch.into_epoch(self.current_epoch()); + self.committee_caches[Self::cache_index(relative_epoch)] = + CommitteeCache::initialized(&self, epoch, spec)?; Ok(()) } @@ -718,51 +820,42 @@ impl BeaconState { /// /// This should be used if the `slot` of this state is advanced beyond an epoch boundary. /// - /// The `Next` cache becomes the `Current` and the `Current` cache becomes the `Previous`. The - /// `Previous` cache is abandoned. - /// - /// Care should be taken to update the `Current` epoch in case a registry update is performed - /// -- `Next` epoch is always _without_ a registry change. If you perform a registry update, - /// you should rebuild the `Current` cache so it uses the new seed. + /// Note: whilst this function will preserve already-built caches, it will not build any. pub fn advance_caches(&mut self) { - self.drop_cache(RelativeEpoch::Previous); + let next = Self::cache_index(RelativeEpoch::Previous); - self.cache_index_offset += 1; - self.cache_index_offset %= CACHED_EPOCHS; + let caches = &mut self.committee_caches[..]; + caches.rotate_left(1); + caches[next] = CommitteeCache::default(); } - /// Removes the specified cache and sets it to uninitialized. - pub fn drop_cache(&mut self, relative_epoch: RelativeEpoch) { - let previous_cache_index = self.cache_index(relative_epoch); - self.caches[previous_cache_index] = EpochCache::default(); - } - - /// Returns the index of `self.caches` for some `RelativeEpoch`. - fn cache_index(&self, relative_epoch: RelativeEpoch) -> usize { - let base_index = match relative_epoch { + fn cache_index(relative_epoch: RelativeEpoch) -> usize { + match relative_epoch { RelativeEpoch::Previous => 0, RelativeEpoch::Current => 1, - RelativeEpoch::NextWithoutRegistryChange => 2, - RelativeEpoch::NextWithRegistryChange => 3, - }; - - (base_index + self.cache_index_offset) % CACHED_EPOCHS + RelativeEpoch::Next => 2, + } } /// Returns the cache for some `RelativeEpoch`. Returns an error if the cache has not been /// initialized. - fn cache(&self, relative_epoch: RelativeEpoch, spec: &ChainSpec) -> Result<&EpochCache, Error> { - let cache = &self.caches[self.cache_index(relative_epoch)]; + fn cache(&self, relative_epoch: RelativeEpoch) -> Result<&CommitteeCache, Error> { + let cache = &self.committee_caches[Self::cache_index(relative_epoch)]; - let epoch = relative_epoch.into_epoch(self.slot.epoch(spec.slots_per_epoch)); - - if cache.initialized_epoch == Some(epoch) { + if cache.is_initialized_at(relative_epoch.into_epoch(self.current_epoch())) { Ok(cache) } else { - Err(Error::EpochCacheUninitialized(relative_epoch)) + Err(Error::CommitteeCacheUninitialized(relative_epoch)) } } + /// Drops the cache, leaving it in an uninitialized state. + fn drop_committee_cache(&mut self, relative_epoch: RelativeEpoch) { + self.committee_caches[Self::cache_index(relative_epoch)] = CommitteeCache::default(); + } + + // FIXME(sproul): drop_previous/current_committee_cache + /// Updates the pubkey cache, if required. /// /// Adds all `pubkeys` from the `validator_registry` which are not already in the cache. Will @@ -820,6 +913,11 @@ impl BeaconState { .and_then(|b| Ok(Hash256::from_slice(b))) .map_err(Into::into) } + + /// Completely drops the tree hash cache, replacing it with a new, empty cache. + pub fn drop_tree_hash_cache(&mut self) { + self.tree_hash_cache = TreeHashCache::default() + } } impl From for Error { @@ -828,12 +926,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: EpochCacheError) -> Error { - Error::EpochCacheError(e) - } -} - impl From for Error { fn from(e: TreeHashCacheError) -> Error { Error::TreeHashCacheError(e) diff --git a/eth2/types/src/beacon_state/beacon_state_types.rs b/eth2/types/src/beacon_state/beacon_state_types.rs index b6c943a36..ec6eb68bc 100644 --- a/eth2/types/src/beacon_state/beacon_state_types.rs +++ b/eth2/types/src/beacon_state/beacon_state_types.rs @@ -3,9 +3,7 @@ use fixed_len_vec::typenum::{Unsigned, U1024, U8, U8192}; use serde_derive::{Deserialize, Serialize}; use std::fmt::Debug; -pub trait EthSpec: - 'static + Default + Sync + Send + Clone + Debug + PartialEq + serde::de::DeserializeOwned -{ +pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq { type ShardCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; type SlotsPerHistoricalRoot: Unsigned + Clone + Sync + Send + Debug + PartialEq; type LatestRandaoMixesLength: Unsigned + Clone + Sync + Send + Debug + PartialEq; @@ -14,37 +12,77 @@ pub trait EthSpec: fn spec() -> ChainSpec; + /// Return the number of committees in one epoch. + /// + /// Spec v0.6.1 + fn get_epoch_committee_count(active_validator_count: usize) -> usize { + let target_committee_size = Self::spec().target_committee_size; + let shard_count = Self::shard_count(); + let slots_per_epoch = Self::slots_per_epoch() as usize; + + std::cmp::max( + 1, + std::cmp::min( + shard_count / slots_per_epoch, + active_validator_count / slots_per_epoch / target_committee_size, + ), + ) * slots_per_epoch + } + + /// Returns the minimum number of validators required for this spec. + /// + /// This is the _absolute_ minimum, the number required to make the chain operate in the most + /// basic sense. This count is not required to provide any security guarantees regarding + /// decentralization, entropy, etc. + fn minimum_validator_count() -> usize { + Self::slots_per_epoch() as usize + } + + /// Returns the `SLOTS_PER_EPOCH` constant for this specification. + /// + /// Spec v0.6.1 + fn slots_per_epoch() -> u64 { + Self::spec().slots_per_epoch + } + + /// Returns the `SLOTS_PER_EPOCH` constant for this specification. + /// + /// Spec v0.6.1 + fn genesis_epoch() -> Epoch { + Self::spec().genesis_epoch + } + /// Returns the `SHARD_COUNT` constant for this specification. /// - /// Spec v0.5.1 + /// Spec v0.6.1 fn shard_count() -> usize { Self::ShardCount::to_usize() } /// Returns the `SLOTS_PER_HISTORICAL_ROOT` constant for this specification. /// - /// Spec v0.5.1 + /// Spec v0.6.1 fn slots_per_historical_root() -> usize { Self::SlotsPerHistoricalRoot::to_usize() } /// Returns the `LATEST_RANDAO_MIXES_LENGTH` constant for this specification. /// - /// Spec v0.5.1 + /// Spec v0.6.1 fn latest_randao_mixes_length() -> usize { Self::LatestRandaoMixesLength::to_usize() } /// Returns the `LATEST_ACTIVE_INDEX_ROOTS` constant for this specification. /// - /// Spec v0.5.1 + /// Spec v0.6.1 fn latest_active_index_roots() -> usize { Self::LatestActiveIndexRootsLength::to_usize() } /// Returns the `LATEST_SLASHED_EXIT_LENGTH` constant for this specification. /// - /// Spec v0.5.1 + /// Spec v0.6.1 fn latest_slashed_exit_length() -> usize { Self::LatestSlashedExitLength::to_usize() } @@ -52,7 +90,7 @@ pub trait EthSpec: /// Ethereum Foundation specifications. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)] pub struct FoundationEthSpec; @@ -71,8 +109,6 @@ impl EthSpec for FoundationEthSpec { pub type FoundationBeaconState = BeaconState; /// Ethereum Foundation specifications, modified to be suitable for < 1000 validators. -/// -/// Spec v0.5.1 #[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)] pub struct FewValidatorsEthSpec; @@ -91,8 +127,6 @@ impl EthSpec for FewValidatorsEthSpec { pub type FewValidatorsBeaconState = BeaconState; /// Specifications suitable for a small-scale (< 1000 validators) lighthouse testnet. -/// -/// Spec v0.5.1 #[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)] pub struct LighthouseTestnetEthSpec; diff --git a/eth2/types/src/beacon_state/committee_cache.rs b/eth2/types/src/beacon_state/committee_cache.rs new file mode 100644 index 000000000..27374c339 --- /dev/null +++ b/eth2/types/src/beacon_state/committee_cache.rs @@ -0,0 +1,323 @@ +use super::BeaconState; +use crate::*; +use core::num::NonZeroUsize; +use serde_derive::{Deserialize, Serialize}; +use std::ops::Range; +use swap_or_not_shuffle::shuffle_list; + +mod tests; + +/// Computes and stores the shuffling for an epoch. Provides various getters to allow callers to +/// read the committees for the given epoch. +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] +pub struct CommitteeCache { + initialized_epoch: Option, + shuffling: Vec, + shuffling_positions: Vec>, + shuffling_start_shard: u64, + shard_count: u64, + committee_count: usize, + slots_per_epoch: u64, +} + +impl CommitteeCache { + /// Return a new, fully initialized cache. + /// + /// Spec v0.6.1 + pub fn initialized( + state: &BeaconState, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result { + let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), epoch) + .map_err(|_| Error::EpochOutOfBounds)?; + + // May cause divide-by-zero errors. + if T::slots_per_epoch() == 0 { + return Err(Error::ZeroSlotsPerEpoch); + } + + let active_validator_indices = + get_active_validator_indices(&state.validator_registry, epoch); + + if active_validator_indices.is_empty() { + return Err(Error::InsufficientValidators); + } + + let committee_count = T::get_epoch_committee_count(active_validator_indices.len()) as usize; + + let shuffling_start_shard = match relative_epoch { + RelativeEpoch::Current => state.latest_start_shard, + RelativeEpoch::Previous => { + let committees_in_previous_epoch = + T::get_epoch_committee_count(active_validator_indices.len()) as u64; + + (state.latest_start_shard + T::shard_count() as u64 - committees_in_previous_epoch) + % T::shard_count() as u64 + } + RelativeEpoch::Next => { + let current_active_validators = + get_active_validator_count(&state.validator_registry, state.current_epoch()); + let committees_in_current_epoch = + T::get_epoch_committee_count(current_active_validators) as u64; + + (state.latest_start_shard + committees_in_current_epoch) % T::shard_count() as u64 + } + }; + + let seed = state.generate_seed(epoch, spec)?; + + let shuffling = shuffle_list( + active_validator_indices, + spec.shuffle_round_count, + &seed[..], + false, + ) + .ok_or_else(|| Error::UnableToShuffle)?; + + // The use of `NonZeroUsize` reduces the maximum number of possible validators by one. + if state.validator_registry.len() > usize::max_value() - 1 { + return Err(Error::TooManyValidators); + } + + let mut shuffling_positions = vec![None; state.validator_registry.len()]; + for (i, v) in shuffling.iter().enumerate() { + shuffling_positions[*v] = NonZeroUsize::new(i + 1); + } + + Ok(CommitteeCache { + initialized_epoch: Some(epoch), + shuffling_start_shard, + shuffling, + shard_count: T::shard_count() as u64, + committee_count, + slots_per_epoch: T::slots_per_epoch(), + shuffling_positions, + }) + } + + /// Returns `true` if the cache has been initialized at the supplied `epoch`. + /// + /// An non-initialized cache does not provide any useful information. + pub fn is_initialized_at(&self, epoch: Epoch) -> bool { + Some(epoch) == self.initialized_epoch + } + + /// Returns the **shuffled** list of active validator indices for the initialized epoch. + /// + /// These indices are not in ascending order. + /// + /// Always returns `&[]` for a non-initialized epoch. + /// + /// Spec v0.6.1 + pub fn active_validator_indices(&self) -> &[usize] { + &self.shuffling + } + + /// Returns the shuffled list of active validator indices for the initialized epoch. + /// + /// Always returns `&[]` for a non-initialized epoch. + /// + /// Spec v0.6.1 + pub fn shuffling(&self) -> &[usize] { + &self.shuffling + } + + /// Return `Some(CrosslinkCommittee)` if the given shard has a committee during the given + /// `epoch`. + /// + /// Always returns `None` for a non-initialized epoch. + /// + /// Spec v0.6.1 + pub fn get_crosslink_committee_for_shard(&self, shard: Shard) -> Option { + if shard >= self.shard_count || self.initialized_epoch.is_none() { + return None; + } + + let committee_index = + (shard + self.shard_count - self.shuffling_start_shard) % self.shard_count; + let committee = self.compute_committee(committee_index as usize)?; + let slot = self.crosslink_slot_for_shard(shard)?; + + Some(CrosslinkCommittee { + shard, + committee, + slot, + }) + } + + /// Returns the `AttestationDuty` for the given `validator_index`. + /// + /// Returns `None` if the `validator_index` does not exist, does not have duties or `Self` is + /// non-initialized. + pub fn get_attestation_duties(&self, validator_index: usize) -> Option { + let i = self.shuffled_position(validator_index)?; + + (0..self.committee_count) + .into_iter() + .map(|nth_committee| (nth_committee, self.compute_committee_range(nth_committee))) + .find(|(_, range)| { + if let Some(range) = range { + (range.start <= i) && (range.end > i) + } else { + false + } + }) + .and_then(|(nth_committee, range)| { + let shard = (self.shuffling_start_shard + nth_committee as u64) % self.shard_count; + let slot = self.crosslink_slot_for_shard(shard)?; + let range = range?; + let committee_index = i - range.start; + let committee_len = range.end - range.start; + + Some(AttestationDuty { + slot, + shard, + committee_index, + committee_len, + }) + }) + } + + /// Returns the number of active validators in the initialized epoch. + /// + /// Always returns `usize::default()` for a non-initialized epoch. + /// + /// Spec v0.6.1 + pub fn active_validator_count(&self) -> usize { + self.shuffling.len() + } + + /// Returns the total number of committees in the initialized epoch. + /// + /// Always returns `usize::default()` for a non-initialized epoch. + /// + /// Spec v0.6.1 + pub fn epoch_committee_count(&self) -> usize { + self.committee_count + } + + /// Returns the shard assigned to the first committee in the initialized epoch. + /// + /// Always returns `u64::default()` for a non-initialized epoch. + pub fn epoch_start_shard(&self) -> u64 { + self.shuffling_start_shard + } + + /// Returns all crosslink committees, if any, for the given slot in the initialized epoch. + /// + /// Returns `None` if `slot` is not in the initialized epoch, or if `Self` is not initialized. + /// + /// Spec v0.6.1 + pub fn get_crosslink_committees_for_slot(&self, slot: Slot) -> Option> { + let position = self + .initialized_epoch? + .position(slot, self.slots_per_epoch)?; + let committees_per_slot = self.committee_count / self.slots_per_epoch as usize; + let position = position * committees_per_slot; + + if position >= self.committee_count { + None + } else { + let mut committees = Vec::with_capacity(committees_per_slot); + + for index in position..position + committees_per_slot { + let committee = self.compute_committee(index)?; + let shard = (self.shuffling_start_shard + index as u64) % self.shard_count; + + committees.push(CrosslinkCommittee { + committee, + shard, + slot, + }); + } + + Some(committees) + } + } + + /// Returns the first committee of the first slot of the initialized epoch. + /// + /// Always returns `None` for a non-initialized epoch. + /// + /// Spec v0.6.1 + pub fn first_committee_at_slot(&self, slot: Slot) -> Option<&[usize]> { + self.get_crosslink_committees_for_slot(slot)? + .first() + .and_then(|cc| Some(cc.committee)) + } + + /// Returns a slice of `self.shuffling` that represents the `index`'th committee in the epoch. + /// + /// Spec v0.6.1 + fn compute_committee(&self, index: usize) -> Option<&[usize]> { + Some(&self.shuffling[self.compute_committee_range(index)?]) + } + + /// Returns a range of `self.shuffling` that represents the `index`'th committee in the epoch. + /// + /// To avoid a divide-by-zero, returns `None` if `self.committee_count` is zero. + /// + /// Spec v0.6.1 + fn compute_committee_range(&self, index: usize) -> Option> { + if self.committee_count == 0 { + return None; + } + + let num_validators = self.shuffling.len(); + let count = self.committee_count; + + let start = (num_validators * index) / count; + let end = (num_validators * (index + 1)) / count; + + Some(start..end) + } + + /// Returns the `slot` that `shard` will be crosslink-ed in during the initialized epoch. + /// + /// Always returns `None` for a non-initialized epoch. + /// + /// Spec v0.6.1 + fn crosslink_slot_for_shard(&self, shard: u64) -> Option { + let offset = (shard + self.shard_count - self.shuffling_start_shard) % self.shard_count; + Some( + self.initialized_epoch?.start_slot(self.slots_per_epoch) + + offset / (self.committee_count as u64 / self.slots_per_epoch), + ) + } + + /// 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 { + self.shuffling_positions + .get(validator_index)? + .and_then(|p| Some(p.get() - 1)) + } +} + +/// Returns a list of all `validator_registry` indices where the validator is active at the given +/// `epoch`. +/// +/// Spec v0.6.1 +pub fn get_active_validator_indices(validators: &[Validator], epoch: Epoch) -> Vec { + let mut active = Vec::with_capacity(validators.len()); + + for (index, validator) in validators.iter().enumerate() { + if validator.is_active_at(epoch) { + active.push(index) + } + } + + active.shrink_to_fit(); + + active +} + +/// Returns the count of all `validator_registry` indices where the validator is active at the given +/// `epoch`. +/// +/// Spec v0.6.1 +fn get_active_validator_count(validators: &[Validator], epoch: Epoch) -> usize { + validators.iter().filter(|v| v.is_active_at(epoch)).count() +} diff --git a/eth2/types/src/beacon_state/committee_cache/tests.rs b/eth2/types/src/beacon_state/committee_cache/tests.rs new file mode 100644 index 000000000..ecaa3d457 --- /dev/null +++ b/eth2/types/src/beacon_state/committee_cache/tests.rs @@ -0,0 +1,241 @@ +#![cfg(test)] +use super::*; +use crate::{test_utils::*, *}; +use fixed_len_vec::typenum::*; +use serde_derive::{Deserialize, Serialize}; + +#[test] +fn default_values() { + let cache = CommitteeCache::default(); + + assert_eq!(cache.is_initialized_at(Epoch::new(0)), false); + assert_eq!(cache.active_validator_indices(), &[]); + assert_eq!(cache.get_crosslink_committee_for_shard(0), None); + assert_eq!(cache.get_attestation_duties(0), None); + assert_eq!(cache.active_validator_count(), 0); + assert_eq!(cache.epoch_committee_count(), 0); + assert_eq!(cache.epoch_start_shard(), 0); + assert_eq!(cache.get_crosslink_committees_for_slot(Slot::new(0)), None); + assert_eq!(cache.first_committee_at_slot(Slot::new(0)), None); +} + +fn new_state(validator_count: usize, slot: Slot) -> BeaconState { + let spec = &T::spec(); + + let mut builder = + TestingBeaconStateBuilder::from_single_keypair(validator_count, &Keypair::random(), spec); + + builder.teleport_to_slot(slot, spec); + + let (state, _keypairs) = builder.build(); + + state +} + +#[test] +fn fails_without_validators() { + let state = new_state::(0, Slot::new(0)); + let spec = &FewValidatorsEthSpec::spec(); + + assert_eq!( + CommitteeCache::initialized(&state, state.current_epoch(), &spec), + Err(BeaconStateError::InsufficientValidators) + ); +} + +#[test] +fn initializes_with_the_right_epoch() { + let state = new_state::(16, Slot::new(0)); + let spec = &FewValidatorsEthSpec::spec(); + + let cache = CommitteeCache::default(); + assert_eq!(cache.initialized_epoch, None); + + let cache = CommitteeCache::initialized(&state, state.current_epoch(), &spec).unwrap(); + assert_eq!(cache.initialized_epoch, Some(state.current_epoch())); + + let cache = CommitteeCache::initialized(&state, state.previous_epoch(), &spec).unwrap(); + assert_eq!(cache.initialized_epoch, Some(state.previous_epoch())); + + let cache = CommitteeCache::initialized(&state, state.next_epoch(), &spec).unwrap(); + assert_eq!(cache.initialized_epoch, Some(state.next_epoch())); +} + +#[test] +fn shuffles_for_the_right_epoch() { + let num_validators = FewValidatorsEthSpec::minimum_validator_count() * 2; + let epoch = Epoch::new(100_000_000); + let slot = epoch.start_slot(FewValidatorsEthSpec::slots_per_epoch()); + + let mut state = new_state::(num_validators, slot); + let spec = &FewValidatorsEthSpec::spec(); + + let distinct_hashes: Vec = (0..FewValidatorsEthSpec::latest_randao_mixes_length()) + .into_iter() + .map(|i| Hash256::from(i as u64)) + .collect(); + + state.latest_randao_mixes = FixedLenVec::from(distinct_hashes); + + let previous_seed = state.generate_seed(state.previous_epoch(), spec).unwrap(); + let current_seed = state.generate_seed(state.current_epoch(), spec).unwrap(); + let next_seed = state.generate_seed(state.next_epoch(), spec).unwrap(); + + assert!((previous_seed != current_seed) && (current_seed != next_seed)); + + let shuffling_with_seed = |seed: Hash256| { + shuffle_list( + (0..num_validators).collect(), + spec.shuffle_round_count, + &seed[..], + false, + ) + .unwrap() + }; + + let assert_shuffling_positions_accurate = |cache: &CommitteeCache| { + for (i, v) in cache.shuffling.iter().enumerate() { + assert_eq!( + cache.shuffling_positions[*v].unwrap().get() - 1, + i, + "Shuffling position inaccurate" + ); + } + }; + + let cache = CommitteeCache::initialized(&state, state.current_epoch(), spec).unwrap(); + assert_eq!(cache.shuffling, shuffling_with_seed(current_seed)); + assert_shuffling_positions_accurate(&cache); + + let cache = CommitteeCache::initialized(&state, state.previous_epoch(), spec).unwrap(); + assert_eq!(cache.shuffling, shuffling_with_seed(previous_seed)); + assert_shuffling_positions_accurate(&cache); + + let cache = CommitteeCache::initialized(&state, state.next_epoch(), spec).unwrap(); + assert_eq!(cache.shuffling, shuffling_with_seed(next_seed)); + assert_shuffling_positions_accurate(&cache); +} + +#[test] +fn can_start_on_any_shard() { + let num_validators = FewValidatorsEthSpec::minimum_validator_count() * 2; + let epoch = Epoch::new(100_000_000); + let slot = epoch.start_slot(FewValidatorsEthSpec::slots_per_epoch()); + + let mut state = new_state::(num_validators, slot); + let spec = &FewValidatorsEthSpec::spec(); + + for i in 0..FewValidatorsEthSpec::shard_count() as u64 { + state.latest_start_shard = i; + + let cache = CommitteeCache::initialized(&state, state.current_epoch(), spec).unwrap(); + assert_eq!(cache.shuffling_start_shard, i); + + let cache = CommitteeCache::initialized(&state, state.previous_epoch(), spec).unwrap(); + assert_eq!(cache.shuffling_start_shard, i); + + let cache = CommitteeCache::initialized(&state, state.next_epoch(), spec).unwrap(); + assert_eq!(cache.shuffling_start_shard, i); + } +} + +/// This spec has more shards than slots in an epoch, permitting epochs where not all shards are +/// included in the committee. +#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)] +pub struct ExcessShardsEthSpec; + +impl EthSpec for ExcessShardsEthSpec { + type ShardCount = U128; + type SlotsPerHistoricalRoot = U8192; + type LatestRandaoMixesLength = U8192; + type LatestActiveIndexRootsLength = U8192; + type LatestSlashedExitLength = U8192; + + fn spec() -> ChainSpec { + ChainSpec::few_validators() + } +} + +#[test] +fn starts_on_the_correct_shard() { + let spec = &ExcessShardsEthSpec::spec(); + + let num_validators = ExcessShardsEthSpec::shard_count(); + + let epoch = Epoch::new(100_000_000); + let slot = epoch.start_slot(ExcessShardsEthSpec::slots_per_epoch()); + + let mut state = new_state::(num_validators, slot); + + let validator_count = state.validator_registry.len(); + + let previous_epoch = state.previous_epoch(); + let current_epoch = state.current_epoch(); + let next_epoch = state.next_epoch(); + + for (i, mut v) in state.validator_registry.iter_mut().enumerate() { + let epoch = if i < validator_count / 4 { + previous_epoch + } else if i < validator_count / 2 { + current_epoch + } else { + next_epoch + }; + + v.activation_epoch = epoch; + } + + assert_eq!( + get_active_validator_count(&state.validator_registry, previous_epoch), + validator_count / 4 + ); + assert_eq!( + get_active_validator_count(&state.validator_registry, current_epoch), + validator_count / 2 + ); + assert_eq!( + get_active_validator_count(&state.validator_registry, next_epoch), + validator_count + ); + + let previous_shards = ExcessShardsEthSpec::get_epoch_committee_count( + get_active_validator_count(&state.validator_registry, previous_epoch), + ); + let current_shards = ExcessShardsEthSpec::get_epoch_committee_count( + get_active_validator_count(&state.validator_registry, current_epoch), + ); + let next_shards = ExcessShardsEthSpec::get_epoch_committee_count(get_active_validator_count( + &state.validator_registry, + next_epoch, + )); + + assert_eq!( + previous_shards as usize, + ExcessShardsEthSpec::shard_count() / 4 + ); + assert_eq!( + current_shards as usize, + ExcessShardsEthSpec::shard_count() / 2 + ); + assert_eq!(next_shards as usize, ExcessShardsEthSpec::shard_count()); + + let shard_count = ExcessShardsEthSpec::shard_count(); + for i in 0..ExcessShardsEthSpec::shard_count() { + state.latest_start_shard = i as u64; + + let cache = CommitteeCache::initialized(&state, state.current_epoch(), spec).unwrap(); + assert_eq!(cache.shuffling_start_shard as usize, i); + + let cache = CommitteeCache::initialized(&state, state.previous_epoch(), spec).unwrap(); + assert_eq!( + cache.shuffling_start_shard as usize, + (i + shard_count - previous_shards) % shard_count + ); + + let cache = CommitteeCache::initialized(&state, state.next_epoch(), spec).unwrap(); + assert_eq!( + cache.shuffling_start_shard as usize, + (i + current_shards) % shard_count + ); + } +} diff --git a/eth2/types/src/beacon_state/epoch_cache.rs b/eth2/types/src/beacon_state/epoch_cache.rs deleted file mode 100644 index c76c71684..000000000 --- a/eth2/types/src/beacon_state/epoch_cache.rs +++ /dev/null @@ -1,325 +0,0 @@ -use super::BeaconState; -use crate::*; -use honey_badger_split::SplitExt; -use serde_derive::{Deserialize, Serialize}; -use swap_or_not_shuffle::shuffle_list; - -#[derive(Debug, PartialEq)] -pub enum Error { - UnableToShuffle, - UnableToGenerateSeed, -} - -mod tests; - -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] -pub struct EpochCache { - /// `Some(epoch)` if the cache is initialized, where `epoch` is the cache it holds. - pub initialized_epoch: Option, - /// All crosslink committees for an epoch. - pub epoch_crosslink_committees: EpochCrosslinkCommittees, - /// Maps validator index to a slot, shard and committee index for attestation. - pub attestation_duties: Vec>, - /// Maps a shard to an index of `self.committees`. - pub shard_committee_indices: Vec>, - /// Indices of all active validators in the epoch - pub active_validator_indices: Vec, -} - -impl EpochCache { - /// Return a new, fully initialized cache. - pub fn initialized( - state: &BeaconState, - relative_epoch: RelativeEpoch, - spec: &ChainSpec, - ) -> Result { - let epoch = relative_epoch.into_epoch(state.slot.epoch(spec.slots_per_epoch)); - - let active_validator_indices = - get_active_validator_indices(&state.validator_registry, epoch); - - let builder = match relative_epoch { - RelativeEpoch::Previous => EpochCrosslinkCommitteesBuilder::for_previous_epoch( - state, - active_validator_indices.clone(), - spec, - ), - RelativeEpoch::Current => EpochCrosslinkCommitteesBuilder::for_current_epoch( - state, - active_validator_indices.clone(), - spec, - ), - RelativeEpoch::NextWithRegistryChange => { - EpochCrosslinkCommitteesBuilder::for_next_epoch( - state, - active_validator_indices.clone(), - true, - spec, - )? - } - RelativeEpoch::NextWithoutRegistryChange => { - EpochCrosslinkCommitteesBuilder::for_next_epoch( - state, - active_validator_indices.clone(), - false, - spec, - )? - } - }; - let epoch_crosslink_committees = builder.build(spec)?; - - // Loop through all the validators in the committees and create the following maps: - // - // 1. `attestation_duties`: maps `ValidatorIndex` to `AttestationDuty`. - // 2. `shard_committee_indices`: maps `Shard` into a `CrosslinkCommittee` in - // `EpochCrosslinkCommittees`. - let mut attestation_duties = vec![None; state.validator_registry.len()]; - let mut shard_committee_indices = vec![None; spec.shard_count as usize]; - for (i, slot_committees) in epoch_crosslink_committees - .crosslink_committees - .iter() - .enumerate() - { - let slot = epoch.start_slot(spec.slots_per_epoch) + i as u64; - - for (j, crosslink_committee) in slot_committees.iter().enumerate() { - let shard = crosslink_committee.shard; - - shard_committee_indices[shard as usize] = Some((slot, j)); - - for (k, validator_index) in crosslink_committee.committee.iter().enumerate() { - let attestation_duty = AttestationDuty { - slot, - shard, - committee_index: k, - committee_len: crosslink_committee.committee.len(), - }; - attestation_duties[*validator_index] = Some(attestation_duty) - } - } - } - - Ok(EpochCache { - initialized_epoch: Some(epoch), - epoch_crosslink_committees, - attestation_duties, - shard_committee_indices, - active_validator_indices, - }) - } - - /// Return a vec of `CrosslinkCommittee` for a given slot. - pub fn get_crosslink_committees_at_slot( - &self, - slot: Slot, - spec: &ChainSpec, - ) -> Option<&Vec> { - self.epoch_crosslink_committees - .get_crosslink_committees_at_slot(slot, spec) - } - - /// Return `Some(CrosslinkCommittee)` if the given shard has a committee during the given - /// `epoch`. - pub fn get_crosslink_committee_for_shard( - &self, - shard: Shard, - spec: &ChainSpec, - ) -> Option<&CrosslinkCommittee> { - if shard > self.shard_committee_indices.len() as u64 { - None - } else { - let (slot, committee) = self.shard_committee_indices[shard as usize]?; - let slot_committees = self.get_crosslink_committees_at_slot(slot, spec)?; - slot_committees.get(committee) - } - } -} - -/// Returns a list of all `validator_registry` indices where the validator is active at the given -/// `epoch`. -/// -/// Spec v0.5.1 -pub fn get_active_validator_indices(validators: &[Validator], epoch: Epoch) -> Vec { - let mut active = Vec::with_capacity(validators.len()); - - for (index, validator) in validators.iter().enumerate() { - if validator.is_active_at(epoch) { - active.push(index) - } - } - - active.shrink_to_fit(); - - active -} - -/// Contains all `CrosslinkCommittees` for an epoch. -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] -pub struct EpochCrosslinkCommittees { - /// The epoch the committees are present in. - epoch: Epoch, - /// Each commitee for each slot of the epoch. - pub crosslink_committees: Vec>, -} - -impl EpochCrosslinkCommittees { - /// Return a new instances where all slots have zero committees. - fn new(epoch: Epoch, spec: &ChainSpec) -> Self { - Self { - epoch, - crosslink_committees: vec![vec![]; spec.slots_per_epoch as usize], - } - } - - /// Return a vec of `CrosslinkCommittee` for a given slot. - fn get_crosslink_committees_at_slot( - &self, - slot: Slot, - spec: &ChainSpec, - ) -> Option<&Vec> { - let epoch_start_slot = self.epoch.start_slot(spec.slots_per_epoch); - let epoch_end_slot = self.epoch.end_slot(spec.slots_per_epoch); - - if (epoch_start_slot <= slot) && (slot <= epoch_end_slot) { - let index = slot - epoch_start_slot; - self.crosslink_committees.get(index.as_usize()) - } else { - None - } - } -} - -/// Builds an `EpochCrosslinkCommittees` object. -pub struct EpochCrosslinkCommitteesBuilder { - epoch: Epoch, - shuffling_start_shard: Shard, - shuffling_seed: Hash256, - active_validator_indices: Vec, - committees_per_epoch: u64, -} - -impl EpochCrosslinkCommitteesBuilder { - /// Instantiates a builder that will build for the `state`'s previous epoch. - pub fn for_previous_epoch( - state: &BeaconState, - active_validator_indices: Vec, - spec: &ChainSpec, - ) -> Self { - Self { - epoch: state.previous_epoch(spec), - shuffling_start_shard: state.previous_shuffling_start_shard, - shuffling_seed: state.previous_shuffling_seed, - committees_per_epoch: spec.get_epoch_committee_count(active_validator_indices.len()), - active_validator_indices, - } - } - - /// Instantiates a builder that will build for the `state`'s next epoch. - pub fn for_current_epoch( - state: &BeaconState, - active_validator_indices: Vec, - spec: &ChainSpec, - ) -> Self { - Self { - epoch: state.current_epoch(spec), - shuffling_start_shard: state.current_shuffling_start_shard, - shuffling_seed: state.current_shuffling_seed, - committees_per_epoch: spec.get_epoch_committee_count(active_validator_indices.len()), - active_validator_indices, - } - } - - /// Instantiates a builder that will build for the `state`'s next epoch. - /// - /// Note: there are two possible epoch builds for the next epoch, one where there is a registry - /// change and one where there is not. - pub fn for_next_epoch( - state: &BeaconState, - active_validator_indices: Vec, - registry_change: bool, - spec: &ChainSpec, - ) -> Result { - let current_epoch = state.current_epoch(spec); - let next_epoch = state.next_epoch(spec); - let committees_per_epoch = spec.get_epoch_committee_count(active_validator_indices.len()); - - let epochs_since_last_registry_update = - current_epoch - state.validator_registry_update_epoch; - - let (seed, shuffling_start_shard) = if registry_change { - let next_seed = state - .generate_seed(next_epoch, spec) - .map_err(|_| Error::UnableToGenerateSeed)?; - ( - next_seed, - (state.current_shuffling_start_shard + committees_per_epoch) % spec.shard_count, - ) - } else if (epochs_since_last_registry_update > 1) - & epochs_since_last_registry_update.is_power_of_two() - { - let next_seed = state - .generate_seed(next_epoch, spec) - .map_err(|_| Error::UnableToGenerateSeed)?; - (next_seed, state.current_shuffling_start_shard) - } else { - ( - state.current_shuffling_seed, - state.current_shuffling_start_shard, - ) - }; - - Ok(Self { - epoch: state.next_epoch(spec), - shuffling_start_shard, - shuffling_seed: seed, - active_validator_indices, - committees_per_epoch, - }) - } - - /// Consumes the builder, returning a fully-build `EpochCrosslinkCommittee`. - pub fn build(self, spec: &ChainSpec) -> Result { - // The shuffler fails on a empty list, so if there are no active validator indices, simply - // return an empty list. - let shuffled_active_validator_indices = if self.active_validator_indices.is_empty() { - vec![] - } else { - shuffle_list( - self.active_validator_indices, - spec.shuffle_round_count, - &self.shuffling_seed[..], - false, - ) - .ok_or_else(|| Error::UnableToShuffle)? - }; - - let mut committees: Vec> = shuffled_active_validator_indices - .honey_badger_split(self.committees_per_epoch as usize) - .map(|slice: &[usize]| slice.to_vec()) - .collect(); - - let mut epoch_crosslink_committees = EpochCrosslinkCommittees::new(self.epoch, spec); - let mut shard = self.shuffling_start_shard; - - let committees_per_slot = (self.committees_per_epoch / spec.slots_per_epoch) as usize; - - for (i, slot) in self.epoch.slot_iter(spec.slots_per_epoch).enumerate() { - for j in (0..committees.len()) - .skip(i * committees_per_slot) - .take(committees_per_slot) - { - let crosslink_committee = CrosslinkCommittee { - slot, - shard, - committee: committees[j].drain(..).collect(), - }; - epoch_crosslink_committees.crosslink_committees[i].push(crosslink_committee); - - shard += 1; - shard %= spec.shard_count; - } - } - - Ok(epoch_crosslink_committees) - } -} diff --git a/eth2/types/src/beacon_state/epoch_cache/tests.rs b/eth2/types/src/beacon_state/epoch_cache/tests.rs deleted file mode 100644 index 2a3034192..000000000 --- a/eth2/types/src/beacon_state/epoch_cache/tests.rs +++ /dev/null @@ -1,159 +0,0 @@ -#![cfg(test)] - -use super::*; -use crate::beacon_state::FewValidatorsEthSpec; -use crate::test_utils::*; -use swap_or_not_shuffle::shuffle_list; - -fn do_sane_cache_test( - state: BeaconState, - epoch: Epoch, - relative_epoch: RelativeEpoch, - validator_count: usize, - expected_seed: Hash256, - expected_shuffling_start: u64, - spec: &ChainSpec, -) { - let active_indices: Vec = (0..validator_count).collect(); - - assert_eq!( - &active_indices[..], - state - .get_cached_active_validator_indices(relative_epoch, &spec) - .unwrap(), - "Validator indices mismatch" - ); - - let shuffling = shuffle_list( - active_indices, - spec.shuffle_round_count, - &expected_seed[..], - false, - ) - .unwrap(); - - let committees_per_epoch = spec.get_epoch_committee_count(shuffling.len()); - let committees_per_slot = committees_per_epoch / spec.slots_per_epoch; - - let mut expected_indices_iter = shuffling.iter(); - let mut shard_counter = expected_shuffling_start; - - for (i, slot) in epoch.slot_iter(spec.slots_per_epoch).enumerate() { - let crosslink_committees_at_slot = - state.get_crosslink_committees_at_slot(slot, &spec).unwrap(); - - assert_eq!( - crosslink_committees_at_slot.len(), - committees_per_slot as usize, - "Bad committees per slot ({})", - i - ); - - for c in crosslink_committees_at_slot { - assert_eq!(c.shard, shard_counter, "Bad shard"); - shard_counter += 1; - shard_counter %= spec.shard_count; - - for &i in &c.committee { - assert_eq!( - i, - *expected_indices_iter.next().unwrap(), - "Non-sequential validators." - ); - } - } - } -} - -fn setup_sane_cache_test(validator_count: usize, spec: &ChainSpec) -> BeaconState { - let mut builder = - TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(validator_count, spec); - - let epoch = spec.genesis_epoch + 4; - let slot = epoch.start_slot(spec.slots_per_epoch); - builder.teleport_to_slot(slot, spec); - - let (mut state, _keypairs) = builder.build(); - - state.current_shuffling_start_shard = 0; - state.current_shuffling_seed = Hash256::from_slice(&[1; 32]); - - state.previous_shuffling_start_shard = spec.shard_count - 1; - state.previous_shuffling_seed = Hash256::from_slice(&[2; 32]); - - state - .build_epoch_cache(RelativeEpoch::Previous, spec) - .unwrap(); - state - .build_epoch_cache(RelativeEpoch::Current, spec) - .unwrap(); - state - .build_epoch_cache(RelativeEpoch::NextWithRegistryChange, spec) - .unwrap(); - state - .build_epoch_cache(RelativeEpoch::NextWithoutRegistryChange, spec) - .unwrap(); - - state -} - -#[test] -fn builds_sane_current_epoch_cache() { - let mut spec = FewValidatorsEthSpec::spec(); - spec.shard_count = 4; - let validator_count = (spec.shard_count * spec.target_committee_size) + 1; - - let state: BeaconState = - setup_sane_cache_test(validator_count as usize, &spec); - - do_sane_cache_test( - state.clone(), - state.current_epoch(&spec), - RelativeEpoch::Current, - validator_count as usize, - state.current_shuffling_seed, - state.current_shuffling_start_shard, - &spec, - ); -} - -#[test] -fn builds_sane_previous_epoch_cache() { - let mut spec = FewValidatorsEthSpec::spec(); - spec.shard_count = 2; - let validator_count = (spec.shard_count * spec.target_committee_size) + 1; - - let state: BeaconState = - setup_sane_cache_test(validator_count as usize, &spec); - - do_sane_cache_test( - state.clone(), - state.previous_epoch(&spec), - RelativeEpoch::Previous, - validator_count as usize, - state.previous_shuffling_seed, - state.previous_shuffling_start_shard, - &spec, - ); -} - -#[test] -fn builds_sane_next_without_update_epoch_cache() { - let mut spec = FewValidatorsEthSpec::spec(); - spec.shard_count = 2; - let validator_count = (spec.shard_count * spec.target_committee_size) + 1; - - let mut state: BeaconState = - setup_sane_cache_test(validator_count as usize, &spec); - - state.validator_registry_update_epoch = state.slot.epoch(spec.slots_per_epoch); - do_sane_cache_test( - state.clone(), - state.next_epoch(&spec), - RelativeEpoch::NextWithoutRegistryChange, - validator_count as usize, - state.current_shuffling_seed, - state.current_shuffling_start_shard, - &spec, - ); -} diff --git a/eth2/types/src/beacon_state/exit_cache.rs b/eth2/types/src/beacon_state/exit_cache.rs new file mode 100644 index 000000000..c129d70a2 --- /dev/null +++ b/eth2/types/src/beacon_state/exit_cache.rs @@ -0,0 +1,35 @@ +use super::{ChainSpec, Epoch, Validator}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Map from exit epoch to the number of validators known to be exiting/exited at that epoch. +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct ExitCache(HashMap); + +impl ExitCache { + /// Add all validators with a non-trivial exit epoch to the cache. + pub fn build_from_registry(&mut self, validator_registry: &[Validator], spec: &ChainSpec) { + validator_registry + .iter() + .filter(|validator| validator.exit_epoch != spec.far_future_epoch) + .for_each(|validator| self.record_validator_exit(validator.exit_epoch)); + } + + /// Record the exit of a single validator in the cache. + /// + /// Must only be called once per exiting validator. + pub fn record_validator_exit(&mut self, exit_epoch: Epoch) { + *self.0.entry(exit_epoch).or_insert(0) += 1; + } + + /// Get the greatest epoch for which validator exits are known. + pub fn max_epoch(&self) -> Option { + // This could probably be made even faster by caching the maximum. + self.0.keys().max().cloned() + } + + /// Get the number of validators exiting/exited at a given epoch, or zero if not known. + pub fn get_churn_at(&self, epoch: Epoch) -> u64 { + self.0.get(&epoch).cloned().unwrap_or(0) + } +} diff --git a/eth2/types/src/beacon_state/tests.rs b/eth2/types/src/beacon_state/tests.rs index aa3c0b98a..588d24aa8 100644 --- a/eth2/types/src/beacon_state/tests.rs +++ b/eth2/types/src/beacon_state/tests.rs @@ -1,11 +1,127 @@ #![cfg(test)] use super::*; -use crate::beacon_state::FewValidatorsEthSpec; use crate::test_utils::*; +use std::ops::RangeInclusive; ssz_tests!(FoundationBeaconState); cached_tree_hash_tests!(FoundationBeaconState); +fn test_beacon_proposer_index() { + let spec = T::spec(); + let relative_epoch = RelativeEpoch::Current; + + // Build a state for testing. + let build_state = |validator_count: usize| -> BeaconState { + let builder: TestingBeaconStateBuilder = + TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(validator_count, &spec); + let (mut state, _keypairs) = builder.build(); + state.build_committee_cache(relative_epoch, &spec).unwrap(); + + state + }; + + // Run a test on the state. + let test = |state: &BeaconState, slot: Slot, shuffling_index: usize| { + let shuffling = state.get_shuffling(relative_epoch).unwrap(); + assert_eq!( + state.get_beacon_proposer_index(slot, relative_epoch, &spec), + Ok(shuffling[shuffling_index]) + ); + }; + + // Test where we have one validator per slot + let state = build_state(T::slots_per_epoch() as usize); + for i in 0..T::slots_per_epoch() { + test(&state, Slot::from(i), i as usize); + } + + // Test where we have two validators per slot + let state = build_state(T::slots_per_epoch() as usize * 2); + for i in 0..T::slots_per_epoch() { + test(&state, Slot::from(i), i as usize * 2); + } + + // Test with two validators per slot, first validator has zero balance. + let mut state = build_state(T::slots_per_epoch() as usize * 2); + let shuffling = state.get_shuffling(relative_epoch).unwrap().to_vec(); + state.validator_registry[shuffling[0]].effective_balance = 0; + test(&state, Slot::new(0), 1); + for i in 1..T::slots_per_epoch() { + test(&state, Slot::from(i), i as usize * 2); + } +} + +#[test] +fn beacon_proposer_index() { + test_beacon_proposer_index::(); +} + +/// Should produce (note the set notation brackets): +/// +/// (current_epoch - LATEST_ACTIVE_INDEX_ROOTS_LENGTH + ACTIVATION_EXIT_DELAY, current_epoch + +/// ACTIVATION_EXIT_DELAY] +fn active_index_range(current_epoch: Epoch) -> RangeInclusive { + let delay = T::spec().activation_exit_delay; + + let start: i32 = + current_epoch.as_u64() as i32 - T::latest_active_index_roots() as i32 + delay as i32; + let end = current_epoch + delay; + + let start: Epoch = if start < 0 { + Epoch::new(0) + } else { + Epoch::from(start as u64 + 1) + }; + + start..=end +} + +/// Test getting an active index root at the start and end of the valid range, and one either side +/// of that range. +fn test_active_index(state_slot: Slot) { + let spec = T::spec(); + let builder: TestingBeaconStateBuilder = + TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(16, &spec); + let (mut state, _keypairs) = builder.build(); + state.slot = state_slot; + + let range = active_index_range::(state.current_epoch()); + + let modulo = |epoch: Epoch| epoch.as_usize() % T::latest_active_index_roots(); + + // Test the start and end of the range. + assert_eq!( + state.get_active_index_root_index(*range.start(), &spec), + Ok(modulo(*range.start())) + ); + assert_eq!( + state.get_active_index_root_index(*range.end(), &spec), + Ok(modulo(*range.end())) + ); + + // One either side of the range. + if state.current_epoch() > 0 { + // Test is invalid on epoch zero, cannot subtract from zero. + assert_eq!( + state.get_active_index_root_index(*range.start() - 1, &spec), + Err(Error::EpochOutOfBounds) + ); + } + assert_eq!( + state.get_active_index_root_index(*range.end() + 1, &spec), + Err(Error::EpochOutOfBounds) + ); +} + +#[test] +fn get_active_index_root_index() { + test_active_index::(Slot::new(0)); + + let epoch = Epoch::from(FoundationEthSpec::latest_active_index_roots() * 4); + let slot = epoch.start_slot(FoundationEthSpec::slots_per_epoch()); + test_active_index::(slot); +} + /// Test that /// /// 1. Using the cache before it's built fails. @@ -22,12 +138,14 @@ fn test_cache_initialization<'a, T: EthSpec>( // Assuming the cache isn't already built, assert that a call to a cache-using function fails. assert_eq!( - state.get_beacon_proposer_index(slot, relative_epoch, spec), - Err(BeaconStateError::EpochCacheUninitialized(relative_epoch)) + state.get_attestation_duties(0, relative_epoch), + Err(BeaconStateError::CommitteeCacheUninitialized( + relative_epoch + )) ); // Build the cache. - state.build_epoch_cache(relative_epoch, spec).unwrap(); + state.build_committee_cache(relative_epoch, spec).unwrap(); // Assert a call to a cache-using function passes. let _ = state @@ -35,12 +153,14 @@ fn test_cache_initialization<'a, T: EthSpec>( .unwrap(); // Drop the cache. - state.drop_cache(relative_epoch); + state.drop_committee_cache(relative_epoch); // Assert a call to a cache-using function fail. assert_eq!( state.get_beacon_proposer_index(slot, relative_epoch, spec), - Err(BeaconStateError::EpochCacheUninitialized(relative_epoch)) + Err(BeaconStateError::CommitteeCacheUninitialized( + relative_epoch + )) ); } @@ -56,8 +176,7 @@ fn cache_initialization() { test_cache_initialization(&mut state, RelativeEpoch::Previous, &spec); test_cache_initialization(&mut state, RelativeEpoch::Current, &spec); - test_cache_initialization(&mut state, RelativeEpoch::NextWithRegistryChange, &spec); - test_cache_initialization(&mut state, RelativeEpoch::NextWithoutRegistryChange, &spec); + test_cache_initialization(&mut state, RelativeEpoch::Next, &spec); } #[test] @@ -78,3 +197,165 @@ fn tree_hash_cache() { 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::FewValidatorsEthSpec; + use swap_or_not_shuffle::shuffle_list; + + fn execute_committee_consistency_test( + state: BeaconState, + epoch: Epoch, + validator_count: usize, + spec: &ChainSpec, + ) { + let active_indices: Vec = (0..validator_count).collect(); + let seed = state.generate_seed(epoch, spec).unwrap(); + let start_shard = 0; + let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), epoch).unwrap(); + + let mut ordered_indices = state + .get_cached_active_validator_indices(relative_epoch) + .unwrap() + .to_vec(); + ordered_indices.sort_unstable(); + assert_eq!( + active_indices, ordered_indices, + "Validator indices mismatch" + ); + + let shuffling = + shuffle_list(active_indices, spec.shuffle_round_count, &seed[..], false).unwrap(); + + let mut expected_indices_iter = shuffling.iter(); + let mut expected_shards_iter = + (start_shard..start_shard + T::shard_count() as u64).into_iter(); + + // Loop through all slots in the epoch being tested. + for slot in epoch.slot_iter(spec.slots_per_epoch) { + let crosslink_committees = state.get_crosslink_committees_at_slot(slot).unwrap(); + + // Assert that the number of committees in this slot is consistent with the reported number + // of committees in an epoch. + assert_eq!( + crosslink_committees.len() as u64, + state.get_epoch_committee_count(relative_epoch).unwrap() / T::slots_per_epoch() + ); + + for cc in crosslink_committees { + // Assert that shards are assigned contiguously across committees. + assert_eq!(expected_shards_iter.next().unwrap(), cc.shard); + // Assert that a committee lookup via slot is identical to a committee lookup via + // shard. + assert_eq!( + state + .get_crosslink_committee_for_shard(cc.shard, relative_epoch) + .unwrap(), + cc + ); + + // Loop through each validator in the committee. + for (committee_i, validator_i) in cc.committee.iter().enumerate() { + // Assert the validators are assigned contiguously across committees. + assert_eq!( + *validator_i, + *expected_indices_iter.next().unwrap(), + "Non-sequential validators." + ); + // Assert a call to `get_attestation_duties` is consistent with a call to + // `get_crosslink_committees_at_slot` + let attestation_duty = state + .get_attestation_duties(*validator_i, relative_epoch) + .unwrap() + .unwrap(); + assert_eq!(attestation_duty.slot, slot); + assert_eq!(attestation_duty.shard, cc.shard); + assert_eq!(attestation_duty.committee_index, committee_i); + assert_eq!(attestation_duty.committee_len, cc.committee.len()); + } + } + } + + // Assert that all validators were assigned to a committee. + assert!(expected_indices_iter.next().is_none()); + + // Assert that all shards were assigned to a committee. + assert!(expected_shards_iter.next().is_none()); + } + + fn committee_consistency_test( + validator_count: usize, + state_epoch: Epoch, + cache_epoch: RelativeEpoch, + ) { + let spec = &T::spec(); + + let mut builder = TestingBeaconStateBuilder::from_single_keypair( + validator_count, + &Keypair::random(), + spec, + ); + + let slot = state_epoch.start_slot(spec.slots_per_epoch); + builder.teleport_to_slot(slot, spec); + + let (mut state, _keypairs): (BeaconState, _) = builder.build(); + + let distinct_hashes: Vec = (0..T::latest_randao_mixes_length()) + .into_iter() + .map(|i| Hash256::from(i as u64)) + .collect(); + state.latest_randao_mixes = FixedLenVec::from(distinct_hashes); + + state + .build_committee_cache(RelativeEpoch::Previous, spec) + .unwrap(); + state + .build_committee_cache(RelativeEpoch::Current, spec) + .unwrap(); + state + .build_committee_cache(RelativeEpoch::Next, spec) + .unwrap(); + + let cache_epoch = cache_epoch.into_epoch(state_epoch); + + execute_committee_consistency_test(state, cache_epoch, validator_count as usize, &spec); + } + + fn committee_consistency_test_suite(cached_epoch: RelativeEpoch) { + let spec = T::spec(); + + let validator_count = (T::shard_count() * spec.target_committee_size) + 1; + + committee_consistency_test::(validator_count as usize, Epoch::new(0), cached_epoch); + + committee_consistency_test::( + validator_count as usize, + spec.genesis_epoch + 4, + cached_epoch, + ); + + committee_consistency_test::( + validator_count as usize, + spec.genesis_epoch + T::slots_per_historical_root() as u64 * T::slots_per_epoch() * 4, + cached_epoch, + ); + } + + #[test] + fn current_epoch_committee_consistency() { + committee_consistency_test_suite::(RelativeEpoch::Current); + } + + #[test] + fn previous_epoch_committee_consistency() { + committee_consistency_test_suite::(RelativeEpoch::Previous); + } + + #[test] + fn next_epoch_committee_consistency() { + committee_consistency_test_suite::(RelativeEpoch::Next); + } +} diff --git a/eth2/types/src/chain_spec.rs b/eth2/types/src/chain_spec.rs index 974bcfc4a..20aa6fcdb 100644 --- a/eth2/types/src/chain_spec.rs +++ b/eth2/types/src/chain_spec.rs @@ -1,63 +1,56 @@ use crate::*; -use bls::Signature; use int_to_bytes::int_to_bytes4; use serde_derive::Deserialize; use test_utils::u8_from_hex_str; -const GWEI: u64 = 1_000_000_000; - /// Each of the BLS signature domains. /// -/// Spec v0.5.1 +/// Spec v0.6.1 pub enum Domain { - BeaconBlock, + BeaconProposer, Randao, Attestation, Deposit, - Exit, + VoluntaryExit, Transfer, } /// Holds all the "constants" for a BeaconChain. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive(PartialEq, Debug, Clone, Deserialize)] #[serde(default)] pub struct ChainSpec { /* * Misc */ - pub shard_count: u64, - pub target_committee_size: u64, - pub max_balance_churn_quotient: u64, - pub max_indices_per_slashable_vote: usize, - pub max_exit_dequeues_per_epoch: u64, + pub target_committee_size: usize, + pub max_indices_per_attestation: u64, + pub min_per_epoch_churn_limit: u64, + pub churn_limit_quotient: u64, + pub base_rewards_per_epoch: u64, pub shuffle_round_count: u8, /* * Deposit contract */ - pub deposit_contract_address: Address, pub deposit_contract_tree_depth: u64, /* * Gwei values */ pub min_deposit_amount: u64, - pub max_deposit_amount: u64, - pub fork_choice_balance_increment: u64, + pub max_effective_balance: u64, pub ejection_balance: u64, + pub effective_balance_increment: u64, /* * Initial Values */ - pub genesis_fork_version: u32, pub genesis_slot: Slot, pub genesis_epoch: Epoch, - pub genesis_start_shard: u64, pub far_future_epoch: Epoch, pub zero_hash: Hash256, - pub empty_signature: Signature, #[serde(deserialize_with = "u8_from_hex_str")] pub bls_withdrawal_prefix_byte: u8, @@ -69,18 +62,21 @@ pub struct ChainSpec { pub slots_per_epoch: u64, pub min_seed_lookahead: Epoch, pub activation_exit_delay: u64, - pub epochs_per_eth1_voting_period: u64, + pub slots_per_eth1_voting_period: u64, + pub slots_per_historical_root: usize, pub min_validator_withdrawability_delay: Epoch, pub persistent_committee_period: u64, + pub max_crosslink_epochs: u64, + pub min_epochs_to_inactivity_penalty: u64, /* * Reward and penalty quotients */ pub base_reward_quotient: u64, - pub whistleblower_reward_quotient: u64, - pub attestation_inclusion_reward_quotient: u64, + pub whistleblowing_reward_quotient: u64, + pub proposer_reward_quotient: u64, pub inactivity_penalty_quotient: u64, - pub min_penalty_quotient: u64, + pub min_slashing_penalty_quotient: u64, /* * Max operations per block @@ -100,11 +96,11 @@ pub struct ChainSpec { * * Use `ChainSpec::get_domain(..)` to access these values. */ - domain_beacon_block: u32, + domain_beacon_proposer: u32, domain_randao: u32, domain_attestation: u32, domain_deposit: u32, - domain_exit: u32, + domain_voluntary_exit: u32, domain_transfer: u32, /* @@ -116,29 +112,16 @@ pub struct ChainSpec { } impl ChainSpec { - /// Return the number of committees in one epoch. - /// - /// Spec v0.5.1 - pub fn get_epoch_committee_count(&self, active_validator_count: usize) -> u64 { - std::cmp::max( - 1, - std::cmp::min( - self.shard_count / self.slots_per_epoch, - active_validator_count as u64 / self.slots_per_epoch / self.target_committee_size, - ), - ) * self.slots_per_epoch - } - /// Get the domain number that represents the fork meta and signature domain. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn get_domain(&self, epoch: Epoch, domain: Domain, fork: &Fork) -> u64 { let domain_constant = match domain { - Domain::BeaconBlock => self.domain_beacon_block, + Domain::BeaconProposer => self.domain_beacon_proposer, Domain::Randao => self.domain_randao, Domain::Attestation => self.domain_attestation, Domain::Deposit => self.domain_deposit, - Domain::Exit => self.domain_exit, + Domain::VoluntaryExit => self.domain_voluntary_exit, Domain::Transfer => self.domain_transfer, }; @@ -153,47 +136,39 @@ impl ChainSpec { /// Returns a `ChainSpec` compatible with the Ethereum Foundation specification. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub(crate) fn foundation() -> Self { - let genesis_slot = Slot::new(2_u64.pow(32)); - let slots_per_epoch = 64; - let genesis_epoch = genesis_slot.epoch(slots_per_epoch); - Self { /* * Misc */ - shard_count: 1_024, target_committee_size: 128, - max_balance_churn_quotient: 32, - max_indices_per_slashable_vote: 4_096, - max_exit_dequeues_per_epoch: 4, + max_indices_per_attestation: 4096, + min_per_epoch_churn_limit: 4, + churn_limit_quotient: 65_536, + base_rewards_per_epoch: 5, shuffle_round_count: 90, /* * Deposit contract */ - deposit_contract_address: Address::zero(), deposit_contract_tree_depth: 32, /* * Gwei values */ - min_deposit_amount: u64::pow(2, 0) * GWEI, - max_deposit_amount: u64::pow(2, 5) * GWEI, - fork_choice_balance_increment: u64::pow(2, 0) * GWEI, - ejection_balance: u64::pow(2, 4) * GWEI, + min_deposit_amount: u64::pow(2, 0) * u64::pow(10, 9), + max_effective_balance: u64::pow(2, 5) * u64::pow(10, 9), + ejection_balance: u64::pow(2, 4) * u64::pow(10, 9), + effective_balance_increment: u64::pow(2, 0) * u64::pow(10, 9), /* * Initial Values */ - genesis_fork_version: 0, - genesis_slot, - genesis_epoch, - genesis_start_shard: 0, + genesis_slot: Slot::new(0), + genesis_epoch: Epoch::new(0), far_future_epoch: Epoch::new(u64::max_value()), zero_hash: Hash256::zero(), - empty_signature: Signature::empty_signature(), bls_withdrawal_prefix_byte: 0, /* @@ -201,21 +176,24 @@ impl ChainSpec { */ seconds_per_slot: 6, min_attestation_inclusion_delay: 4, - slots_per_epoch, + slots_per_epoch: 64, min_seed_lookahead: Epoch::new(1), activation_exit_delay: 4, - epochs_per_eth1_voting_period: 16, + slots_per_eth1_voting_period: 1_024, + slots_per_historical_root: 8_192, min_validator_withdrawability_delay: Epoch::new(256), persistent_committee_period: 2_048, + max_crosslink_epochs: 64, + min_epochs_to_inactivity_penalty: 4, /* * Reward and penalty quotients */ base_reward_quotient: 32, - whistleblower_reward_quotient: 512, - attestation_inclusion_reward_quotient: 8, - inactivity_penalty_quotient: 16_777_216, - min_penalty_quotient: 32, + whistleblowing_reward_quotient: 512, + proposer_reward_quotient: 8, + inactivity_penalty_quotient: 33_554_432, + min_slashing_penalty_quotient: 32, /* * Max operations per block @@ -225,16 +203,16 @@ impl ChainSpec { max_attestations: 128, max_deposits: 16, max_voluntary_exits: 16, - max_transfers: 16, + max_transfers: 0, /* * Signature domains */ - domain_beacon_block: 0, + domain_beacon_proposer: 0, domain_randao: 1, domain_attestation: 2, domain_deposit: 3, - domain_exit: 4, + domain_voluntary_exit: 4, domain_transfer: 5, /* @@ -265,12 +243,11 @@ impl ChainSpec { /// Returns a `ChainSpec` compatible with the specification suitable for 8 validators. pub(crate) fn few_validators() -> Self { - let genesis_slot = Slot::new(2_u64.pow(32)); + let genesis_slot = Slot::new(0); let slots_per_epoch = 8; let genesis_epoch = genesis_slot.epoch(slots_per_epoch); Self { - shard_count: 8, target_committee_size: 1, genesis_slot, genesis_epoch, @@ -312,11 +289,11 @@ mod tests { fn test_get_domain() { let spec = ChainSpec::foundation(); - test_domain(Domain::BeaconBlock, spec.domain_beacon_block, &spec); + test_domain(Domain::BeaconProposer, spec.domain_beacon_proposer, &spec); test_domain(Domain::Randao, spec.domain_randao, &spec); test_domain(Domain::Attestation, spec.domain_attestation, &spec); test_domain(Domain::Deposit, spec.domain_deposit, &spec); - test_domain(Domain::Exit, spec.domain_exit, &spec); + test_domain(Domain::VoluntaryExit, spec.domain_voluntary_exit, &spec); test_domain(Domain::Transfer, spec.domain_transfer, &spec); } } diff --git a/eth2/types/src/crosslink.rs b/eth2/types/src/crosslink.rs index cb3ce5615..9a06ca8bf 100644 --- a/eth2/types/src/crosslink.rs +++ b/eth2/types/src/crosslink.rs @@ -8,11 +8,12 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// Specifies the block hash for a shard at an epoch. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, Clone, PartialEq, + Eq, Default, Serialize, Deserialize, @@ -25,6 +26,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; )] pub struct Crosslink { pub epoch: Epoch, + pub previous_crosslink_root: Hash256, pub crosslink_data_root: Hash256, } diff --git a/eth2/types/src/crosslink_committee.rs b/eth2/types/src/crosslink_committee.rs index 25c42c07b..188d56255 100644 --- a/eth2/types/src/crosslink_committee.rs +++ b/eth2/types/src/crosslink_committee.rs @@ -1,21 +1,25 @@ use crate::*; -use serde_derive::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; use tree_hash_derive::{CachedTreeHash, TreeHash}; -#[derive( - Default, - Clone, - Debug, - PartialEq, - Serialize, - Deserialize, - Decode, - Encode, - TreeHash, - CachedTreeHash, -)] -pub struct CrosslinkCommittee { +#[derive(Default, Clone, Debug, PartialEq, TreeHash, CachedTreeHash)] +pub struct CrosslinkCommittee<'a> { + pub slot: Slot, + pub shard: Shard, + pub committee: &'a [usize], +} + +impl<'a> CrosslinkCommittee<'a> { + pub fn into_owned(self) -> OwnedCrosslinkCommittee { + OwnedCrosslinkCommittee { + slot: self.slot, + shard: self.shard, + committee: self.committee.to_vec(), + } + } +} + +#[derive(Default, Clone, Debug, PartialEq, TreeHash, CachedTreeHash)] +pub struct OwnedCrosslinkCommittee { pub slot: Slot, pub shard: Shard, pub committee: Vec, diff --git a/eth2/types/src/deposit.rs b/eth2/types/src/deposit.rs index 67ceea5be..1b031b0d5 100644 --- a/eth2/types/src/deposit.rs +++ b/eth2/types/src/deposit.rs @@ -9,7 +9,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// A deposit to potentially become a beacon chain validator. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, @@ -25,7 +25,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; pub struct Deposit { pub proof: FixedLenVec, pub index: u64, - pub deposit_data: DepositData, + pub data: DepositData, } #[cfg(test)] diff --git a/eth2/types/src/deposit_data.rs b/eth2/types/src/deposit_data.rs index bee1d503e..274fa68a4 100644 --- a/eth2/types/src/deposit_data.rs +++ b/eth2/types/src/deposit_data.rs @@ -1,14 +1,16 @@ -use super::DepositInput; use crate::test_utils::TestRandom; +use crate::*; +use bls::{PublicKey, Signature}; use serde_derive::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; -use tree_hash_derive::{CachedTreeHash, TreeHash}; +use tree_hash::{SignedRoot, TreeHash}; +use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; -/// Data generated by the deposit contract. +/// The data supplied by the user to the deposit contract. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, @@ -17,14 +19,35 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; Deserialize, Encode, Decode, + SignedRoot, TreeHash, CachedTreeHash, TestRandom, )] pub struct DepositData { + pub pubkey: PublicKey, + pub withdrawal_credentials: Hash256, pub amount: u64, - pub timestamp: u64, - pub deposit_input: DepositInput, + #[signed_root(skip_hashing)] + pub signature: Signature, +} + +impl DepositData { + /// Generate the signature for a given DepositData details. + /// + /// Spec v0.6.1 + pub fn create_signature( + &self, + secret_key: &SecretKey, + epoch: Epoch, + fork: &Fork, + spec: &ChainSpec, + ) -> Signature { + let msg = self.signed_root(); + let domain = spec.get_domain(epoch, Domain::Deposit, fork); + + Signature::new(msg.as_slice(), domain, secret_key) + } } #[cfg(test)] diff --git a/eth2/types/src/deposit_input.rs b/eth2/types/src/deposit_input.rs deleted file mode 100644 index f44c75f5a..000000000 --- a/eth2/types/src/deposit_input.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::test_utils::TestRandom; -use crate::*; -use bls::{PublicKey, Signature}; - -use serde_derive::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use test_random_derive::TestRandom; -use tree_hash::{SignedRoot, TreeHash}; -use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; - -/// The data supplied by the user to the deposit contract. -/// -/// Spec v0.5.1 -#[derive( - Debug, - PartialEq, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - SignedRoot, - TreeHash, - CachedTreeHash, - TestRandom, -)] -pub struct DepositInput { - pub pubkey: PublicKey, - pub withdrawal_credentials: Hash256, - #[signed_root(skip_hashing)] - pub proof_of_possession: Signature, -} - -impl DepositInput { - /// Generate the 'proof_of_posession' signature for a given DepositInput details. - /// - /// Spec v0.5.1 - pub fn create_proof_of_possession( - &self, - secret_key: &SecretKey, - epoch: Epoch, - fork: &Fork, - spec: &ChainSpec, - ) -> Signature { - let msg = self.signed_root(); - let domain = spec.get_domain(epoch, Domain::Deposit, fork); - - Signature::new(msg.as_slice(), domain, secret_key) - } - - /// Verify that proof-of-possession is valid. - /// - /// Spec v0.5.1 - pub fn validate_proof_of_possession( - &self, - epoch: Epoch, - fork: &Fork, - spec: &ChainSpec, - ) -> bool { - let msg = self.signed_root(); - let domain = spec.get_domain(epoch, Domain::Deposit, fork); - - self.proof_of_possession.verify(&msg, domain, &self.pubkey) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - ssz_tests!(DepositInput); - cached_tree_hash_tests!(DepositInput); - - #[test] - fn can_create_and_validate() { - let spec = ChainSpec::foundation(); - let fork = Fork::genesis(&spec); - let keypair = Keypair::random(); - let epoch = Epoch::new(0); - - let mut deposit_input = DepositInput { - pubkey: keypair.pk.clone(), - withdrawal_credentials: Hash256::zero(), - proof_of_possession: Signature::empty_signature(), - }; - - deposit_input.proof_of_possession = - deposit_input.create_proof_of_possession(&keypair.sk, epoch, &fork, &spec); - - assert!(deposit_input.validate_proof_of_possession(epoch, &fork, &spec)); - } -} diff --git a/eth2/types/src/eth1_data.rs b/eth2/types/src/eth1_data.rs index aaf5bca54..0a7043543 100644 --- a/eth2/types/src/eth1_data.rs +++ b/eth2/types/src/eth1_data.rs @@ -8,7 +8,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// Contains data obtained from the Eth1 chain. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, @@ -24,6 +24,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; )] pub struct Eth1Data { pub deposit_root: Hash256, + pub deposit_count: u64, pub block_hash: Hash256, } diff --git a/eth2/types/src/eth1_data_vote.rs b/eth2/types/src/eth1_data_vote.rs deleted file mode 100644 index 27cb0be78..000000000 --- a/eth2/types/src/eth1_data_vote.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::Eth1Data; -use crate::test_utils::TestRandom; - -use serde_derive::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use test_random_derive::TestRandom; -use tree_hash_derive::{CachedTreeHash, TreeHash}; - -/// A summation of votes for some `Eth1Data`. -/// -/// Spec v0.5.1 -#[derive( - Debug, - PartialEq, - Clone, - Default, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - CachedTreeHash, - TestRandom, -)] -pub struct Eth1DataVote { - pub eth1_data: Eth1Data, - pub vote_count: u64, -} - -#[cfg(test)] -mod tests { - use super::*; - - ssz_tests!(Eth1DataVote); - cached_tree_hash_tests!(Eth1DataVote); -} diff --git a/eth2/types/src/fork.rs b/eth2/types/src/fork.rs index 3e7254dd3..eb4e183f2 100644 --- a/eth2/types/src/fork.rs +++ b/eth2/types/src/fork.rs @@ -2,7 +2,6 @@ use crate::{ test_utils::{fork_from_hex_str, TestRandom}, ChainSpec, Epoch, }; -use int_to_bytes::int_to_bytes4; use serde_derive::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -11,7 +10,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// Specifies a fork of the `BeaconChain`, to prevent replay attacks. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, Clone, @@ -36,21 +35,18 @@ pub struct Fork { impl Fork { /// Initialize the `Fork` from the genesis parameters in the `spec`. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn genesis(spec: &ChainSpec) -> Self { - let mut current_version: [u8; 4] = [0; 4]; - current_version.copy_from_slice(&int_to_bytes4(spec.genesis_fork_version)); - Self { - previous_version: current_version, - current_version, + previous_version: [0; 4], + current_version: [0; 4], epoch: spec.genesis_epoch, } } /// Return the fork version of the given ``epoch``. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn get_fork_version(&self, epoch: Epoch) -> [u8; 4] { if epoch < self.epoch { return self.previous_version; @@ -66,10 +62,9 @@ mod tests { ssz_tests!(Fork); cached_tree_hash_tests!(Fork); - fn test_genesis(version: u32, epoch: Epoch) { + fn test_genesis(epoch: Epoch) { let mut spec = ChainSpec::foundation(); - spec.genesis_fork_version = version; spec.genesis_epoch = epoch; let fork = Fork::genesis(&spec); @@ -79,19 +74,14 @@ mod tests { fork.previous_version, fork.current_version, "previous and current are not identical" ); - assert_eq!( - fork.current_version, - version.to_le_bytes(), - "current version incorrect" - ); } #[test] fn genesis() { - test_genesis(0, Epoch::new(0)); - test_genesis(9, Epoch::new(11)); - test_genesis(2_u32.pow(31), Epoch::new(2_u64.pow(63))); - test_genesis(u32::max_value(), Epoch::max_value()); + test_genesis(Epoch::new(0)); + test_genesis(Epoch::new(11)); + test_genesis(Epoch::new(2_u64.pow(63))); + test_genesis(Epoch::max_value()); } #[test] diff --git a/eth2/types/src/historical_batch.rs b/eth2/types/src/historical_batch.rs index d80838221..3480508dc 100644 --- a/eth2/types/src/historical_batch.rs +++ b/eth2/types/src/historical_batch.rs @@ -9,7 +9,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// Historical block and state roots. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, Clone, diff --git a/eth2/types/src/indexed_attestation.rs b/eth2/types/src/indexed_attestation.rs new file mode 100644 index 000000000..14f6e470b --- /dev/null +++ b/eth2/types/src/indexed_attestation.rs @@ -0,0 +1,135 @@ +use crate::{test_utils::TestRandom, AggregateSignature, AttestationData}; +use serde_derive::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash::TreeHash; +use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; + +/// Details an attestation that can be slashable. +/// +/// To be included in an `AttesterSlashing`. +/// +/// Spec v0.6.1 +#[derive( + Debug, + PartialEq, + Clone, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + CachedTreeHash, + TestRandom, + SignedRoot, +)] +pub struct IndexedAttestation { + /// Lists validator registry indices, not committee indices. + pub custody_bit_0_indices: Vec, + pub custody_bit_1_indices: Vec, + pub data: AttestationData, + #[signed_root(skip_hashing)] + pub signature: AggregateSignature, +} + +impl IndexedAttestation { + /// Check if ``attestation_data_1`` and ``attestation_data_2`` have the same target. + /// + /// Spec v0.6.1 + pub fn is_double_vote(&self, other: &IndexedAttestation) -> bool { + self.data.target_epoch == other.data.target_epoch && self.data != other.data + } + + /// Check if ``attestation_data_1`` surrounds ``attestation_data_2``. + /// + /// Spec v0.6.1 + pub fn is_surround_vote(&self, other: &IndexedAttestation) -> bool { + self.data.source_epoch < other.data.source_epoch + && other.data.target_epoch < self.data.target_epoch + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::slot_epoch::Epoch; + use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng}; + + #[test] + pub fn test_is_double_vote_true() { + let indexed_vote_first = create_indexed_attestation(3, 1); + let indexed_vote_second = create_indexed_attestation(3, 2); + + assert_eq!( + indexed_vote_first.is_double_vote(&indexed_vote_second), + true + ) + } + + #[test] + pub fn test_is_double_vote_false() { + let indexed_vote_first = create_indexed_attestation(1, 1); + let indexed_vote_second = create_indexed_attestation(2, 1); + + assert_eq!( + indexed_vote_first.is_double_vote(&indexed_vote_second), + false + ); + } + + #[test] + pub fn test_is_surround_vote_true() { + let indexed_vote_first = create_indexed_attestation(2, 1); + let indexed_vote_second = create_indexed_attestation(1, 2); + + assert_eq!( + indexed_vote_first.is_surround_vote(&indexed_vote_second), + true + ); + } + + #[test] + pub fn test_is_surround_vote_true_realistic() { + let indexed_vote_first = create_indexed_attestation(4, 1); + let indexed_vote_second = create_indexed_attestation(3, 2); + + assert_eq!( + indexed_vote_first.is_surround_vote(&indexed_vote_second), + true + ); + } + + #[test] + pub fn test_is_surround_vote_false_source_epoch_fails() { + let indexed_vote_first = create_indexed_attestation(2, 2); + let indexed_vote_second = create_indexed_attestation(1, 1); + + assert_eq!( + indexed_vote_first.is_surround_vote(&indexed_vote_second), + false + ); + } + + #[test] + pub fn test_is_surround_vote_false_target_epoch_fails() { + let indexed_vote_first = create_indexed_attestation(1, 1); + let indexed_vote_second = create_indexed_attestation(2, 2); + + assert_eq!( + indexed_vote_first.is_surround_vote(&indexed_vote_second), + false + ); + } + + ssz_tests!(IndexedAttestation); + cached_tree_hash_tests!(IndexedAttestation); + + fn create_indexed_attestation(target_epoch: u64, source_epoch: u64) -> IndexedAttestation { + let mut rng = XorShiftRng::from_seed([42; 16]); + let mut indexed_vote = IndexedAttestation::random_for_test(&mut rng); + + indexed_vote.data.source_epoch = Epoch::new(source_epoch); + indexed_vote.data.target_epoch = Epoch::new(target_epoch); + indexed_vote + } +} diff --git a/eth2/types/src/lib.rs b/eth2/types/src/lib.rs index 79981c890..4d0ec5fae 100644 --- a/eth2/types/src/lib.rs +++ b/eth2/types/src/lib.rs @@ -17,15 +17,13 @@ pub mod crosslink; pub mod crosslink_committee; pub mod deposit; pub mod deposit_data; -pub mod deposit_input; pub mod eth1_data; -pub mod eth1_data_vote; pub mod fork; pub mod free_attestation; pub mod historical_batch; +pub mod indexed_attestation; pub mod pending_attestation; pub mod proposer_slashing; -pub mod slashable_attestation; pub mod transfer; pub mod voluntary_exit; #[macro_use] @@ -49,19 +47,17 @@ pub use crate::beacon_block_header::BeaconBlockHeader; pub use crate::beacon_state::{Error as BeaconStateError, *}; pub use crate::chain_spec::{ChainSpec, Domain}; pub use crate::crosslink::Crosslink; -pub use crate::crosslink_committee::CrosslinkCommittee; +pub use crate::crosslink_committee::{CrosslinkCommittee, OwnedCrosslinkCommittee}; pub use crate::deposit::Deposit; pub use crate::deposit_data::DepositData; -pub use crate::deposit_input::DepositInput; pub use crate::eth1_data::Eth1Data; -pub use crate::eth1_data_vote::Eth1DataVote; pub use crate::fork::Fork; pub use crate::free_attestation::FreeAttestation; pub use crate::historical_batch::HistoricalBatch; +pub use crate::indexed_attestation::IndexedAttestation; pub use crate::pending_attestation::PendingAttestation; pub use crate::proposer_slashing::ProposerSlashing; pub use crate::relative_epoch::{Error as RelativeEpochError, RelativeEpoch}; -pub use crate::slashable_attestation::SlashableAttestation; pub use crate::slot_epoch::{Epoch, Slot}; pub use crate::slot_height::SlotHeight; pub use crate::transfer::Transfer; @@ -85,7 +81,7 @@ pub type AttesterMap = HashMap<(u64, u64), Vec>; pub type ProposerMap = HashMap; pub use bls::{AggregatePublicKey, AggregateSignature, Keypair, PublicKey, SecretKey, Signature}; -pub use fixed_len_vec::{typenum::Unsigned, FixedLenVec}; +pub use fixed_len_vec::{typenum, typenum::Unsigned, FixedLenVec}; pub use libp2p::floodsub::{Topic, TopicBuilder, TopicHash}; pub use libp2p::multiaddr; pub use libp2p::Multiaddr; diff --git a/eth2/types/src/pending_attestation.rs b/eth2/types/src/pending_attestation.rs index b158f1274..2c3b04a42 100644 --- a/eth2/types/src/pending_attestation.rs +++ b/eth2/types/src/pending_attestation.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{Attestation, AttestationData, Bitfield, Slot}; +use crate::{AttestationData, Bitfield}; use serde_derive::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -8,7 +8,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// An attestation that has been included in the state but not yet fully processed. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, Clone, @@ -24,20 +24,8 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; pub struct PendingAttestation { pub aggregation_bitfield: Bitfield, pub data: AttestationData, - pub custody_bitfield: Bitfield, - pub inclusion_slot: Slot, -} - -impl PendingAttestation { - /// Create a `PendingAttestation` from an `Attestation`, at the given `inclusion_slot`. - pub fn from_attestation(attestation: &Attestation, inclusion_slot: Slot) -> Self { - PendingAttestation { - data: attestation.data.clone(), - aggregation_bitfield: attestation.aggregation_bitfield.clone(), - custody_bitfield: attestation.custody_bitfield.clone(), - inclusion_slot, - } - } + pub inclusion_delay: u64, + pub proposer_index: u64, } #[cfg(test)] diff --git a/eth2/types/src/proposer_slashing.rs b/eth2/types/src/proposer_slashing.rs index 0c419dd56..b4d5ef0e5 100644 --- a/eth2/types/src/proposer_slashing.rs +++ b/eth2/types/src/proposer_slashing.rs @@ -8,7 +8,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// Two conflicting proposals from the same proposer (validator). /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, diff --git a/eth2/types/src/relative_epoch.rs b/eth2/types/src/relative_epoch.rs index 6538ca4aa..3178701e8 100644 --- a/eth2/types/src/relative_epoch.rs +++ b/eth2/types/src/relative_epoch.rs @@ -4,41 +4,32 @@ use crate::*; pub enum Error { EpochTooLow { base: Epoch, other: Epoch }, EpochTooHigh { base: Epoch, other: Epoch }, - AmbiguiousNextEpoch, } /// Defines the epochs relative to some epoch. Most useful when referring to the committees prior /// to and following some epoch. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive(Debug, PartialEq, Clone, Copy)] pub enum RelativeEpoch { /// The prior epoch. Previous, /// The current epoch. Current, - /// The next epoch if there _is_ a validator registry update. - /// - /// If the validator registry is updated during an epoch transition, a new shuffling seed is - /// generated, this changes the attestation and proposal roles. - NextWithRegistryChange, - /// The next epoch if there _is not_ a validator registry update. - /// - /// If the validator registry _is not_ updated during an epoch transition, the shuffling stays - /// the same. - NextWithoutRegistryChange, + /// The next epoch. + Next, } impl RelativeEpoch { /// Returns the `epoch` that `self` refers to, with respect to the `base` epoch. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn into_epoch(self, base: Epoch) -> Epoch { match self { - RelativeEpoch::Previous => base - 1, + // Due to saturating nature of epoch, check for current first. RelativeEpoch::Current => base, - RelativeEpoch::NextWithoutRegistryChange => base + 1, - RelativeEpoch::NextWithRegistryChange => base + 1, + RelativeEpoch::Previous => base - 1, + RelativeEpoch::Next => base + 1, } } @@ -48,17 +39,16 @@ impl RelativeEpoch { /// Returns an error when: /// - `EpochTooLow` when `other` is more than 1 prior to `base`. /// - `EpochTooHigh` when `other` is more than 1 after `base`. - /// - `AmbiguiousNextEpoch` whenever `other` is one after `base`, because it's unknowable if - /// there will be a registry change. /// - /// Spec v0.5.1 + /// Spec v0.6.1 pub fn from_epoch(base: Epoch, other: Epoch) -> Result { - if other == base - 1 { - Ok(RelativeEpoch::Previous) - } else if other == base { + // Due to saturating nature of epoch, check for current first. + if other == base { Ok(RelativeEpoch::Current) + } else if other == base - 1 { + Ok(RelativeEpoch::Previous) } else if other == base + 1 { - Err(Error::AmbiguiousNextEpoch) + Ok(RelativeEpoch::Next) } else if other < base { Err(Error::EpochTooLow { base, other }) } else { @@ -67,11 +57,8 @@ impl RelativeEpoch { } /// Convenience function for `Self::from_epoch` where both slots are converted into epochs. - pub fn from_slot(base: Slot, other: Slot, spec: &ChainSpec) -> Result { - Self::from_epoch( - base.epoch(spec.slots_per_epoch), - other.epoch(spec.slots_per_epoch), - ) + pub fn from_slot(base: Slot, other: Slot, slots_per_epoch: u64) -> Result { + Self::from_epoch(base.epoch(slots_per_epoch), other.epoch(slots_per_epoch)) } } @@ -85,14 +72,7 @@ mod tests { assert_eq!(RelativeEpoch::Current.into_epoch(base), base); assert_eq!(RelativeEpoch::Previous.into_epoch(base), base - 1); - assert_eq!( - RelativeEpoch::NextWithRegistryChange.into_epoch(base), - base + 1 - ); - assert_eq!( - RelativeEpoch::NextWithoutRegistryChange.into_epoch(base), - base + 1 - ); + assert_eq!(RelativeEpoch::Next.into_epoch(base), base + 1); } #[test] @@ -109,26 +89,26 @@ mod tests { ); assert_eq!( RelativeEpoch::from_epoch(base, base + 1), - Err(RelativeEpochError::AmbiguiousNextEpoch) + Ok(RelativeEpoch::Next) ); } #[test] fn from_slot() { - let spec = ChainSpec::foundation(); - let base = Epoch::new(10).start_slot(spec.slots_per_epoch); + let slots_per_epoch: u64 = 64; + let base = Slot::new(10 * slots_per_epoch); assert_eq!( - RelativeEpoch::from_slot(base, base - 1, &spec), + RelativeEpoch::from_slot(base, base - 1, slots_per_epoch), Ok(RelativeEpoch::Previous) ); assert_eq!( - RelativeEpoch::from_slot(base, base, &spec), + RelativeEpoch::from_slot(base, base, slots_per_epoch), Ok(RelativeEpoch::Current) ); assert_eq!( - RelativeEpoch::from_slot(base, base + spec.slots_per_epoch, &spec), - Err(RelativeEpochError::AmbiguiousNextEpoch) + RelativeEpoch::from_slot(base, base + slots_per_epoch, slots_per_epoch), + Ok(RelativeEpoch::Next) ); } } diff --git a/eth2/types/src/slashable_attestation.rs b/eth2/types/src/slashable_attestation.rs deleted file mode 100644 index 19d9a87b6..000000000 --- a/eth2/types/src/slashable_attestation.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::{test_utils::TestRandom, AggregateSignature, AttestationData, Bitfield, ChainSpec}; - -use serde_derive::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use test_random_derive::TestRandom; -use tree_hash::TreeHash; -use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; - -/// Details an attestation that can be slashable. -/// -/// To be included in an `AttesterSlashing`. -/// -/// Spec v0.5.1 -#[derive( - Debug, - PartialEq, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - CachedTreeHash, - TestRandom, - SignedRoot, -)] -pub struct SlashableAttestation { - /// Lists validator registry indices, not committee indices. - pub validator_indices: Vec, - pub data: AttestationData, - pub custody_bitfield: Bitfield, - #[signed_root(skip_hashing)] - pub aggregate_signature: AggregateSignature, -} - -impl SlashableAttestation { - /// Check if ``attestation_data_1`` and ``attestation_data_2`` have the same target. - /// - /// Spec v0.5.1 - pub fn is_double_vote(&self, other: &SlashableAttestation, spec: &ChainSpec) -> bool { - self.data.slot.epoch(spec.slots_per_epoch) == other.data.slot.epoch(spec.slots_per_epoch) - } - - /// Check if ``attestation_data_1`` surrounds ``attestation_data_2``. - /// - /// Spec v0.5.1 - pub fn is_surround_vote(&self, other: &SlashableAttestation, spec: &ChainSpec) -> bool { - let source_epoch_1 = self.data.source_epoch; - let source_epoch_2 = other.data.source_epoch; - let target_epoch_1 = self.data.slot.epoch(spec.slots_per_epoch); - let target_epoch_2 = other.data.slot.epoch(spec.slots_per_epoch); - - (source_epoch_1 < source_epoch_2) & (target_epoch_2 < target_epoch_1) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::chain_spec::ChainSpec; - use crate::slot_epoch::{Epoch, Slot}; - use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng}; - - #[test] - pub fn test_is_double_vote_true() { - let spec = ChainSpec::foundation(); - let slashable_vote_first = create_slashable_attestation(1, 1, &spec); - let slashable_vote_second = create_slashable_attestation(1, 1, &spec); - - assert_eq!( - slashable_vote_first.is_double_vote(&slashable_vote_second, &spec), - true - ) - } - - #[test] - pub fn test_is_double_vote_false() { - let spec = ChainSpec::foundation(); - let slashable_vote_first = create_slashable_attestation(1, 1, &spec); - let slashable_vote_second = create_slashable_attestation(2, 1, &spec); - - assert_eq!( - slashable_vote_first.is_double_vote(&slashable_vote_second, &spec), - false - ); - } - - #[test] - pub fn test_is_surround_vote_true() { - let spec = ChainSpec::foundation(); - let slashable_vote_first = create_slashable_attestation(2, 1, &spec); - let slashable_vote_second = create_slashable_attestation(1, 2, &spec); - - assert_eq!( - slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec), - true - ); - } - - #[test] - pub fn test_is_surround_vote_true_realistic() { - let spec = ChainSpec::foundation(); - let slashable_vote_first = create_slashable_attestation(4, 1, &spec); - let slashable_vote_second = create_slashable_attestation(3, 2, &spec); - - assert_eq!( - slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec), - true - ); - } - - #[test] - pub fn test_is_surround_vote_false_source_epoch_fails() { - let spec = ChainSpec::foundation(); - let slashable_vote_first = create_slashable_attestation(2, 2, &spec); - let slashable_vote_second = create_slashable_attestation(1, 1, &spec); - - assert_eq!( - slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec), - false - ); - } - - #[test] - pub fn test_is_surround_vote_false_target_epoch_fails() { - let spec = ChainSpec::foundation(); - let slashable_vote_first = create_slashable_attestation(1, 1, &spec); - let slashable_vote_second = create_slashable_attestation(2, 2, &spec); - - assert_eq!( - slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec), - false - ); - } - - ssz_tests!(SlashableAttestation); - cached_tree_hash_tests!(SlashableAttestation); - - fn create_slashable_attestation( - slot_factor: u64, - source_epoch: u64, - spec: &ChainSpec, - ) -> SlashableAttestation { - let mut rng = XorShiftRng::from_seed([42; 16]); - let mut slashable_vote = SlashableAttestation::random_for_test(&mut rng); - - slashable_vote.data.slot = Slot::new(slot_factor * spec.slots_per_epoch); - slashable_vote.data.source_epoch = Epoch::new(source_epoch); - slashable_vote - } -} diff --git a/eth2/types/src/slot_epoch.rs b/eth2/types/src/slot_epoch.rs index b0f4bdcf9..82cee0d75 100644 --- a/eth2/types/src/slot_epoch.rs +++ b/eth2/types/src/slot_epoch.rs @@ -58,10 +58,12 @@ impl Epoch { Epoch(u64::max_value()) } + /// The first slot in the epoch. pub fn start_slot(self, slots_per_epoch: u64) -> Slot { Slot::from(self.0.saturating_mul(slots_per_epoch)) } + /// The last slot in the epoch. pub fn end_slot(self, slots_per_epoch: u64) -> Slot { Slot::from( self.0 @@ -71,6 +73,20 @@ impl Epoch { ) } + /// Position of some slot inside an epoch, if any. + /// + /// E.g., the first `slot` in `epoch` is at position `0`. + pub fn position(&self, slot: Slot, slots_per_epoch: u64) -> Option { + let start = self.start_slot(slots_per_epoch); + let end = self.end_slot(slots_per_epoch); + + if (slot >= start) && (slot <= end) { + Some(slot.as_usize() - start.as_usize()) + } else { + None + } + } + pub fn slot_iter(&self, slots_per_epoch: u64) -> SlotIter { SlotIter { current_iteration: 0, @@ -124,6 +140,26 @@ mod epoch_tests { assert_eq!(epoch.end_slot(slots_per_epoch), Slot::new(7)); } + #[test] + fn position() { + let slots_per_epoch = 8; + + let epoch = Epoch::new(0); + assert_eq!(epoch.position(Slot::new(0), slots_per_epoch), Some(0)); + assert_eq!(epoch.position(Slot::new(1), slots_per_epoch), Some(1)); + assert_eq!(epoch.position(Slot::new(2), slots_per_epoch), Some(2)); + assert_eq!(epoch.position(Slot::new(3), slots_per_epoch), Some(3)); + assert_eq!(epoch.position(Slot::new(4), slots_per_epoch), Some(4)); + assert_eq!(epoch.position(Slot::new(5), slots_per_epoch), Some(5)); + assert_eq!(epoch.position(Slot::new(6), slots_per_epoch), Some(6)); + assert_eq!(epoch.position(Slot::new(7), slots_per_epoch), Some(7)); + assert_eq!(epoch.position(Slot::new(8), slots_per_epoch), None); + + let epoch = Epoch::new(1); + assert_eq!(epoch.position(Slot::new(7), slots_per_epoch), None); + assert_eq!(epoch.position(Slot::new(8), slots_per_epoch), Some(0)); + } + #[test] fn slot_iter() { let slots_per_epoch = 8; diff --git a/eth2/types/src/test_utils/builders.rs b/eth2/types/src/test_utils/builders.rs new file mode 100644 index 000000000..8017e4e5d --- /dev/null +++ b/eth2/types/src/test_utils/builders.rs @@ -0,0 +1,21 @@ +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_transfer_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_transfer_builder::*; +pub use testing_voluntary_exit_builder::*; diff --git a/eth2/types/src/test_utils/testing_attestation_builder.rs b/eth2/types/src/test_utils/builders/testing_attestation_builder.rs similarity index 93% rename from eth2/types/src/test_utils/testing_attestation_builder.rs rename to eth2/types/src/test_utils/builders/testing_attestation_builder.rs index 0df1c01b8..27fae4e76 100644 --- a/eth2/types/src/test_utils/testing_attestation_builder.rs +++ b/eth2/types/src/test_utils/builders/testing_attestation_builder.rs @@ -33,7 +33,7 @@ impl TestingAttestationBuilder { aggregation_bitfield, data: data_builder.build(), custody_bitfield, - aggregate_signature: AggregateSignature::new(), + signature: AggregateSignature::new(), }; Self { @@ -77,13 +77,13 @@ impl TestingAttestationBuilder { .tree_hash_root(); let domain = spec.get_domain( - self.attestation.data.slot.epoch(spec.slots_per_epoch), + self.attestation.data.target_epoch, Domain::Attestation, fork, ); let signature = Signature::new(&message, domain, secret_keys[key_index]); - self.attestation.aggregate_signature.add(&signature) + self.attestation.signature.add(&signature) } } diff --git a/eth2/types/src/test_utils/testing_attestation_data_builder.rs b/eth2/types/src/test_utils/builders/testing_attestation_data_builder.rs similarity index 71% rename from eth2/types/src/test_utils/testing_attestation_data_builder.rs rename to eth2/types/src/test_utils/builders/testing_attestation_data_builder.rs index fbe79df1e..2150f5433 100644 --- a/eth2/types/src/test_utils/testing_attestation_data_builder.rs +++ b/eth2/types/src/test_utils/builders/testing_attestation_data_builder.rs @@ -1,4 +1,5 @@ use crate::*; +use tree_hash::TreeHash; /// Builds an `AttestationData` to be used for testing purposes. /// @@ -16,8 +17,8 @@ impl TestingAttestationDataBuilder { slot: Slot, spec: &ChainSpec, ) -> Self { - let current_epoch = state.current_epoch(spec); - let previous_epoch = state.previous_epoch(spec); + let current_epoch = state.current_epoch(); + let previous_epoch = state.previous_epoch(); let is_previous_epoch = state.slot.epoch(spec.slots_per_epoch) != slot.epoch(spec.slots_per_epoch); @@ -28,6 +29,12 @@ impl TestingAttestationDataBuilder { state.current_justified_epoch }; + let target_epoch = if is_previous_epoch { + state.previous_epoch() + } else { + state.current_epoch() + }; + let target_root = if is_previous_epoch { *state .get_block_root(previous_epoch.start_slot(spec.slots_per_epoch)) @@ -38,26 +45,34 @@ impl TestingAttestationDataBuilder { .unwrap() }; + let previous_crosslink_root = if is_previous_epoch { + Hash256::from_slice( + &state + .get_previous_crosslink(shard) + .unwrap() + .tree_hash_root(), + ) + } else { + Hash256::from_slice(&state.get_current_crosslink(shard).unwrap().tree_hash_root()) + }; + let source_root = *state .get_block_root(source_epoch.start_slot(spec.slots_per_epoch)) .unwrap(); let data = AttestationData { // LMD GHOST vote - slot, beacon_block_root: *state.get_block_root(slot).unwrap(), // FFG Vote source_epoch, source_root, + target_epoch, target_root, // Crosslink vote shard, - previous_crosslink: Crosslink { - epoch: slot.epoch(spec.slots_per_epoch), - crosslink_data_root: spec.zero_hash, - }, + previous_crosslink_root, crosslink_data_root: spec.zero_hash, }; diff --git a/eth2/types/src/test_utils/testing_attester_slashing_builder.rs b/eth2/types/src/test_utils/builders/testing_attester_slashing_builder.rs similarity index 56% rename from eth2/types/src/test_utils/testing_attester_slashing_builder.rs rename to eth2/types/src/test_utils/builders/testing_attester_slashing_builder.rs index dc01f7fb0..6cde3f145 100644 --- a/eth2/types/src/test_utils/testing_attester_slashing_builder.rs +++ b/eth2/types/src/test_utils/builders/testing_attester_slashing_builder.rs @@ -21,23 +21,20 @@ impl TestingAttesterSlashingBuilder { where F: Fn(u64, &[u8], Epoch, Domain) -> Signature, { - let double_voted_slot = Slot::new(0); let shard = 0; - let epoch = Epoch::new(0); + let epoch_1 = Epoch::new(1); + let epoch_2 = Epoch::new(2); let hash_1 = Hash256::from_low_u64_le(1); let hash_2 = Hash256::from_low_u64_le(2); let data_1 = AttestationData { - slot: double_voted_slot, beacon_block_root: hash_1, - source_epoch: epoch, + source_epoch: epoch_1, source_root: hash_1, + target_epoch: epoch_2, target_root: hash_1, shard, - previous_crosslink: Crosslink { - epoch, - crosslink_data_root: hash_1, - }, + previous_crosslink_root: hash_1, crosslink_data_root: hash_1, }; @@ -46,21 +43,21 @@ impl TestingAttesterSlashingBuilder { ..data_1.clone() }; - let mut slashable_attestation_1 = SlashableAttestation { - validator_indices: validator_indices.to_vec(), + let mut attestation_1 = IndexedAttestation { + custody_bit_0_indices: validator_indices.to_vec(), + custody_bit_1_indices: vec![], data: data_1, - custody_bitfield: Bitfield::new(), - aggregate_signature: AggregateSignature::new(), + signature: AggregateSignature::new(), }; - let mut slashable_attestation_2 = SlashableAttestation { - validator_indices: validator_indices.to_vec(), + let mut attestation_2 = IndexedAttestation { + custody_bit_0_indices: validator_indices.to_vec(), + custody_bit_1_indices: vec![], data: data_2, - custody_bitfield: Bitfield::new(), - aggregate_signature: AggregateSignature::new(), + signature: AggregateSignature::new(), }; - let add_signatures = |attestation: &mut SlashableAttestation| { + let add_signatures = |attestation: &mut IndexedAttestation| { // All validators sign with a `false` custody bit. let attestation_data_and_custody_bit = AttestationDataAndCustodyBit { data: attestation.data.clone(), @@ -68,19 +65,19 @@ impl TestingAttesterSlashingBuilder { }; let message = attestation_data_and_custody_bit.tree_hash_root(); - for (i, validator_index) in validator_indices.iter().enumerate() { - attestation.custody_bitfield.set(i, false); - let signature = signer(*validator_index, &message[..], epoch, Domain::Attestation); - attestation.aggregate_signature.add(&signature); + for validator_index in validator_indices { + let signature = + signer(*validator_index, &message[..], epoch_2, Domain::Attestation); + attestation.signature.add(&signature); } }; - add_signatures(&mut slashable_attestation_1); - add_signatures(&mut slashable_attestation_2); + add_signatures(&mut attestation_1); + add_signatures(&mut attestation_2); AttesterSlashing { - slashable_attestation_1, - slashable_attestation_2, + attestation_1, + attestation_2, } } } diff --git a/eth2/types/src/test_utils/testing_beacon_block_builder.rs b/eth2/types/src/test_utils/builders/testing_beacon_block_builder.rs similarity index 97% rename from eth2/types/src/test_utils/testing_beacon_block_builder.rs rename to eth2/types/src/test_utils/builders/testing_beacon_block_builder.rs index 9dca6222a..941ad8fdd 100644 --- a/eth2/types/src/test_utils/testing_beacon_block_builder.rs +++ b/eth2/types/src/test_utils/builders/testing_beacon_block_builder.rs @@ -39,7 +39,7 @@ impl TestingBeaconBlockBuilder { pub fn sign(&mut self, sk: &SecretKey, fork: &Fork, spec: &ChainSpec) { let message = self.block.signed_root(); let epoch = self.block.slot.epoch(spec.slots_per_epoch); - let domain = spec.get_domain(epoch, Domain::BeaconBlock, fork); + let domain = spec.get_domain(epoch, Domain::BeaconProposer, fork); self.block.signature = Signature::new(&message, domain, sk); } @@ -119,15 +119,15 @@ impl TestingBeaconBlockBuilder { break; } - for crosslink_committee in state.get_crosslink_committees_at_slot(slot, spec)? { + for crosslink_committee in state.get_crosslink_committees_at_slot(slot)? { if attestations_added >= num_attestations { break; } committees.push(( slot, - crosslink_committee.committee.clone(), - crosslink_committee.committee.clone(), + crosslink_committee.committee.to_vec(), + crosslink_committee.committee.to_vec(), crosslink_committee.shard, )); diff --git a/eth2/types/src/test_utils/testing_beacon_state_builder.rs b/eth2/types/src/test_utils/builders/testing_beacon_state_builder.rs similarity index 86% rename from eth2/types/src/test_utils/testing_beacon_state_builder.rs rename to eth2/types/src/test_utils/builders/testing_beacon_state_builder.rs index 727b70d93..20ed8a893 100644 --- a/eth2/types/src/test_utils/testing_beacon_state_builder.rs +++ b/eth2/types/src/test_utils/builders/testing_beacon_state_builder.rs @@ -1,4 +1,4 @@ -use super::{generate_deterministic_keypairs, KeypairsFile}; +use super::super::{generate_deterministic_keypairs, KeypairsFile}; use crate::test_utils::TestingPendingAttestationBuilder; use crate::*; use bls::get_withdrawal_credentials; @@ -95,6 +95,7 @@ impl TestingBeaconStateBuilder { /// Creates the builder from an existing set of keypairs. pub fn from_keypairs(keypairs: Vec, spec: &ChainSpec) -> Self { let validator_count = keypairs.len(); + let starting_balance = 32_000_000_000; debug!( "Building {} Validator objects from keypairs...", @@ -112,11 +113,12 @@ impl TestingBeaconStateBuilder { pubkey: keypair.pk.clone(), withdrawal_credentials, // All validators start active. + activation_eligibility_epoch: spec.genesis_epoch, activation_epoch: spec.genesis_epoch, exit_epoch: spec.far_future_epoch, withdrawable_epoch: spec.far_future_epoch, - initiated_exit: false, slashed: false, + effective_balance: starting_balance, } }) .collect(); @@ -137,16 +139,17 @@ impl TestingBeaconStateBuilder { genesis_time, Eth1Data { deposit_root: Hash256::zero(), + deposit_count: 0, block_hash: Hash256::zero(), }, spec, ); - let balances = vec![32_000_000_000; validator_count]; + let balances = vec![starting_balance; validator_count]; debug!("Importing {} existing validators...", validator_count); state.validator_registry = validators; - state.validator_balances = balances; + state.balances = balances; debug!("BeaconState initialized."); @@ -163,14 +166,7 @@ impl TestingBeaconStateBuilder { /// 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> { - let state = &mut self.state; - - state.build_epoch_cache(RelativeEpoch::Previous, &spec)?; - state.build_epoch_cache(RelativeEpoch::Current, &spec)?; - state.build_epoch_cache(RelativeEpoch::NextWithRegistryChange, &spec)?; - state.build_epoch_cache(RelativeEpoch::NextWithoutRegistryChange, &spec)?; - - state.update_pubkey_cache()?; + self.state.build_all_caches(spec).unwrap(); Ok(()) } @@ -192,18 +188,13 @@ impl TestingBeaconStateBuilder { state.slot = slot; - state.previous_shuffling_epoch = epoch - 1; - state.current_shuffling_epoch = epoch; - - state.previous_shuffling_seed = Hash256::from_low_u64_le(0); - state.current_shuffling_seed = Hash256::from_low_u64_le(1); + // FIXME(sproul): update latest_start_shard? state.previous_justified_epoch = epoch - 3; state.current_justified_epoch = epoch - 2; state.justification_bitfield = u64::max_value(); state.finalized_epoch = epoch - 3; - state.validator_registry_update_epoch = epoch - 3; } /// Creates a full set of attestations for the `BeaconState`. Each attestation has full @@ -214,14 +205,14 @@ impl TestingBeaconStateBuilder { let state = &mut self.state; state - .build_epoch_cache(RelativeEpoch::Previous, spec) + .build_committee_cache(RelativeEpoch::Previous, spec) .unwrap(); state - .build_epoch_cache(RelativeEpoch::Current, spec) + .build_committee_cache(RelativeEpoch::Current, spec) .unwrap(); - let current_epoch = state.current_epoch(spec); - let previous_epoch = state.previous_epoch(spec); + let current_epoch = state.current_epoch(); + let previous_epoch = state.previous_epoch(); let first_slot = previous_epoch.start_slot(spec.slots_per_epoch).as_u64(); let last_slot = current_epoch.end_slot(spec.slots_per_epoch).as_u64() @@ -231,10 +222,12 @@ impl TestingBeaconStateBuilder { for slot in first_slot..=last_slot { let slot = Slot::from(slot); - let committees = state - .get_crosslink_committees_at_slot(slot, spec) + let committees: Vec = state + .get_crosslink_committees_at_slot(slot) .unwrap() - .clone(); + .into_iter() + .map(|c| c.clone().into_owned()) + .collect(); for crosslink_committee in committees { let mut builder = TestingPendingAttestationBuilder::new( @@ -248,7 +241,7 @@ impl TestingBeaconStateBuilder { builder.add_committee_participation(signers); let attestation = builder.build(); - if attestation.data.slot.epoch(spec.slots_per_epoch) < state.current_epoch(spec) { + if attestation.data.target_epoch < state.current_epoch() { state.previous_epoch_attestations.push(attestation) } else { state.current_epoch_attestations.push(attestation) diff --git a/eth2/types/src/test_utils/testing_deposit_builder.rs b/eth2/types/src/test_utils/builders/testing_deposit_builder.rs similarity index 62% rename from eth2/types/src/test_utils/testing_deposit_builder.rs rename to eth2/types/src/test_utils/builders/testing_deposit_builder.rs index 080ed5cfb..dcb6a56ef 100644 --- a/eth2/types/src/test_utils/testing_deposit_builder.rs +++ b/eth2/types/src/test_utils/builders/testing_deposit_builder.rs @@ -14,14 +14,11 @@ impl TestingDepositBuilder { let deposit = Deposit { proof: vec![].into(), index: 0, - deposit_data: DepositData { + data: DepositData { + pubkey, + withdrawal_credentials: Hash256::zero(), amount, - timestamp: 1, - deposit_input: DepositInput { - pubkey, - withdrawal_credentials: Hash256::zero(), - proof_of_possession: Signature::empty_signature(), - }, + signature: Signature::empty_signature(), }, }; @@ -43,17 +40,13 @@ impl TestingDepositBuilder { &get_withdrawal_credentials(&keypair.pk, spec.bls_withdrawal_prefix_byte)[..], ); - self.deposit.deposit_data.deposit_input.pubkey = keypair.pk.clone(); - self.deposit - .deposit_data - .deposit_input - .withdrawal_credentials = withdrawal_credentials; + self.deposit.data.pubkey = keypair.pk.clone(); + self.deposit.data.withdrawal_credentials = withdrawal_credentials; - self.deposit.deposit_data.deposit_input.proof_of_possession = self - .deposit - .deposit_data - .deposit_input - .create_proof_of_possession(&keypair.sk, epoch, fork, spec); + self.deposit.data.signature = + self.deposit + .data + .create_signature(&keypair.sk, epoch, fork, spec); } /// Builds the deposit, consuming the builder. diff --git a/eth2/types/src/test_utils/testing_pending_attestation_builder.rs b/eth2/types/src/test_utils/builders/testing_pending_attestation_builder.rs similarity index 77% rename from eth2/types/src/test_utils/testing_pending_attestation_builder.rs rename to eth2/types/src/test_utils/builders/testing_pending_attestation_builder.rs index 023b039b0..89000f219 100644 --- a/eth2/types/src/test_utils/testing_pending_attestation_builder.rs +++ b/eth2/types/src/test_utils/builders/testing_pending_attestation_builder.rs @@ -11,8 +11,7 @@ pub struct TestingPendingAttestationBuilder { impl TestingPendingAttestationBuilder { /// Create a new valid* `PendingAttestation` for the given parameters. /// - /// The `inclusion_slot` will be set to be the earliest possible slot the `Attestation` could - /// have been included (`slot + MIN_ATTESTATION_INCLUSION_DELAY`). + /// The `inclusion_delay` will be set to `MIN_ATTESTATION_INCLUSION_DELAY`. /// /// * The aggregation and custody bitfields will all be empty, they need to be set with /// `Self::add_committee_participation`. @@ -27,8 +26,9 @@ impl TestingPendingAttestationBuilder { let pending_attestation = PendingAttestation { aggregation_bitfield: Bitfield::new(), data: data_builder.build(), - custody_bitfield: Bitfield::new(), - inclusion_slot: slot + spec.min_attestation_inclusion_delay, + inclusion_delay: spec.min_attestation_inclusion_delay, + // FIXME(sproul) + proposer_index: 0, }; Self { @@ -42,15 +42,12 @@ impl TestingPendingAttestationBuilder { /// `signers` is true. pub fn add_committee_participation(&mut self, signers: Vec) { let mut aggregation_bitfield = Bitfield::new(); - let mut custody_bitfield = Bitfield::new(); for (i, signed) in signers.iter().enumerate() { aggregation_bitfield.set(i, *signed); - custody_bitfield.set(i, false); // Fixed to `false` for phase 0. } self.pending_attestation.aggregation_bitfield = aggregation_bitfield; - self.pending_attestation.custody_bitfield = custody_bitfield; } /// Returns the `PendingAttestation`, consuming the builder. diff --git a/eth2/types/src/test_utils/testing_proposer_slashing_builder.rs b/eth2/types/src/test_utils/builders/testing_proposer_slashing_builder.rs similarity index 98% rename from eth2/types/src/test_utils/testing_proposer_slashing_builder.rs rename to eth2/types/src/test_utils/builders/testing_proposer_slashing_builder.rs index 03c257b2d..458082de2 100644 --- a/eth2/types/src/test_utils/testing_proposer_slashing_builder.rs +++ b/eth2/types/src/test_utils/builders/testing_proposer_slashing_builder.rs @@ -41,13 +41,13 @@ impl TestingProposerSlashingBuilder { header_1.signature = { let message = header_1.signed_root(); let epoch = slot.epoch(spec.slots_per_epoch); - signer(proposer_index, &message[..], epoch, Domain::BeaconBlock) + signer(proposer_index, &message[..], epoch, Domain::BeaconProposer) }; header_2.signature = { let message = header_2.signed_root(); let epoch = slot.epoch(spec.slots_per_epoch); - signer(proposer_index, &message[..], epoch, Domain::BeaconBlock) + signer(proposer_index, &message[..], epoch, Domain::BeaconProposer) }; ProposerSlashing { diff --git a/eth2/types/src/test_utils/testing_transfer_builder.rs b/eth2/types/src/test_utils/builders/testing_transfer_builder.rs similarity index 100% rename from eth2/types/src/test_utils/testing_transfer_builder.rs rename to eth2/types/src/test_utils/builders/testing_transfer_builder.rs diff --git a/eth2/types/src/test_utils/testing_voluntary_exit_builder.rs b/eth2/types/src/test_utils/builders/testing_voluntary_exit_builder.rs similarity index 92% rename from eth2/types/src/test_utils/testing_voluntary_exit_builder.rs rename to eth2/types/src/test_utils/builders/testing_voluntary_exit_builder.rs index 8583bc451..58e8e750d 100644 --- a/eth2/types/src/test_utils/testing_voluntary_exit_builder.rs +++ b/eth2/types/src/test_utils/builders/testing_voluntary_exit_builder.rs @@ -25,7 +25,7 @@ impl TestingVoluntaryExitBuilder { /// The signing secret key must match that of the exiting validator. pub fn sign(&mut self, secret_key: &SecretKey, fork: &Fork, spec: &ChainSpec) { let message = self.exit.signed_root(); - let domain = spec.get_domain(self.exit.epoch, Domain::Exit, fork); + let domain = spec.get_domain(self.exit.epoch, Domain::VoluntaryExit, fork); self.exit.signature = Signature::new(&message, domain, secret_key); } diff --git a/eth2/types/src/test_utils/mod.rs b/eth2/types/src/test_utils/mod.rs index 20d53e72e..ee8327be8 100644 --- a/eth2/types/src/test_utils/mod.rs +++ b/eth2/types/src/test_utils/mod.rs @@ -1,20 +1,12 @@ #[macro_use] mod macros; +mod builders; mod generate_deterministic_keypairs; mod keypairs_file; mod serde_utils; mod test_random; -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_transfer_builder; -mod testing_voluntary_exit_builder; +pub use builders::*; pub use generate_deterministic_keypairs::generate_deterministic_keypair; pub use generate_deterministic_keypairs::generate_deterministic_keypairs; pub use keypairs_file::KeypairsFile; @@ -22,15 +14,5 @@ pub use rand::{ RngCore, {prng::XorShiftRng, SeedableRng}, }; -pub use serde_utils::{fork_from_hex_str, u8_from_hex_str}; +pub use serde_utils::{fork_from_hex_str, graffiti_from_hex_str, u8_from_hex_str}; pub use test_random::TestRandom; -pub use testing_attestation_builder::TestingAttestationBuilder; -pub use testing_attestation_data_builder::TestingAttestationDataBuilder; -pub use testing_attester_slashing_builder::TestingAttesterSlashingBuilder; -pub use testing_beacon_block_builder::TestingBeaconBlockBuilder; -pub use testing_beacon_state_builder::{keypairs_path, TestingBeaconStateBuilder}; -pub use testing_deposit_builder::TestingDepositBuilder; -pub use testing_pending_attestation_builder::TestingPendingAttestationBuilder; -pub use testing_proposer_slashing_builder::TestingProposerSlashingBuilder; -pub use testing_transfer_builder::TestingTransferBuilder; -pub use testing_voluntary_exit_builder::TestingVoluntaryExitBuilder; diff --git a/eth2/types/src/test_utils/serde_utils.rs b/eth2/types/src/test_utils/serde_utils.rs index 761aee523..5c0238c0b 100644 --- a/eth2/types/src/test_utils/serde_utils.rs +++ b/eth2/types/src/test_utils/serde_utils.rs @@ -2,6 +2,7 @@ use serde::de::Error; use serde::{Deserialize, Deserializer}; pub const FORK_BYTES_LEN: usize = 4; +pub const GRAFFITI_BYTES_LEN: usize = 32; pub fn u8_from_hex_str<'de, D>(deserializer: D) -> Result where @@ -32,3 +33,24 @@ where } Ok(array) } + +pub fn graffiti_from_hex_str<'de, D>(deserializer: D) -> Result<[u8; GRAFFITI_BYTES_LEN], D::Error> +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + let mut array = [0 as u8; GRAFFITI_BYTES_LEN]; + let decoded: Vec = hex::decode(&s.as_str()[2..]).map_err(D::Error::custom)?; + + if decoded.len() > GRAFFITI_BYTES_LEN { + return Err(D::Error::custom("Fork length too long")); + } + + for (i, item) in array.iter_mut().enumerate() { + if i > decoded.len() { + break; + } + *item = decoded[i]; + } + Ok(array) +} diff --git a/eth2/types/src/test_utils/test_random.rs b/eth2/types/src/test_utils/test_random.rs index ceb785424..4f56d1596 100644 --- a/eth2/types/src/test_utils/test_random.rs +++ b/eth2/types/src/test_utils/test_random.rs @@ -81,3 +81,4 @@ macro_rules! impl_test_random_for_u8_array { } impl_test_random_for_u8_array!(4); +impl_test_random_for_u8_array!(32); diff --git a/eth2/types/src/transfer.rs b/eth2/types/src/transfer.rs index 176a9d75d..ffac85c7b 100644 --- a/eth2/types/src/transfer.rs +++ b/eth2/types/src/transfer.rs @@ -11,7 +11,7 @@ use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; /// The data submitted to the deposit contract. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, Clone, diff --git a/eth2/types/src/validator.rs b/eth2/types/src/validator.rs index ff4cabf35..f16225d7c 100644 --- a/eth2/types/src/validator.rs +++ b/eth2/types/src/validator.rs @@ -7,7 +7,7 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; /// Information about a `BeaconChain` validator. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, Clone, @@ -23,11 +23,12 @@ use tree_hash_derive::{CachedTreeHash, TreeHash}; pub struct Validator { pub pubkey: PublicKey, pub withdrawal_credentials: Hash256, + pub activation_eligibility_epoch: Epoch, pub activation_epoch: Epoch, pub exit_epoch: Epoch, pub withdrawable_epoch: Epoch, - pub initiated_exit: bool, pub slashed: bool, + pub effective_balance: u64, } impl Validator { @@ -36,6 +37,11 @@ impl Validator { self.activation_epoch <= epoch && epoch < self.exit_epoch } + /// Returns `true` if the validator is slashable at some epoch. + pub fn is_slashable_at(&self, epoch: Epoch) -> bool { + !self.slashed && self.activation_epoch <= epoch && epoch < self.withdrawable_epoch + } + /// Returns `true` if the validator is considered exited at some epoch. pub fn is_exited_at(&self, epoch: Epoch) -> bool { self.exit_epoch <= epoch @@ -43,7 +49,7 @@ impl Validator { /// Returns `true` if the validator is able to withdraw at some epoch. pub fn is_withdrawable_at(&self, epoch: Epoch) -> bool { - self.withdrawable_epoch <= epoch + epoch >= self.withdrawable_epoch } } @@ -53,11 +59,12 @@ impl Default for Validator { Self { pubkey: PublicKey::default(), withdrawal_credentials: Hash256::default(), + activation_eligibility_epoch: Epoch::from(std::u64::MAX), activation_epoch: Epoch::from(std::u64::MAX), exit_epoch: Epoch::from(std::u64::MAX), withdrawable_epoch: Epoch::from(std::u64::MAX), - initiated_exit: false, slashed: false, + effective_balance: std::u64::MAX, } } } @@ -75,7 +82,6 @@ mod tests { assert_eq!(v.is_active_at(epoch), false); assert_eq!(v.is_exited_at(epoch), false); assert_eq!(v.is_withdrawable_at(epoch), false); - assert_eq!(v.initiated_exit, false); assert_eq!(v.slashed, false); } diff --git a/eth2/types/src/voluntary_exit.rs b/eth2/types/src/voluntary_exit.rs index a138fb480..6754aa2c9 100644 --- a/eth2/types/src/voluntary_exit.rs +++ b/eth2/types/src/voluntary_exit.rs @@ -9,7 +9,7 @@ use tree_hash_derive::{CachedTreeHash, SignedRoot, TreeHash}; /// An exit voluntarily submitted a validator who wishes to withdraw. /// -/// Spec v0.5.1 +/// Spec v0.6.1 #[derive( Debug, PartialEq, diff --git a/eth2/utils/bls/Cargo.toml b/eth2/utils/bls/Cargo.toml index dcace15c8..4fb1246be 100644 --- a/eth2/utils/bls/Cargo.toml +++ b/eth2/utils/bls/Cargo.toml @@ -5,10 +5,11 @@ authors = ["Paul Hauner "] edition = "2018" [dependencies] -bls-aggregates = { git = "https://github.com/sigp/signature-schemes", tag = "0.6.1" } +milagro_bls = { git = "https://github.com/sigp/milagro_bls", tag = "v0.9.0" } cached_tree_hash = { path = "../cached_tree_hash" } hashing = { path = "../hashing" } hex = "0.3" +rand = "^0.5" serde = "1.0" serde_derive = "1.0" serde_hex = { path = "../serde_hex" } diff --git a/eth2/utils/bls/src/aggregate_public_key.rs b/eth2/utils/bls/src/aggregate_public_key.rs index 2e6ee7882..d36477d76 100644 --- a/eth2/utils/bls/src/aggregate_public_key.rs +++ b/eth2/utils/bls/src/aggregate_public_key.rs @@ -1,5 +1,5 @@ use super::PublicKey; -use bls_aggregates::AggregatePublicKey as RawAggregatePublicKey; +use milagro_bls::AggregatePublicKey as RawAggregatePublicKey; /// A BLS aggregate public key. /// diff --git a/eth2/utils/bls/src/aggregate_signature.rs b/eth2/utils/bls/src/aggregate_signature.rs index 2e4615324..cf7ce78c2 100644 --- a/eth2/utils/bls/src/aggregate_signature.rs +++ b/eth2/utils/bls/src/aggregate_signature.rs @@ -1,8 +1,8 @@ use super::*; -use bls_aggregates::{ +use cached_tree_hash::cached_tree_hash_ssz_encoding_as_vector; +use milagro_bls::{ AggregatePublicKey as RawAggregatePublicKey, AggregateSignature as RawAggregateSignature, }; -use cached_tree_hash::cached_tree_hash_ssz_encoding_as_vector; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use serde_hex::{encode as hex_encode, HexVisitor}; diff --git a/eth2/utils/bls/src/fake_aggregate_public_key.rs b/eth2/utils/bls/src/fake_aggregate_public_key.rs new file mode 100644 index 000000000..80256034a --- /dev/null +++ b/eth2/utils/bls/src/fake_aggregate_public_key.rs @@ -0,0 +1,36 @@ +use super::{PublicKey, BLS_PUBLIC_KEY_BYTE_SIZE}; +use bls_aggregates::AggregatePublicKey as RawAggregatePublicKey; + +/// A BLS aggregate public key. +/// +/// This struct is a wrapper upon a base type and provides helper functions (e.g., SSZ +/// serialization). +#[derive(Debug, Clone, Default)] +pub struct FakeAggregatePublicKey { + bytes: Vec, +} + +impl FakeAggregatePublicKey { + pub fn new() -> Self { + Self::zero() + } + + /// Creates a new all-zero's aggregate public key + pub fn zero() -> Self { + Self { + bytes: vec![0; BLS_PUBLIC_KEY_BYTE_SIZE], + } + } + + pub fn add(&mut self, _public_key: &PublicKey) { + // No nothing. + } + + pub fn as_raw(&self) -> &FakeAggregatePublicKey { + &self + } + + pub fn as_bytes(&self) -> Vec { + self.bytes.clone() + } +} diff --git a/eth2/utils/bls/src/fake_aggregate_signature.rs b/eth2/utils/bls/src/fake_aggregate_signature.rs index 12532d080..709c008aa 100644 --- a/eth2/utils/bls/src/fake_aggregate_signature.rs +++ b/eth2/utils/bls/src/fake_aggregate_signature.rs @@ -1,4 +1,7 @@ -use super::{fake_signature::FakeSignature, AggregatePublicKey, BLS_AGG_SIG_BYTE_SIZE}; +use super::{ + fake_aggregate_public_key::FakeAggregatePublicKey, fake_signature::FakeSignature, + BLS_AGG_SIG_BYTE_SIZE, +}; use cached_tree_hash::cached_tree_hash_ssz_encoding_as_vector; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; @@ -43,7 +46,7 @@ impl FakeAggregateSignature { &self, _msg: &[u8], _domain: u64, - _aggregate_public_key: &AggregatePublicKey, + _aggregate_public_key: &FakeAggregatePublicKey, ) -> bool { true } @@ -53,7 +56,7 @@ impl FakeAggregateSignature { &self, _messages: &[&[u8]], _domain: u64, - _aggregate_public_keys: &[&AggregatePublicKey], + _aggregate_public_keys: &[&FakeAggregatePublicKey], ) -> bool { true } diff --git a/eth2/utils/bls/src/fake_public_key.rs b/eth2/utils/bls/src/fake_public_key.rs new file mode 100644 index 000000000..2c14191c0 --- /dev/null +++ b/eth2/utils/bls/src/fake_public_key.rs @@ -0,0 +1,169 @@ +use super::{SecretKey, BLS_PUBLIC_KEY_BYTE_SIZE}; +use bls_aggregates::PublicKey as RawPublicKey; +use cached_tree_hash::cached_tree_hash_ssz_encoding_as_vector; +use serde::de::{Deserialize, Deserializer}; +use serde::ser::{Serialize, Serializer}; +use serde_hex::{encode as hex_encode, HexVisitor}; +use ssz::{ssz_encode, Decode, DecodeError}; +use std::default; +use std::fmt; +use std::hash::{Hash, Hasher}; +use tree_hash::tree_hash_ssz_encoding_as_vector; + +/// A single BLS signature. +/// +/// This struct is a wrapper upon a base type and provides helper functions (e.g., SSZ +/// serialization). +#[derive(Debug, Clone, Eq)] +pub struct FakePublicKey { + bytes: Vec, +} + +impl FakePublicKey { + pub fn from_secret_key(_secret_key: &SecretKey) -> Self { + Self::zero() + } + + /// Creates a new all-zero's public key + pub fn zero() -> Self { + Self { + bytes: vec![0; BLS_PUBLIC_KEY_BYTE_SIZE], + } + } + + /// Returns the underlying point as compressed bytes. + /// + /// Identical to `self.as_uncompressed_bytes()`. + pub fn as_bytes(&self) -> Vec { + self.bytes.clone() + } + + /// Converts compressed bytes to FakePublicKey + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(Self { + bytes: bytes.to_vec(), + }) + } + + /// Returns the FakePublicKey as (x, y) bytes + pub fn as_uncompressed_bytes(&self) -> Vec { + self.as_bytes() + } + + /// Converts (x, y) bytes to FakePublicKey + pub fn from_uncompressed_bytes(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } + + /// Returns the last 6 bytes of the SSZ encoding of the public key, as a hex string. + /// + /// Useful for providing a short identifier to the user. + pub fn concatenated_hex_id(&self) -> String { + let bytes = ssz_encode(self); + let end_bytes = &bytes[bytes.len().saturating_sub(6)..bytes.len()]; + hex_encode(end_bytes) + } + + // Returns itself + pub fn as_raw(&self) -> &Self { + self + } +} + +impl fmt::Display for FakePublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.concatenated_hex_id()) + } +} + +impl default::Default for FakePublicKey { + fn default() -> Self { + let secret_key = SecretKey::random(); + FakePublicKey::from_secret_key(&secret_key) + } +} + +impl_ssz!(FakePublicKey, BLS_PUBLIC_KEY_BYTE_SIZE, "FakePublicKey"); + +impl Serialize for FakePublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&hex_encode(self.as_bytes())) + } +} + +impl<'de> Deserialize<'de> for FakePublicKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes = deserializer.deserialize_str(HexVisitor)?; + let pubkey = Self::from_ssz_bytes(&bytes[..]) + .map_err(|e| serde::de::Error::custom(format!("invalid pubkey ({:?})", e)))?; + Ok(pubkey) + } +} + +tree_hash_ssz_encoding_as_vector!(FakePublicKey); +cached_tree_hash_ssz_encoding_as_vector!(FakePublicKey, 48); + +impl PartialEq for FakePublicKey { + fn eq(&self, other: &FakePublicKey) -> bool { + ssz_encode(self) == ssz_encode(other) + } +} + +impl Hash for FakePublicKey { + /// Note: this is distinct from consensus serialization, it will produce a different hash. + /// + /// This method uses the uncompressed bytes, which are much faster to obtain than the + /// compressed bytes required for consensus serialization. + /// + /// Use `ssz::Encode` to obtain the bytes required for consensus hashing. + fn hash(&self, state: &mut H) { + self.as_uncompressed_bytes().hash(state) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ssz::ssz_encode; + use tree_hash::TreeHash; + + #[test] + pub fn test_ssz_round_trip() { + let sk = SecretKey::random(); + let original = FakePublicKey::from_secret_key(&sk); + + let bytes = ssz_encode(&original); + let decoded = FakePublicKey::from_ssz_bytes(&bytes).unwrap(); + + assert_eq!(original, decoded); + } + + #[test] + pub fn test_cached_tree_hash() { + let sk = SecretKey::random(); + let original = FakePublicKey::from_secret_key(&sk); + + let mut cache = cached_tree_hash::TreeHashCache::new(&original).unwrap(); + + assert_eq!( + cache.tree_hash_root().unwrap().to_vec(), + original.tree_hash_root() + ); + + let sk = SecretKey::random(); + let modified = FakePublicKey::from_secret_key(&sk); + + cache.update(&modified).unwrap(); + + assert_eq!( + cache.tree_hash_root().unwrap().to_vec(), + modified.tree_hash_root() + ); + } +} diff --git a/eth2/utils/bls/src/lib.rs b/eth2/utils/bls/src/lib.rs index afe655939..e8d71ebeb 100644 --- a/eth2/utils/bls/src/lib.rs +++ b/eth2/utils/bls/src/lib.rs @@ -1,35 +1,52 @@ -extern crate bls_aggregates; +extern crate milagro_bls; extern crate ssz; #[macro_use] mod macros; -mod aggregate_public_key; mod keypair; -mod public_key; mod secret_key; -#[cfg(not(feature = "fake_crypto"))] -mod aggregate_signature; -#[cfg(not(feature = "fake_crypto"))] -mod signature; -#[cfg(not(feature = "fake_crypto"))] -pub use crate::aggregate_signature::AggregateSignature; -#[cfg(not(feature = "fake_crypto"))] -pub use crate::signature::Signature; +pub use crate::keypair::Keypair; +pub use crate::secret_key::SecretKey; +pub use milagro_bls::{compress_g2, hash_on_g2}; +#[cfg(feature = "fake_crypto")] +mod fake_aggregate_public_key; #[cfg(feature = "fake_crypto")] mod fake_aggregate_signature; #[cfg(feature = "fake_crypto")] +mod fake_public_key; +#[cfg(feature = "fake_crypto")] mod fake_signature; -#[cfg(feature = "fake_crypto")] -pub use crate::fake_aggregate_signature::FakeAggregateSignature as AggregateSignature; -#[cfg(feature = "fake_crypto")] -pub use crate::fake_signature::FakeSignature as Signature; -pub use crate::aggregate_public_key::AggregatePublicKey; -pub use crate::keypair::Keypair; -pub use crate::public_key::PublicKey; -pub use crate::secret_key::SecretKey; +#[cfg(not(feature = "fake_crypto"))] +mod aggregate_public_key; +#[cfg(not(feature = "fake_crypto"))] +mod aggregate_signature; +#[cfg(not(feature = "fake_crypto"))] +mod public_key; +#[cfg(not(feature = "fake_crypto"))] +mod signature; + +#[cfg(feature = "fake_crypto")] +pub use fakes::*; +#[cfg(feature = "fake_crypto")] +mod fakes { + pub use crate::fake_aggregate_public_key::FakeAggregatePublicKey as AggregatePublicKey; + pub use crate::fake_aggregate_signature::FakeAggregateSignature as AggregateSignature; + pub use crate::fake_public_key::FakePublicKey as PublicKey; + pub use crate::fake_signature::FakeSignature as Signature; +} + +#[cfg(not(feature = "fake_crypto"))] +pub use reals::*; +#[cfg(not(feature = "fake_crypto"))] +mod reals { + pub use crate::aggregate_public_key::AggregatePublicKey; + pub use crate::aggregate_signature::AggregateSignature; + pub use crate::public_key::PublicKey; + pub use crate::signature::Signature; +} pub const BLS_AGG_SIG_BYTE_SIZE: usize = 96; pub const BLS_SIG_BYTE_SIZE: usize = 96; diff --git a/eth2/utils/bls/src/public_key.rs b/eth2/utils/bls/src/public_key.rs index f72bb7646..09451331d 100644 --- a/eth2/utils/bls/src/public_key.rs +++ b/eth2/utils/bls/src/public_key.rs @@ -1,10 +1,10 @@ use super::{SecretKey, BLS_PUBLIC_KEY_BYTE_SIZE}; -use bls_aggregates::PublicKey as RawPublicKey; use cached_tree_hash::cached_tree_hash_ssz_encoding_as_vector; +use milagro_bls::PublicKey as RawPublicKey; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use serde_hex::{encode as hex_encode, HexVisitor}; -use ssz::{ssz_encode, Decode, DecodeError}; +use ssz::{Decode, DecodeError, Encode}; use std::default; use std::fmt; use std::hash::{Hash, Hasher}; @@ -14,7 +14,7 @@ use tree_hash::tree_hash_ssz_encoding_as_vector; /// /// This struct is a wrapper upon a base type and provides helper functions (e.g., SSZ /// serialization). -#[derive(Debug, Clone, Eq)] +#[derive(Clone, Eq)] pub struct PublicKey(RawPublicKey); impl PublicKey { @@ -60,9 +60,14 @@ impl PublicKey { /// /// Useful for providing a short identifier to the user. pub fn concatenated_hex_id(&self) -> String { - let bytes = ssz_encode(self); - let end_bytes = &bytes[bytes.len().saturating_sub(6)..bytes.len()]; - hex_encode(end_bytes) + self.as_hex_string()[0..6].to_string() + } + + /// Returns the point as a hex string of the SSZ encoding. + /// + /// Note: the string is prefixed with `0x`. + pub fn as_hex_string(&self) -> String { + hex_encode(self.as_ssz_bytes()) } } @@ -72,6 +77,12 @@ impl fmt::Display for PublicKey { } } +impl fmt::Debug for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_hex_string()) + } +} + impl default::Default for PublicKey { fn default() -> Self { let secret_key = SecretKey::random(); @@ -107,7 +118,7 @@ cached_tree_hash_ssz_encoding_as_vector!(PublicKey, 48); impl PartialEq for PublicKey { fn eq(&self, other: &PublicKey) -> bool { - ssz_encode(self) == ssz_encode(other) + self.as_ssz_bytes() == other.as_ssz_bytes() } } diff --git a/eth2/utils/bls/src/secret_key.rs b/eth2/utils/bls/src/secret_key.rs index 620780261..1107c9332 100644 --- a/eth2/utils/bls/src/secret_key.rs +++ b/eth2/utils/bls/src/secret_key.rs @@ -1,6 +1,8 @@ +extern crate rand; + use super::BLS_SECRET_KEY_BYTE_SIZE; -use bls_aggregates::SecretKey as RawSecretKey; use hex::encode as hex_encode; +use milagro_bls::SecretKey as RawSecretKey; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use serde_hex::HexVisitor; @@ -16,7 +18,7 @@ pub struct SecretKey(RawSecretKey); impl SecretKey { pub fn random() -> Self { - SecretKey(RawSecretKey::random()) + SecretKey(RawSecretKey::random(&mut rand::thread_rng())) } /// Returns the underlying point as compressed bytes. diff --git a/eth2/utils/bls/src/signature.rs b/eth2/utils/bls/src/signature.rs index 5ad159828..5009d060c 100644 --- a/eth2/utils/bls/src/signature.rs +++ b/eth2/utils/bls/src/signature.rs @@ -1,7 +1,7 @@ use super::{PublicKey, SecretKey, BLS_SIG_BYTE_SIZE}; -use bls_aggregates::Signature as RawSignature; use cached_tree_hash::cached_tree_hash_ssz_encoding_as_vector; use hex::encode as hex_encode; +use milagro_bls::Signature as RawSignature; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use serde_hex::HexVisitor; diff --git a/eth2/utils/cached_tree_hash/src/impls.rs b/eth2/utils/cached_tree_hash/src/impls.rs index 5105ad6a7..aec61f34b 100644 --- a/eth2/utils/cached_tree_hash/src/impls.rs +++ b/eth2/utils/cached_tree_hash/src/impls.rs @@ -60,29 +60,36 @@ impl CachedTreeHash for bool { } } -impl CachedTreeHash for [u8; 4] { - fn new_tree_hash_cache(&self, _depth: usize) -> Result { - Ok(TreeHashCache::from_bytes( - merkleize(self.to_vec()), - false, - None, - )?) - } +macro_rules! impl_for_u8_array { + ($len: expr) => { + impl CachedTreeHash for [u8; $len] { + fn new_tree_hash_cache(&self, _depth: usize) -> Result { + Ok(TreeHashCache::from_bytes( + merkleize(self.to_vec()), + false, + None, + )?) + } - fn tree_hash_cache_schema(&self, depth: usize) -> BTreeSchema { - BTreeSchema::from_lengths(depth, vec![1]) - } + fn tree_hash_cache_schema(&self, depth: usize) -> BTreeSchema { + BTreeSchema::from_lengths(depth, vec![1]) + } - fn update_tree_hash_cache(&self, cache: &mut TreeHashCache) -> Result<(), Error> { - let leaf = merkleize(self.to_vec()); - cache.maybe_update_chunk(cache.chunk_index, &leaf)?; + fn update_tree_hash_cache(&self, cache: &mut TreeHashCache) -> Result<(), Error> { + let leaf = merkleize(self.to_vec()); + cache.maybe_update_chunk(cache.chunk_index, &leaf)?; - cache.chunk_index += 1; + cache.chunk_index += 1; - Ok(()) - } + Ok(()) + } + } + }; } +impl_for_u8_array!(4); +impl_for_u8_array!(32); + impl CachedTreeHash for H256 { fn new_tree_hash_cache(&self, _depth: usize) -> Result { Ok(TreeHashCache::from_bytes( diff --git a/eth2/utils/compare_fields/Cargo.toml b/eth2/utils/compare_fields/Cargo.toml new file mode 100644 index 000000000..33826c71d --- /dev/null +++ b/eth2/utils/compare_fields/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "compare_fields" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2018" + +[dev-dependencies] +compare_fields_derive = { path = "../compare_fields_derive" } + +[dependencies] diff --git a/eth2/utils/compare_fields/src/lib.rs b/eth2/utils/compare_fields/src/lib.rs new file mode 100644 index 000000000..a0166eb50 --- /dev/null +++ b/eth2/utils/compare_fields/src/lib.rs @@ -0,0 +1,179 @@ +//! Provides field-by-field comparisons for structs and vecs. +//! +//! Returns comparisons as data, without making assumptions about the desired equality (e.g., +//! does not `panic!` on inequality). +//! +//! Note: `compare_fields_derive` requires `PartialEq` and `Debug` implementations. +//! +//! ## Example +//! +//! ```rust +//! use compare_fields::{CompareFields, Comparison, FieldComparison}; +//! use compare_fields_derive::CompareFields; +//! +//! #[derive(PartialEq, Debug, CompareFields)] +//! pub struct Bar { +//! a: u64, +//! b: u16, +//! #[compare_fields(as_slice)] +//! c: Vec +//! } +//! +//! #[derive(Clone, PartialEq, Debug, CompareFields)] +//! pub struct Foo { +//! d: String +//! } +//! +//! let cat = Foo {d: "cat".to_string()}; +//! let dog = Foo {d: "dog".to_string()}; +//! let chicken = Foo {d: "chicken".to_string()}; +//! +//! let mut bar_a = Bar { +//! a: 42, +//! b: 12, +//! c: vec![ cat.clone(), dog.clone() ], +//! }; +//! +//! let mut bar_b = Bar { +//! a: 42, +//! b: 99, +//! c: vec![ chicken.clone(), dog.clone()] +//! }; +//! +//! let cat_dog = Comparison::Child(FieldComparison { +//! field_name: "d".to_string(), +//! equal: false, +//! a: "\"cat\"".to_string(), +//! b: "\"dog\"".to_string(), +//! }); +//! assert_eq!(cat.compare_fields(&dog), vec![cat_dog]); +//! +//! let bar_a_b = vec![ +//! Comparison::Child(FieldComparison { +//! field_name: "a".to_string(), +//! equal: true, +//! a: "42".to_string(), +//! b: "42".to_string(), +//! }), +//! Comparison::Child(FieldComparison { +//! field_name: "b".to_string(), +//! equal: false, +//! a: "12".to_string(), +//! b: "99".to_string(), +//! }), +//! Comparison::Parent{ +//! field_name: "c".to_string(), +//! equal: false, +//! children: vec![ +//! FieldComparison { +//! field_name: "0".to_string(), +//! equal: false, +//! a: "Some(Foo { d: \"cat\" })".to_string(), +//! b: "Some(Foo { d: \"chicken\" })".to_string(), +//! }, +//! FieldComparison { +//! field_name: "1".to_string(), +//! equal: true, +//! a: "Some(Foo { d: \"dog\" })".to_string(), +//! b: "Some(Foo { d: \"dog\" })".to_string(), +//! } +//! ] +//! } +//! ]; +//! assert_eq!(bar_a.compare_fields(&bar_b), bar_a_b); +//! +//! +//! +//! // TODO: +//! ``` +use std::fmt::Debug; + +#[derive(Debug, PartialEq, Clone)] +pub enum Comparison { + Child(FieldComparison), + Parent { + field_name: String, + equal: bool, + children: Vec, + }, +} + +impl Comparison { + pub fn child>(field_name: String, a: &T, b: &T) -> Self { + Comparison::Child(FieldComparison::new(field_name, a, b)) + } + + pub fn parent(field_name: String, equal: bool, children: Vec) -> Self { + Comparison::Parent { + field_name, + equal, + children, + } + } + + pub fn from_slice>(field_name: String, a: &[T], b: &[T]) -> Self { + let mut children = vec![]; + + for i in 0..std::cmp::max(a.len(), b.len()) { + children.push(FieldComparison::new( + format!("{:}", i), + &a.get(i), + &b.get(i), + )); + } + + Self::parent(field_name, a == b, children) + } + + pub fn retain_children(&mut self, f: F) + where + F: FnMut(&FieldComparison) -> bool, + { + match self { + Comparison::Child(_) => (), + Comparison::Parent { children, .. } => children.retain(f), + } + } + + pub fn equal(&self) -> bool { + match self { + Comparison::Child(fc) => fc.equal, + Comparison::Parent { equal, .. } => *equal, + } + } + + pub fn not_equal(&self) -> bool { + !self.equal() + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct FieldComparison { + pub field_name: String, + pub equal: bool, + pub a: String, + pub b: String, +} + +pub trait CompareFields { + fn compare_fields(&self, b: &Self) -> Vec; +} + +impl FieldComparison { + pub fn new>(field_name: String, a: &T, b: &T) -> Self { + Self { + field_name, + equal: a == b, + a: format!("{:?}", a), + b: format!("{:?}", b), + } + } + + pub fn equal(&self) -> bool { + self.equal + } + + pub fn not_equal(&self) -> bool { + !self.equal() + } +} diff --git a/eth2/utils/compare_fields_derive/Cargo.toml b/eth2/utils/compare_fields_derive/Cargo.toml new file mode 100644 index 000000000..8832e26d3 --- /dev/null +++ b/eth2/utils/compare_fields_derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "compare_fields_derive" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +syn = "0.15" +quote = "0.6" diff --git a/eth2/utils/compare_fields_derive/src/lib.rs b/eth2/utils/compare_fields_derive/src/lib.rs new file mode 100644 index 000000000..c4ca3d64c --- /dev/null +++ b/eth2/utils/compare_fields_derive/src/lib.rs @@ -0,0 +1,77 @@ +#![recursion_limit = "256"] +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +fn is_slice(field: &syn::Field) -> bool { + for attr in &field.attrs { + if attr.tts.to_string() == "( as_slice )" { + return true; + } + } + false +} + +#[proc_macro_derive(CompareFields, attributes(compare_fields))] +pub fn compare_fields_derive(input: TokenStream) -> TokenStream { + let item = parse_macro_input!(input as DeriveInput); + + let name = &item.ident; + let (impl_generics, ty_generics, where_clause) = &item.generics.split_for_impl(); + + let struct_data = match &item.data { + syn::Data::Struct(s) => s, + _ => panic!("compare_fields_derive only supports structs."), + }; + + let mut quotes = vec![]; + + for field in struct_data.fields.iter() { + let ident_a = match &field.ident { + Some(ref ident) => ident, + _ => panic!("compare_fields_derive only supports named struct fields."), + }; + + let field_name = format!("{:}", ident_a); + let ident_b = ident_a.clone(); + + let quote = if is_slice(field) { + quote! { + comparisons.push(compare_fields::Comparison::from_slice( + #field_name.to_string(), + &self.#ident_a, + &b.#ident_b) + ); + } + } else { + quote! { + comparisons.push( + compare_fields::Comparison::child( + #field_name.to_string(), + &self.#ident_a, + &b.#ident_b + ) + ); + } + }; + + quotes.push(quote); + } + + let output = quote! { + impl #impl_generics compare_fields::CompareFields for #name #ty_generics #where_clause { + fn compare_fields(&self, b: &Self) -> Vec { + let mut comparisons = vec![]; + + #( + #quotes + )* + + comparisons + } + } + }; + output.into() +} diff --git a/eth2/utils/fixed_len_vec/src/lib.rs b/eth2/utils/fixed_len_vec/src/lib.rs index 2976ee4e4..b8a3292bd 100644 --- a/eth2/utils/fixed_len_vec/src/lib.rs +++ b/eth2/utils/fixed_len_vec/src/lib.rs @@ -1,6 +1,6 @@ use serde_derive::{Deserialize, Serialize}; use std::marker::PhantomData; -use std::ops::{Index, IndexMut}; +use std::ops::{Deref, Index, IndexMut}; use std::slice::SliceIndex; use typenum::Unsigned; @@ -9,6 +9,7 @@ pub use typenum; mod impls; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(transparent)] pub struct FixedLenVec { vec: Vec, _phantom: PhantomData, @@ -70,6 +71,14 @@ impl> IndexMut for FixedLenVec { } } +impl Deref for FixedLenVec { + type Target = [T]; + + fn deref(&self) -> &[T] { + &self.vec[..] + } +} + #[cfg(test)] mod test { use super::*; @@ -104,6 +113,16 @@ mod test { let fixed: FixedLenVec = FixedLenVec::from(vec.clone()); assert_eq!(&fixed[..], &vec![0, 0, 0, 0][..]); } + + #[test] + fn deref() { + let vec = vec![0, 2, 4, 6]; + let fixed: FixedLenVec = FixedLenVec::from(vec); + + assert_eq!(fixed.get(0), Some(&0)); + assert_eq!(fixed.get(3), Some(&6)); + assert_eq!(fixed.get(4), None); + } } #[cfg(test)] diff --git a/eth2/utils/hashing/Cargo.toml b/eth2/utils/hashing/Cargo.toml index 1527bceba..78dd70e43 100644 --- a/eth2/utils/hashing/Cargo.toml +++ b/eth2/utils/hashing/Cargo.toml @@ -5,4 +5,4 @@ authors = ["Paul Hauner "] edition = "2018" [dependencies] -tiny-keccak = "1.4.2" +ring = "0.14.6" diff --git a/eth2/utils/hashing/src/lib.rs b/eth2/utils/hashing/src/lib.rs index d1d2c28d9..a9e286c39 100644 --- a/eth2/utils/hashing/src/lib.rs +++ b/eth2/utils/hashing/src/lib.rs @@ -1,11 +1,7 @@ -use tiny_keccak::Keccak; +use ring::digest::{digest, SHA256}; pub fn hash(input: &[u8]) -> Vec { - let mut keccak = Keccak::new_keccak256(); - keccak.update(input); - let mut result = vec![0; 32]; - keccak.finalize(result.as_mut_slice()); - result + digest(&SHA256, input).as_ref().into() } /// Get merkle root of some hashed values - the input leaf nodes is expected to already be hashed @@ -41,19 +37,16 @@ pub fn merkle_root(values: &[Vec]) -> Option> { #[cfg(test)] mod tests { use super::*; - use std::convert::From; + use ring::test; #[test] fn test_hashing() { - let input: Vec = From::from("hello"); + let input: Vec = b"hello world".as_ref().into(); let output = hash(input.as_ref()); - let expected = &[ - 0x1c, 0x8a, 0xff, 0x95, 0x06, 0x85, 0xc2, 0xed, 0x4b, 0xc3, 0x17, 0x4f, 0x34, 0x72, - 0x28, 0x7b, 0x56, 0xd9, 0x51, 0x7b, 0x9c, 0x94, 0x81, 0x27, 0x31, 0x9a, 0x09, 0xa7, - 0xa3, 0x6d, 0xea, 0xc8, - ]; - assert_eq!(expected, output.as_slice()); + let expected_hex = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"; + let expected: Vec = test::from_hex(expected_hex).unwrap(); + assert_eq!(expected, output); } #[test] diff --git a/eth2/utils/honey-badger-split/src/lib.rs b/eth2/utils/honey-badger-split/src/lib.rs index b7097584f..ca02b5c01 100644 --- a/eth2/utils/honey-badger-split/src/lib.rs +++ b/eth2/utils/honey-badger-split/src/lib.rs @@ -52,6 +52,38 @@ impl SplitExt for [T] { mod tests { use super::*; + fn alternative_split_at_index(indices: &[T], index: usize, count: usize) -> &[T] { + let start = (indices.len() * index) / count; + let end = (indices.len() * (index + 1)) / count; + + &indices[start..end] + } + + fn alternative_split(input: &[T], n: usize) -> Vec<&[T]> { + (0..n) + .into_iter() + .map(|i| alternative_split_at_index(&input, i, n)) + .collect() + } + + fn honey_badger_vs_alternative_fn(num_items: usize, num_chunks: usize) { + let input: Vec = (0..num_items).collect(); + + let hb: Vec<&[usize]> = input.honey_badger_split(num_chunks).collect(); + let spec: Vec<&[usize]> = alternative_split(&input, num_chunks); + + assert_eq!(hb, spec); + } + + #[test] + fn vs_eth_spec_fn() { + for i in 0..10 { + for j in 0..10 { + honey_badger_vs_alternative_fn(i, j); + } + } + } + #[test] fn test_honey_badger_split() { /* diff --git a/eth2/utils/ssz/src/decode/impls.rs b/eth2/utils/ssz/src/decode/impls.rs index 213f19bf5..3e567b967 100644 --- a/eth2/utils/ssz/src/decode/impls.rs +++ b/eth2/utils/ssz/src/decode/impls.rs @@ -1,5 +1,5 @@ use super::*; -use ethereum_types::H256; +use ethereum_types::{H256, U128, U256}; macro_rules! impl_decodable_for_uint { ($type: ident, $bit_size: expr) => { @@ -83,6 +83,48 @@ impl Decode for H256 { } } +impl Decode for U256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(U256::from_little_endian(bytes)) + } + } +} + +impl Decode for U128 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 16 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let len = bytes.len(); + let expected = ::ssz_fixed_len(); + + if len != expected { + Err(DecodeError::InvalidByteLength { len, expected }) + } else { + Ok(U128::from_little_endian(bytes)) + } + } +} + macro_rules! impl_decodable_for_u8_array { ($len: expr) => { impl Decode for [u8; $len] { @@ -112,6 +154,7 @@ macro_rules! impl_decodable_for_u8_array { } impl_decodable_for_u8_array!(4); +impl_decodable_for_u8_array!(32); impl Decode for Vec { fn is_ssz_fixed_len() -> bool { diff --git a/eth2/utils/ssz/src/encode/impls.rs b/eth2/utils/ssz/src/encode/impls.rs index 07886d68f..0d6891c5e 100644 --- a/eth2/utils/ssz/src/encode/impls.rs +++ b/eth2/utils/ssz/src/encode/impls.rs @@ -1,5 +1,5 @@ use super::*; -use ethereum_types::H256; +use ethereum_types::{H256, U128, U256}; macro_rules! impl_encodable_for_uint { ($type: ident, $bit_size: expr) => { @@ -77,6 +77,42 @@ impl Encode for H256 { } } +impl Encode for U256 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 32 + } + + fn ssz_append(&self, buf: &mut Vec) { + let n = ::ssz_fixed_len(); + let s = buf.len(); + + buf.resize(s + n, 0); + self.to_little_endian(&mut buf[s..]); + } +} + +impl Encode for U128 { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 16 + } + + fn ssz_append(&self, buf: &mut Vec) { + let n = ::ssz_fixed_len(); + let s = buf.len(); + + buf.resize(s + n, 0); + self.to_little_endian(&mut buf[s..]); + } +} + macro_rules! impl_encodable_for_u8_array { ($len: expr) => { impl Encode for [u8; $len] { @@ -96,6 +132,7 @@ macro_rules! impl_encodable_for_u8_array { } impl_encodable_for_u8_array!(4); +impl_encodable_for_u8_array!(32); #[cfg(test)] mod tests { diff --git a/eth2/utils/swap_or_not_shuffle/Cargo.toml b/eth2/utils/swap_or_not_shuffle/Cargo.toml index 3a866da92..19d5444fb 100644 --- a/eth2/utils/swap_or_not_shuffle/Cargo.toml +++ b/eth2/utils/swap_or_not_shuffle/Cargo.toml @@ -15,6 +15,5 @@ hex = "0.3" ethereum-types = "0.5" [dependencies] -bytes = "0.4" hashing = { path = "../hashing" } int_to_bytes = { path = "../int_to_bytes" } diff --git a/eth2/utils/swap_or_not_shuffle/src/get_permutated_index.rs b/eth2/utils/swap_or_not_shuffle/src/get_permutated_index.rs index 37a82341e..8c6296180 100644 --- a/eth2/utils/swap_or_not_shuffle/src/get_permutated_index.rs +++ b/eth2/utils/swap_or_not_shuffle/src/get_permutated_index.rs @@ -1,8 +1,6 @@ -use bytes::Buf; use hashing::hash; use int_to_bytes::{int_to_bytes1, int_to_bytes4}; use std::cmp::max; -use std::io::Cursor; /// Return `p(index)` in a pseudorandom permutation `p` of `0...list_size-1` with ``seed`` as entropy. /// @@ -43,7 +41,7 @@ pub fn get_permutated_index( } fn do_round(seed: &[u8], index: usize, pivot: usize, round: u8, list_size: usize) -> Option { - let flip = (pivot + list_size - index) % list_size; + let flip = (pivot + (list_size - index)) % list_size; let position = max(index, flip); let source = hash_with_round_and_position(seed, round, position)?; let byte = source[(position % 256) / 8]; @@ -68,9 +66,10 @@ fn hash_with_round(seed: &[u8], round: u8) -> Vec { hash(&seed[..]) } -fn bytes_to_int64(bytes: &[u8]) -> u64 { - let mut cursor = Cursor::new(bytes); - cursor.get_u64_le() +fn bytes_to_int64(slice: &[u8]) -> u64 { + let mut bytes = [0; 8]; + bytes.copy_from_slice(&slice[0..8]); + u64::from_le_bytes(bytes) } #[cfg(test)] diff --git a/eth2/utils/swap_or_not_shuffle/src/shuffle_list.rs b/eth2/utils/swap_or_not_shuffle/src/shuffle_list.rs index f60d793f2..96e100def 100644 --- a/eth2/utils/swap_or_not_shuffle/src/shuffle_list.rs +++ b/eth2/utils/swap_or_not_shuffle/src/shuffle_list.rs @@ -1,7 +1,5 @@ -use bytes::Buf; use hashing::hash; use int_to_bytes::int_to_bytes4; -use std::io::Cursor; const SEED_SIZE: usize = 32; const ROUND_SIZE: usize = 1; @@ -117,9 +115,10 @@ pub fn shuffle_list( Some(input) } -fn bytes_to_int64(bytes: &[u8]) -> u64 { - let mut cursor = Cursor::new(bytes); - cursor.get_u64_le() +fn bytes_to_int64(slice: &[u8]) -> u64 { + let mut bytes = [0; 8]; + bytes.copy_from_slice(&slice[0..8]); + u64::from_le_bytes(bytes) } #[cfg(test)] diff --git a/eth2/utils/tree_hash/src/impls.rs b/eth2/utils/tree_hash/src/impls.rs index 42ea9add0..212aba3c8 100644 --- a/eth2/utils/tree_hash/src/impls.rs +++ b/eth2/utils/tree_hash/src/impls.rs @@ -51,24 +51,31 @@ impl TreeHash for bool { } } -impl TreeHash for [u8; 4] { - fn tree_hash_type() -> TreeHashType { - TreeHashType::Vector - } +macro_rules! impl_for_u8_array { + ($len: expr) => { + impl TreeHash for [u8; $len] { + fn tree_hash_type() -> TreeHashType { + TreeHashType::Vector + } - fn tree_hash_packed_encoding(&self) -> Vec { - unreachable!("bytesN should never be packed.") - } + fn tree_hash_packed_encoding(&self) -> Vec { + unreachable!("bytesN should never be packed.") + } - fn tree_hash_packing_factor() -> usize { - unreachable!("bytesN should never be packed.") - } + fn tree_hash_packing_factor() -> usize { + unreachable!("bytesN should never be packed.") + } - fn tree_hash_root(&self) -> Vec { - merkle_root(&self[..]) - } + fn tree_hash_root(&self) -> Vec { + merkle_root(&self[..]) + } + } + }; } +impl_for_u8_array!(4); +impl_for_u8_array!(32); + impl TreeHash for H256 { fn tree_hash_type() -> TreeHashType { TreeHashType::Vector diff --git a/tests/ef_tests/Cargo.toml b/tests/ef_tests/Cargo.toml new file mode 100644 index 000000000..b0a5e27c5 --- /dev/null +++ b/tests/ef_tests/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ef_tests" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2018" + +[features] +fake_crypto = ["bls/fake_crypto"] + +[dependencies] +bls = { path = "../../eth2/utils/bls" } +compare_fields = { path = "../../eth2/utils/compare_fields" } +ethereum-types = "0.5" +hex = "0.3" +rayon = "1.0" +serde = "1.0" +serde_derive = "1.0" +serde_yaml = "0.8" +ssz = { path = "../../eth2/utils/ssz" } +tree_hash = { path = "../../eth2/utils/tree_hash" } +cached_tree_hash = { path = "../../eth2/utils/cached_tree_hash" } +state_processing = { path = "../../eth2/state_processing" } +swap_or_not_shuffle = { path = "../../eth2/utils/swap_or_not_shuffle" } +types = { path = "../../eth2/types" } +walkdir = "2" +yaml-rust = { git = "https://github.com/sigp/yaml-rust", branch = "escape_all_str"} diff --git a/tests/ef_tests/eth2.0-spec-tests b/tests/ef_tests/eth2.0-spec-tests new file mode 160000 index 000000000..cfc6e3c91 --- /dev/null +++ b/tests/ef_tests/eth2.0-spec-tests @@ -0,0 +1 @@ +Subproject commit cfc6e3c91d781f0dfe493ceb548391f5e88e54c7 diff --git a/tests/ef_tests/src/case_result.rs b/tests/ef_tests/src/case_result.rs new file mode 100644 index 000000000..cd40ac8ce --- /dev/null +++ b/tests/ef_tests/src/case_result.rs @@ -0,0 +1,117 @@ +use super::*; +use compare_fields::{CompareFields, Comparison, FieldComparison}; +use std::fmt::Debug; +use types::BeaconState; + +pub const MAX_VALUE_STRING_LEN: usize = 500; + +#[derive(Debug, PartialEq, Clone)] +pub struct CaseResult { + pub case_index: usize, + pub desc: String, + pub result: Result<(), Error>, +} + +impl CaseResult { + pub fn new(case_index: usize, case: &impl Case, result: Result<(), Error>) -> Self { + CaseResult { + case_index, + desc: case.description(), + result, + } + } +} + +/// Same as `compare_result_detailed`, however it drops the caches on both states before +/// comparison. +pub fn compare_beacon_state_results_without_caches( + result: &mut Result, E>, + expected: &mut Option>, +) -> Result<(), Error> { + match (result.as_mut(), expected.as_mut()) { + (Ok(ref mut result), Some(ref mut expected)) => { + result.drop_all_caches(); + expected.drop_all_caches(); + } + _ => (), + }; + + compare_result_detailed(&result, &expected) +} + +/// Same as `compare_result`, however utilizes the `CompareFields` trait to give a list of +/// mismatching fields when `Ok(result) != Some(expected)`. +pub fn compare_result_detailed( + result: &Result, + expected: &Option, +) -> Result<(), Error> +where + T: PartialEq + Debug + CompareFields, + E: Debug, +{ + match (result, expected) { + (Ok(result), Some(expected)) => { + let mut mismatching_fields: Vec = expected + .compare_fields(result) + .into_iter() + // Filter all out all fields that are equal. + .filter(Comparison::not_equal) + .collect(); + + mismatching_fields + .iter_mut() + .for_each(|f| f.retain_children(FieldComparison::not_equal)); + + if !mismatching_fields.is_empty() { + Err(Error::NotEqual(format!( + "Fields not equal (a = expected, b = result): {:#?}", + mismatching_fields + ))) + } else { + Ok(()) + } + } + _ => compare_result(result, expected), + } +} + +/// Compares `result` with `expected`. +/// +/// If `expected.is_none()` then `result` is expected to be `Err`. Otherwise, `T` in `result` and +/// `expected` must be equal. +pub fn compare_result(result: &Result, expected: &Option) -> Result<(), Error> +where + T: PartialEq + Debug, + E: Debug, +{ + match (result, expected) { + // Pass: The should have failed and did fail. + (Err(_), None) => Ok(()), + // Fail: The test failed when it should have produced a result (fail). + (Err(e), Some(expected)) => Err(Error::NotEqual(format!( + "Got {:?} | Expected {:?}", + e, + fmt_val(expected) + ))), + // Fail: The test produced a result when it should have failed (fail). + (Ok(result), None) => Err(Error::DidntFail(format!("Got {:?}", fmt_val(result)))), + // Potential Pass: The test should have produced a result, and it did. + (Ok(result), Some(expected)) => { + if result == expected { + Ok(()) + } else { + Err(Error::NotEqual(format!( + "Got {:?} | Expected {:?}", + fmt_val(result), + fmt_val(expected) + ))) + } + } + } +} + +fn fmt_val(val: T) -> String { + let mut string = format!("{:?}", val); + string.truncate(MAX_VALUE_STRING_LEN); + string +} diff --git a/tests/ef_tests/src/cases.rs b/tests/ef_tests/src/cases.rs new file mode 100644 index 000000000..df1a9428b --- /dev/null +++ b/tests/ef_tests/src/cases.rs @@ -0,0 +1,102 @@ +use super::*; +use std::fmt::Debug; + +mod bls_aggregate_pubkeys; +mod bls_aggregate_sigs; +mod bls_g2_compressed; +mod bls_g2_uncompressed; +mod bls_priv_to_pub; +mod bls_sign_msg; +mod epoch_processing_crosslinks; +mod epoch_processing_registry_updates; +mod operations_attester_slashing; +mod operations_deposit; +mod operations_exit; +mod operations_proposer_slashing; +mod operations_transfer; +mod shuffling; +mod ssz_generic; +mod ssz_static; + +pub use bls_aggregate_pubkeys::*; +pub use bls_aggregate_sigs::*; +pub use bls_g2_compressed::*; +pub use bls_g2_uncompressed::*; +pub use bls_priv_to_pub::*; +pub use bls_sign_msg::*; +pub use epoch_processing_crosslinks::*; +pub use epoch_processing_registry_updates::*; +pub use operations_attester_slashing::*; +pub use operations_deposit::*; +pub use operations_exit::*; +pub use operations_proposer_slashing::*; +pub use operations_transfer::*; +pub use shuffling::*; +pub use ssz_generic::*; +pub use ssz_static::*; + +pub trait Case: Debug { + /// An optional field for implementing a custom description. + /// + /// Defaults to "no description". + fn description(&self) -> String { + "no description".to_string() + } + + /// Execute a test and return the result. + /// + /// `case_index` reports the index of the case in the set of test cases. It is not strictly + /// necessary, but it's useful when troubleshooting specific failing tests. + fn result(&self, case_index: usize) -> Result<(), Error>; +} + +#[derive(Debug)] +pub struct Cases { + pub test_cases: Vec, +} + +impl EfTest for Cases +where + T: Case + Debug, +{ + fn test_results(&self) -> Vec { + self.test_cases + .iter() + .enumerate() + .map(|(i, tc)| CaseResult::new(i, tc, tc.result(i))) + .collect() + } +} + +impl YamlDecode for Cases { + /// Decodes a YAML list of test cases + fn yaml_decode(yaml: &String) -> Result { + let mut p = 0; + let mut elems: Vec<&str> = yaml + .match_indices("\n- ") + // Skip the `\n` used for matching a new line + .map(|(i, _)| i + 1) + .map(|i| { + let yaml_element = &yaml[p..i]; + p = i; + + yaml_element + }) + .collect(); + + elems.push(&yaml[p..]); + + let test_cases = elems + .iter() + .map(|s| { + // Remove the `- ` prefix. + let s = &s[2..]; + // Remove a single level of indenting. + s.replace("\n ", "\n") + }) + .map(|s| T::yaml_decode(&s.to_string()).unwrap()) + .collect(); + + Ok(Self { test_cases }) + } +} diff --git a/tests/ef_tests/src/cases/bls_aggregate_pubkeys.rs b/tests/ef_tests/src/cases/bls_aggregate_pubkeys.rs new file mode 100644 index 000000000..6cd37ec36 --- /dev/null +++ b/tests/ef_tests/src/cases/bls_aggregate_pubkeys.rs @@ -0,0 +1,39 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{AggregatePublicKey, PublicKey}; +use serde_derive::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsAggregatePubkeys { + pub input: Vec, + pub output: String, +} + +impl YamlDecode for BlsAggregatePubkeys { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for BlsAggregatePubkeys { + fn result(&self, _case_index: usize) -> Result<(), Error> { + let mut aggregate_pubkey = AggregatePublicKey::new(); + + for key_str in &self.input { + let key = hex::decode(&key_str[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let key = PublicKey::from_bytes(&key) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + aggregate_pubkey.add(&key); + } + + let output_bytes = Some( + hex::decode(&self.output[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?, + ); + let aggregate_pubkey = Ok(aggregate_pubkey.as_raw().as_bytes()); + + compare_result::, Vec>(&aggregate_pubkey, &output_bytes) + } +} diff --git a/tests/ef_tests/src/cases/bls_aggregate_sigs.rs b/tests/ef_tests/src/cases/bls_aggregate_sigs.rs new file mode 100644 index 000000000..5b69a6134 --- /dev/null +++ b/tests/ef_tests/src/cases/bls_aggregate_sigs.rs @@ -0,0 +1,39 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{AggregateSignature, Signature}; +use serde_derive::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsAggregateSigs { + pub input: Vec, + pub output: String, +} + +impl YamlDecode for BlsAggregateSigs { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for BlsAggregateSigs { + fn result(&self, _case_index: usize) -> Result<(), Error> { + let mut aggregate_signature = AggregateSignature::new(); + + for key_str in &self.input { + let sig = hex::decode(&key_str[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let sig = Signature::from_bytes(&sig) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + aggregate_signature.add(&sig); + } + + let output_bytes = Some( + hex::decode(&self.output[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?, + ); + let aggregate_signature = Ok(aggregate_signature.as_bytes()); + + compare_result::, Vec>(&aggregate_signature, &output_bytes) + } +} diff --git a/tests/ef_tests/src/cases/bls_g2_compressed.rs b/tests/ef_tests/src/cases/bls_g2_compressed.rs new file mode 100644 index 000000000..e6895ca1a --- /dev/null +++ b/tests/ef_tests/src/cases/bls_g2_compressed.rs @@ -0,0 +1,58 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{compress_g2, hash_on_g2}; +use serde_derive::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsG2CompressedInput { + pub message: String, + pub domain: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsG2Compressed { + pub input: BlsG2CompressedInput, + pub output: Vec, +} + +impl YamlDecode for BlsG2Compressed { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for BlsG2Compressed { + fn result(&self, _case_index: usize) -> Result<(), Error> { + // Convert message and domain to required types + let msg = hex::decode(&self.input.message[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = hex::decode(&self.input.domain[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = bytes_to_u64(&d); + + // Calculate the point and convert it to compressed bytes + let mut point = hash_on_g2(&msg, d); + let point = compress_g2(&mut point); + + // Convert the output to one set of bytes + let mut decoded = hex::decode(&self.output[0][2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let mut decoded_y = hex::decode(&self.output[1][2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + decoded.append(&mut decoded_y); + + compare_result::, Vec>(&Ok(point), &Some(decoded)) + } +} + +// Converts a vector to u64 (from big endian) +fn bytes_to_u64(array: &Vec) -> u64 { + let mut result: u64 = 0; + for (i, value) in array.iter().rev().enumerate() { + if i == 8 { + break; + } + result += u64::pow(2, i as u32 * 8) * (*value as u64); + } + result +} diff --git a/tests/ef_tests/src/cases/bls_g2_uncompressed.rs b/tests/ef_tests/src/cases/bls_g2_uncompressed.rs new file mode 100644 index 000000000..93b1e1c51 --- /dev/null +++ b/tests/ef_tests/src/cases/bls_g2_uncompressed.rs @@ -0,0 +1,68 @@ +use super::*; +use crate::case_result::compare_result; +use bls::hash_on_g2; +use serde_derive::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsG2UncompressedInput { + pub message: String, + pub domain: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsG2Uncompressed { + pub input: BlsG2UncompressedInput, + pub output: Vec>, +} + +impl YamlDecode for BlsG2Uncompressed { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for BlsG2Uncompressed { + fn result(&self, _case_index: usize) -> Result<(), Error> { + // Convert message and domain to required types + let msg = hex::decode(&self.input.message[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = hex::decode(&self.input.domain[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = bytes_to_u64(&d); + + // Calculate the point and convert it to compressed bytes + let point = hash_on_g2(&msg, d); + let mut point_bytes = [0 as u8; 288]; + point.getpx().geta().tobytearray(&mut point_bytes, 0); + point.getpx().getb().tobytearray(&mut point_bytes, 48); + point.getpy().geta().tobytearray(&mut point_bytes, 96); + point.getpy().getb().tobytearray(&mut point_bytes, 144); + point.getpz().geta().tobytearray(&mut point_bytes, 192); + point.getpz().getb().tobytearray(&mut point_bytes, 240); + + // Convert the output to one set of bytes (x.a, x.b, y.a, y.b, z.a, z.b) + let mut decoded: Vec = vec![]; + for coordinate in &self.output { + let mut decoded_part = hex::decode(&coordinate[0][2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + decoded.append(&mut decoded_part); + decoded_part = hex::decode(&coordinate[1][2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + decoded.append(&mut decoded_part); + } + + compare_result::, Vec>(&Ok(point_bytes.to_vec()), &Some(decoded)) + } +} + +// Converts a vector to u64 (from big endian) +fn bytes_to_u64(array: &Vec) -> u64 { + let mut result: u64 = 0; + for (i, value) in array.iter().rev().enumerate() { + if i == 8 { + break; + } + result += u64::pow(2, i as u32 * 8) * (*value as u64); + } + result +} diff --git a/tests/ef_tests/src/cases/bls_priv_to_pub.rs b/tests/ef_tests/src/cases/bls_priv_to_pub.rs new file mode 100644 index 000000000..c558d0142 --- /dev/null +++ b/tests/ef_tests/src/cases/bls_priv_to_pub.rs @@ -0,0 +1,41 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{PublicKey, SecretKey}; +use serde_derive::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsPrivToPub { + pub input: String, + pub output: String, +} + +impl YamlDecode for BlsPrivToPub { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for BlsPrivToPub { + fn result(&self, _case_index: usize) -> Result<(), Error> { + let secret = &self.input; + + // Convert message and domain to required types + let mut sk = + hex::decode(&secret[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + pad_to_48(&mut sk); + let sk = SecretKey::from_bytes(&sk).unwrap(); + let pk = PublicKey::from_secret_key(&sk); + + let decoded = hex::decode(&self.output[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + compare_result::, Vec>(&Ok(pk.as_raw().as_bytes()), &Some(decoded)) + } +} + +// Increase the size of an array to 48 bytes +fn pad_to_48(array: &mut Vec) { + while array.len() < 48 { + array.insert(0, 0); + } +} diff --git a/tests/ef_tests/src/cases/bls_sign_msg.rs b/tests/ef_tests/src/cases/bls_sign_msg.rs new file mode 100644 index 000000000..a361f2523 --- /dev/null +++ b/tests/ef_tests/src/cases/bls_sign_msg.rs @@ -0,0 +1,65 @@ +use super::*; +use crate::case_result::compare_result; +use bls::{SecretKey, Signature}; +use serde_derive::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsSignInput { + pub privkey: String, + pub message: String, + pub domain: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BlsSign { + pub input: BlsSignInput, + pub output: String, +} + +impl YamlDecode for BlsSign { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for BlsSign { + fn result(&self, _case_index: usize) -> Result<(), Error> { + // Convert private_key, message and domain to required types + let mut sk = hex::decode(&self.input.privkey[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + pad_to_48(&mut sk); + let sk = SecretKey::from_bytes(&sk).unwrap(); + let msg = hex::decode(&self.input.message[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = hex::decode(&self.input.domain[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let d = bytes_to_u64(&d); + + let signature = Signature::new(&msg, d, &sk); + + // Convert the output to one set of bytes + let decoded = hex::decode(&self.output[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + compare_result::, Vec>(&Ok(signature.as_bytes()), &Some(decoded)) + } +} + +// Converts a vector to u64 (from big endian) +fn bytes_to_u64(array: &Vec) -> u64 { + let mut result: u64 = 0; + for (i, value) in array.iter().rev().enumerate() { + if i == 8 { + break; + } + result += u64::pow(2, i as u32 * 8) * (*value as u64); + } + result +} + +// Increase the size of an array to 48 bytes +fn pad_to_48(array: &mut Vec) { + while array.len() < 48 { + array.insert(0, 0); + } +} diff --git a/tests/ef_tests/src/cases/epoch_processing_crosslinks.rs b/tests/ef_tests/src/cases/epoch_processing_crosslinks.rs new file mode 100644 index 000000000..fa530f9ad --- /dev/null +++ b/tests/ef_tests/src/cases/epoch_processing_crosslinks.rs @@ -0,0 +1,38 @@ +use super::*; +use crate::case_result::compare_beacon_state_results_without_caches; +use serde_derive::Deserialize; +use state_processing::per_epoch_processing::process_crosslinks; +use types::{BeaconState, EthSpec}; + +#[derive(Debug, Clone, Deserialize)] +pub struct EpochProcessingCrosslinks { + pub description: String, + #[serde(bound = "E: EthSpec")] + pub pre: BeaconState, + #[serde(bound = "E: EthSpec")] + pub post: Option>, +} + +impl YamlDecode for EpochProcessingCrosslinks { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for EpochProcessingCrosslinks { + fn description(&self) -> String { + self.description.clone() + } + + fn result(&self, _case_index: usize) -> Result<(), Error> { + let mut state = self.pre.clone(); + let mut expected = self.post.clone(); + + // Processing requires the epoch cache. + state.build_all_caches(&E::spec()).unwrap(); + + let mut result = process_crosslinks(&mut state, &E::spec()).map(|_| state); + + compare_beacon_state_results_without_caches(&mut result, &mut expected) + } +} diff --git a/tests/ef_tests/src/cases/epoch_processing_registry_updates.rs b/tests/ef_tests/src/cases/epoch_processing_registry_updates.rs new file mode 100644 index 000000000..d91a7a4c3 --- /dev/null +++ b/tests/ef_tests/src/cases/epoch_processing_registry_updates.rs @@ -0,0 +1,39 @@ +use super::*; +use crate::case_result::compare_beacon_state_results_without_caches; +use serde_derive::Deserialize; +use state_processing::per_epoch_processing::registry_updates::process_registry_updates; +use types::{BeaconState, EthSpec}; + +#[derive(Debug, Clone, Deserialize)] +pub struct EpochProcessingRegistryUpdates { + pub description: String, + #[serde(bound = "E: EthSpec")] + pub pre: BeaconState, + #[serde(bound = "E: EthSpec")] + pub post: Option>, +} + +impl YamlDecode for EpochProcessingRegistryUpdates { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for EpochProcessingRegistryUpdates { + fn description(&self) -> String { + self.description.clone() + } + + fn result(&self, _case_index: usize) -> Result<(), Error> { + let mut state = self.pre.clone(); + let mut expected = self.post.clone(); + let spec = &E::spec(); + + // Processing requires the epoch cache. + state.build_all_caches(spec).unwrap(); + + let mut result = process_registry_updates(&mut state, spec).map(|_| state); + + compare_beacon_state_results_without_caches(&mut result, &mut expected) + } +} diff --git a/tests/ef_tests/src/cases/operations_attester_slashing.rs b/tests/ef_tests/src/cases/operations_attester_slashing.rs new file mode 100644 index 000000000..d8f1f06dc --- /dev/null +++ b/tests/ef_tests/src/cases/operations_attester_slashing.rs @@ -0,0 +1,42 @@ +use super::*; +use crate::case_result::compare_beacon_state_results_without_caches; +use serde_derive::Deserialize; +use state_processing::per_block_processing::process_attester_slashings; +use types::{AttesterSlashing, BeaconState, EthSpec}; + +#[derive(Debug, Clone, Deserialize)] +pub struct OperationsAttesterSlashing { + pub description: String, + #[serde(bound = "E: EthSpec")] + pub pre: BeaconState, + pub attester_slashing: AttesterSlashing, + #[serde(bound = "E: EthSpec")] + pub post: Option>, +} + +impl YamlDecode for OperationsAttesterSlashing { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for OperationsAttesterSlashing { + fn description(&self) -> String { + self.description.clone() + } + + fn result(&self, _case_index: usize) -> Result<(), Error> { + let mut state = self.pre.clone(); + let attester_slashing = self.attester_slashing.clone(); + let mut expected = self.post.clone(); + + // Processing requires the epoch cache. + state.build_all_caches(&E::spec()).unwrap(); + + let result = process_attester_slashings(&mut state, &[attester_slashing], &E::spec()); + + let mut result = result.and_then(|_| Ok(state)); + + compare_beacon_state_results_without_caches(&mut result, &mut expected) + } +} diff --git a/tests/ef_tests/src/cases/operations_deposit.rs b/tests/ef_tests/src/cases/operations_deposit.rs new file mode 100644 index 000000000..23b791ba5 --- /dev/null +++ b/tests/ef_tests/src/cases/operations_deposit.rs @@ -0,0 +1,43 @@ +use super::*; +use crate::case_result::compare_beacon_state_results_without_caches; +use serde_derive::Deserialize; +use state_processing::per_block_processing::process_deposits; +use types::{BeaconState, Deposit, EthSpec}; + +#[derive(Debug, Clone, Deserialize)] +pub struct OperationsDeposit { + pub bls_setting: Option, + pub description: String, + #[serde(bound = "E: EthSpec")] + pub pre: BeaconState, + pub deposit: Deposit, + #[serde(bound = "E: EthSpec")] + pub post: Option>, +} + +impl YamlDecode for OperationsDeposit { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for OperationsDeposit { + fn description(&self) -> String { + self.description.clone() + } + + fn result(&self, _case_index: usize) -> Result<(), Error> { + if self.bls_setting == Some(cfg!(feature = "fake_crypto") as u8) { + return Ok(()); + } + let mut state = self.pre.clone(); + let deposit = self.deposit.clone(); + let mut expected = self.post.clone(); + + let result = process_deposits(&mut state, &[deposit], &E::spec()); + + let mut result = result.and_then(|_| Ok(state)); + + compare_beacon_state_results_without_caches(&mut result, &mut expected) + } +} diff --git a/tests/ef_tests/src/cases/operations_exit.rs b/tests/ef_tests/src/cases/operations_exit.rs new file mode 100644 index 000000000..3d0f6b010 --- /dev/null +++ b/tests/ef_tests/src/cases/operations_exit.rs @@ -0,0 +1,42 @@ +use super::*; +use crate::case_result::compare_beacon_state_results_without_caches; +use serde_derive::Deserialize; +use state_processing::per_block_processing::process_exits; +use types::{BeaconState, EthSpec, VoluntaryExit}; + +#[derive(Debug, Clone, Deserialize)] +pub struct OperationsExit { + pub description: String, + #[serde(bound = "E: EthSpec")] + pub pre: BeaconState, + pub voluntary_exit: VoluntaryExit, + #[serde(bound = "E: EthSpec")] + pub post: Option>, +} + +impl YamlDecode for OperationsExit { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for OperationsExit { + fn description(&self) -> String { + self.description.clone() + } + + fn result(&self, _case_index: usize) -> Result<(), Error> { + let mut state = self.pre.clone(); + let exit = self.voluntary_exit.clone(); + let mut expected = self.post.clone(); + + // Exit processing requires the epoch cache. + state.build_all_caches(&E::spec()).unwrap(); + + let result = process_exits(&mut state, &[exit], &E::spec()); + + let mut result = result.and_then(|_| Ok(state)); + + compare_beacon_state_results_without_caches(&mut result, &mut expected) + } +} diff --git a/tests/ef_tests/src/cases/operations_proposer_slashing.rs b/tests/ef_tests/src/cases/operations_proposer_slashing.rs new file mode 100644 index 000000000..416a6f7c3 --- /dev/null +++ b/tests/ef_tests/src/cases/operations_proposer_slashing.rs @@ -0,0 +1,42 @@ +use super::*; +use crate::case_result::compare_beacon_state_results_without_caches; +use serde_derive::Deserialize; +use state_processing::per_block_processing::process_proposer_slashings; +use types::{BeaconState, EthSpec, ProposerSlashing}; + +#[derive(Debug, Clone, Deserialize)] +pub struct OperationsProposerSlashing { + pub description: String, + #[serde(bound = "E: EthSpec")] + pub pre: BeaconState, + pub proposer_slashing: ProposerSlashing, + #[serde(bound = "E: EthSpec")] + pub post: Option>, +} + +impl YamlDecode for OperationsProposerSlashing { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for OperationsProposerSlashing { + fn description(&self) -> String { + self.description.clone() + } + + fn result(&self, _case_index: usize) -> Result<(), Error> { + let mut state = self.pre.clone(); + let proposer_slashing = self.proposer_slashing.clone(); + let mut expected = self.post.clone(); + + // Processing requires the epoch cache. + state.build_all_caches(&E::spec()).unwrap(); + + let result = process_proposer_slashings(&mut state, &[proposer_slashing], &E::spec()); + + let mut result = result.and_then(|_| Ok(state)); + + compare_beacon_state_results_without_caches(&mut result, &mut expected) + } +} diff --git a/tests/ef_tests/src/cases/operations_transfer.rs b/tests/ef_tests/src/cases/operations_transfer.rs new file mode 100644 index 000000000..3ec96cd5c --- /dev/null +++ b/tests/ef_tests/src/cases/operations_transfer.rs @@ -0,0 +1,45 @@ +use super::*; +use crate::case_result::compare_beacon_state_results_without_caches; +use serde_derive::Deserialize; +use state_processing::per_block_processing::process_transfers; +use types::{BeaconState, EthSpec, Transfer}; + +#[derive(Debug, Clone, Deserialize)] +pub struct OperationsTransfer { + pub description: String, + #[serde(bound = "E: EthSpec")] + pub pre: BeaconState, + pub transfer: Transfer, + #[serde(bound = "E: EthSpec")] + pub post: Option>, +} + +impl YamlDecode for OperationsTransfer { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for OperationsTransfer { + fn description(&self) -> String { + self.description.clone() + } + + fn result(&self, _case_index: usize) -> Result<(), Error> { + let mut state = self.pre.clone(); + let transfer = self.transfer.clone(); + let mut expected = self.post.clone(); + + // Transfer processing requires the epoch cache. + state.build_all_caches(&E::spec()).unwrap(); + + let mut spec = E::spec(); + spec.max_transfers = 1; + + let result = process_transfers(&mut state, &[transfer], &spec); + + let mut result = result.and_then(|_| Ok(state)); + + compare_beacon_state_results_without_caches(&mut result, &mut expected) + } +} diff --git a/tests/ef_tests/src/cases/shuffling.rs b/tests/ef_tests/src/cases/shuffling.rs new file mode 100644 index 000000000..ef8a1b934 --- /dev/null +++ b/tests/ef_tests/src/cases/shuffling.rs @@ -0,0 +1,48 @@ +use super::*; +use crate::case_result::compare_result; +use serde_derive::Deserialize; +use std::marker::PhantomData; +use swap_or_not_shuffle::{get_permutated_index, shuffle_list}; + +#[derive(Debug, Clone, Deserialize)] +pub struct Shuffling { + pub seed: String, + pub count: usize, + pub shuffled: Vec, + #[serde(skip)] + _phantom: PhantomData, +} + +impl YamlDecode for Shuffling { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for Shuffling { + fn result(&self, _case_index: usize) -> Result<(), Error> { + if self.count == 0 { + compare_result::<_, Error>(&Ok(vec![]), &Some(self.shuffled.clone()))?; + } else { + let spec = T::spec(); + let seed = hex::decode(&self.seed[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + // Test get_permuted_index + let shuffling = (0..self.count) + .into_iter() + .map(|i| { + get_permutated_index(i, self.count, &seed, spec.shuffle_round_count).unwrap() + }) + .collect(); + compare_result::<_, Error>(&Ok(shuffling), &Some(self.shuffled.clone()))?; + + // Test "shuffle_list" + let input: Vec = (0..self.count).collect(); + let shuffling = shuffle_list(input, spec.shuffle_round_count, &seed, false).unwrap(); + compare_result::<_, Error>(&Ok(shuffling), &Some(self.shuffled.clone()))?; + } + + Ok(()) + } +} diff --git a/tests/ef_tests/src/cases/ssz_generic.rs b/tests/ef_tests/src/cases/ssz_generic.rs new file mode 100644 index 000000000..09aba39f1 --- /dev/null +++ b/tests/ef_tests/src/cases/ssz_generic.rs @@ -0,0 +1,72 @@ +use super::*; +use crate::case_result::compare_result; +use ethereum_types::{U128, U256}; +use serde_derive::Deserialize; +use ssz::Decode; +use std::fmt::Debug; + +#[derive(Debug, Clone, Deserialize)] +pub struct SszGeneric { + #[serde(alias = "type")] + pub type_name: String, + pub valid: bool, + pub value: Option, + pub ssz: Option, +} + +impl YamlDecode for SszGeneric { + fn yaml_decode(yaml: &String) -> Result { + Ok(serde_yaml::from_str(&yaml.as_str()).unwrap()) + } +} + +impl Case for SszGeneric { + fn result(&self, _case_index: usize) -> Result<(), Error> { + if let Some(ssz) = &self.ssz { + match self.type_name.as_ref() { + "uint8" => ssz_generic_test::(self.valid, ssz, &self.value), + "uint16" => ssz_generic_test::(self.valid, ssz, &self.value), + "uint32" => ssz_generic_test::(self.valid, ssz, &self.value), + "uint64" => ssz_generic_test::(self.valid, ssz, &self.value), + "uint128" => ssz_generic_test::(self.valid, ssz, &self.value), + "uint256" => ssz_generic_test::(self.valid, ssz, &self.value), + _ => Err(Error::FailedToParseTest(format!( + "Unknown type: {}", + self.type_name + ))), + } + } else { + // Skip tests that do not have an ssz field. + // + // See: https://github.com/ethereum/eth2.0-specs/issues/1079 + Ok(()) + } + } +} + +/// Execute a `ssz_generic` test case. +fn ssz_generic_test( + should_be_ok: bool, + ssz: &String, + value: &Option, +) -> Result<(), Error> +where + T: Decode + YamlDecode + Debug + PartialEq, +{ + let ssz = hex::decode(&ssz[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + + // We do not cater for the scenario where the test is valid but we are not passed any SSZ. + if should_be_ok && value.is_none() { + panic!("Unexpected test input. Cannot pass without value.") + } + + let expected = if let Some(string) = value { + Some(T::yaml_decode(string)?) + } else { + None + }; + + let decoded = T::from_ssz_bytes(&ssz); + + compare_result(&decoded, &expected) +} diff --git a/tests/ef_tests/src/cases/ssz_static.rs b/tests/ef_tests/src/cases/ssz_static.rs new file mode 100644 index 000000000..374b90bd2 --- /dev/null +++ b/tests/ef_tests/src/cases/ssz_static.rs @@ -0,0 +1,137 @@ +use super::*; +use crate::case_result::compare_result; +use cached_tree_hash::{CachedTreeHash, TreeHashCache}; +use serde_derive::Deserialize; +use ssz::{Decode, Encode}; +use std::fmt::Debug; +use std::marker::PhantomData; +use tree_hash::TreeHash; +use types::{ + test_utils::{SeedableRng, TestRandom, XorShiftRng}, + Attestation, AttestationData, AttestationDataAndCustodyBit, AttesterSlashing, BeaconBlock, + BeaconBlockBody, BeaconBlockHeader, BeaconState, Crosslink, Deposit, DepositData, Eth1Data, + EthSpec, Fork, Hash256, HistoricalBatch, IndexedAttestation, PendingAttestation, + ProposerSlashing, Transfer, Validator, VoluntaryExit, +}; + +// Enum variant names are used by Serde when deserializing the test YAML +#[derive(Debug, Clone, Deserialize)] +pub enum SszStatic +where + E: EthSpec, +{ + Fork(SszStaticInner), + Crosslink(SszStaticInner), + Eth1Data(SszStaticInner), + AttestationData(SszStaticInner), + AttestationDataAndCustodyBit(SszStaticInner), + IndexedAttestation(SszStaticInner), + DepositData(SszStaticInner), + BeaconBlockHeader(SszStaticInner), + Validator(SszStaticInner), + PendingAttestation(SszStaticInner), + HistoricalBatch(SszStaticInner, E>), + ProposerSlashing(SszStaticInner), + AttesterSlashing(SszStaticInner), + Attestation(SszStaticInner), + Deposit(SszStaticInner), + VoluntaryExit(SszStaticInner), + Transfer(SszStaticInner), + BeaconBlockBody(SszStaticInner), + BeaconBlock(SszStaticInner), + BeaconState(SszStaticInner, E>), +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SszStaticInner +where + E: EthSpec, +{ + pub value: T, + pub serialized: String, + pub root: String, + #[serde(skip, default)] + _phantom: PhantomData, +} + +impl YamlDecode for SszStatic { + fn yaml_decode(yaml: &String) -> Result { + serde_yaml::from_str(yaml).map_err(|e| Error::FailedToParseTest(format!("{:?}", e))) + } +} + +impl Case for SszStatic { + fn result(&self, _case_index: usize) -> Result<(), Error> { + use self::SszStatic::*; + + match *self { + Fork(ref val) => ssz_static_test(val), + Crosslink(ref val) => ssz_static_test(val), + Eth1Data(ref val) => ssz_static_test(val), + AttestationData(ref val) => ssz_static_test(val), + AttestationDataAndCustodyBit(ref val) => ssz_static_test(val), + IndexedAttestation(ref val) => ssz_static_test(val), + DepositData(ref val) => ssz_static_test(val), + BeaconBlockHeader(ref val) => ssz_static_test(val), + Validator(ref val) => ssz_static_test(val), + PendingAttestation(ref val) => ssz_static_test(val), + HistoricalBatch(ref val) => ssz_static_test(val), + ProposerSlashing(ref val) => ssz_static_test(val), + AttesterSlashing(ref val) => ssz_static_test(val), + Attestation(ref val) => ssz_static_test(val), + Deposit(ref val) => ssz_static_test(val), + VoluntaryExit(ref val) => ssz_static_test(val), + Transfer(ref val) => ssz_static_test(val), + BeaconBlockBody(ref val) => ssz_static_test(val), + BeaconBlock(ref val) => ssz_static_test(val), + BeaconState(ref val) => ssz_static_test(val), + } + } +} + +fn ssz_static_test(tc: &SszStaticInner) -> Result<(), Error> +where + T: Clone + + Decode + + Debug + + Encode + + PartialEq + + serde::de::DeserializeOwned + + TreeHash + + CachedTreeHash + + TestRandom, +{ + // Verify we can decode SSZ in the same way we can decode YAML. + let ssz = hex::decode(&tc.serialized[2..]) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let expected = tc.value.clone(); + let decode_result = T::from_ssz_bytes(&ssz); + compare_result(&decode_result, &Some(expected))?; + + // Verify we can encode the result back into original ssz bytes + let decoded = decode_result.unwrap(); + let encoded_result = decoded.as_ssz_bytes(); + compare_result::, Error>(&Ok(encoded_result), &Some(ssz))?; + + // Verify the TreeHash root of the decoded struct matches the test. + let expected_root = + &hex::decode(&tc.root[2..]).map_err(|e| Error::FailedToParseTest(format!("{:?}", e)))?; + let expected_root = Hash256::from_slice(&expected_root); + let tree_hash_root = Hash256::from_slice(&decoded.tree_hash_root()); + compare_result::(&Ok(tree_hash_root), &Some(expected_root))?; + + // Verify a _new_ CachedTreeHash root of the decoded struct matches the test. + let cache = TreeHashCache::new(&decoded).unwrap(); + let cached_tree_hash_root = Hash256::from_slice(cache.tree_hash_root().unwrap()); + compare_result::(&Ok(cached_tree_hash_root), &Some(expected_root))?; + + // Verify the root after an update from a random CachedTreeHash to the decoded struct. + let mut rng = XorShiftRng::from_seed([42; 16]); + let random_instance = T::random_for_test(&mut rng); + let mut cache = TreeHashCache::new(&random_instance).unwrap(); + cache.update(&decoded).unwrap(); + let updated_root = Hash256::from_slice(cache.tree_hash_root().unwrap()); + compare_result::(&Ok(updated_root), &Some(expected_root))?; + + Ok(()) +} diff --git a/tests/ef_tests/src/doc.rs b/tests/ef_tests/src/doc.rs new file mode 100644 index 000000000..f69d1f998 --- /dev/null +++ b/tests/ef_tests/src/doc.rs @@ -0,0 +1,161 @@ +use crate::case_result::CaseResult; +use crate::cases::*; +use crate::doc_header::DocHeader; +use crate::eth_specs::{MainnetEthSpec, MinimalEthSpec}; +use crate::yaml_decode::{yaml_split_header_and_cases, YamlDecode}; +use crate::EfTest; +use serde_derive::Deserialize; +use std::{fs::File, io::prelude::*, path::PathBuf}; + +#[derive(Debug, Deserialize)] +pub struct Doc { + pub header_yaml: String, + pub cases_yaml: String, + pub path: PathBuf, +} + +impl Doc { + fn from_path(path: PathBuf) -> Self { + let mut file = File::open(path.clone()).unwrap(); + + let mut yaml = String::new(); + file.read_to_string(&mut yaml).unwrap(); + + let (header_yaml, cases_yaml) = yaml_split_header_and_cases(yaml.clone()); + + Self { + header_yaml, + cases_yaml, + path, + } + } + + pub fn test_results(&self) -> Vec { + let header: DocHeader = serde_yaml::from_str(&self.header_yaml.as_str()).unwrap(); + + match ( + header.runner.as_ref(), + header.handler.as_ref(), + header.config.as_ref(), + ) { + ("ssz", "uint", _) => run_test::(self), + ("ssz", "static", "minimal") => run_test::>(self), + ("ssz", "static", "mainnet") => run_test::>(self), + ("shuffling", "core", "minimal") => run_test::>(self), + ("shuffling", "core", "mainnet") => run_test::>(self), + ("bls", "aggregate_pubkeys", "mainnet") => run_test::(self), + ("bls", "aggregate_sigs", "mainnet") => run_test::(self), + ("bls", "msg_hash_compressed", "mainnet") => run_test::(self), + // Note this test fails due to a difference in our internal representations. It does + // not effect verification or external representation. + // + // It is skipped. + ("bls", "msg_hash_uncompressed", "mainnet") => vec![], + ("bls", "priv_to_pub", "mainnet") => run_test::(self), + ("bls", "sign_msg", "mainnet") => run_test::(self), + ("operations", "deposit", "mainnet") => { + run_test::>(self) + } + ("operations", "deposit", "minimal") => { + run_test::>(self) + } + ("operations", "transfer", "mainnet") => { + run_test::>(self) + } + ("operations", "transfer", "minimal") => { + run_test::>(self) + } + ("operations", "voluntary_exit", "mainnet") => { + run_test::>(self) + } + ("operations", "voluntary_exit", "minimal") => { + run_test::>(self) + } + ("operations", "proposer_slashing", "mainnet") => { + run_test::>(self) + } + ("operations", "proposer_slashing", "minimal") => { + run_test::>(self) + } + ("operations", "attester_slashing", "mainnet") => { + run_test::>(self) + } + ("operations", "attester_slashing", "minimal") => { + run_test::>(self) + } + ("epoch_processing", "crosslinks", "minimal") => { + run_test::>(self) + } + ("epoch_processing", "crosslinks", "mainnet") => { + run_test::>(self) + } + ("epoch_processing", "registry_updates", "minimal") => { + run_test::>(self) + } + ("epoch_processing", "registry_updates", "mainnet") => { + run_test::>(self) + } + (runner, handler, config) => panic!( + "No implementation for runner: \"{}\", handler: \"{}\", config: \"{}\"", + runner, handler, config + ), + } + } + + pub fn assert_tests_pass(path: PathBuf) { + let doc = Self::from_path(path); + let results = doc.test_results(); + + if results.iter().any(|r| r.result.is_err()) { + print_failures(&doc, &results); + panic!("Tests failed (see above)"); + } else { + println!("Passed {} tests in {:?}", results.len(), doc.path); + } + } +} + +pub fn run_test(doc: &Doc) -> Vec +where + Cases: EfTest + YamlDecode, +{ + // Extract only the "test_cases" YAML as a stand-alone string. + //let test_cases_yaml = extract_yaml_by_key(self., "test_cases"); + + // Pass only the "test_cases" YAML string to `yaml_decode`. + let test_cases: Cases = Cases::yaml_decode(&doc.cases_yaml).unwrap(); + + test_cases.test_results() +} + +pub fn print_failures(doc: &Doc, results: &[CaseResult]) { + let header: DocHeader = serde_yaml::from_str(&doc.header_yaml).unwrap(); + let failures: Vec<&CaseResult> = results.iter().filter(|r| r.result.is_err()).collect(); + + println!("--------------------------------------------------"); + println!("Test Failure"); + println!("Title: {}", header.title); + println!("File: {:?}", doc.path); + println!(""); + println!( + "{} tests, {} failures, {} passes.", + results.len(), + failures.len(), + results.len() - failures.len() + ); + println!(""); + + for failure in failures { + let error = failure.result.clone().unwrap_err(); + + println!("-------"); + println!( + "case[{}] ({}) failed with {}:", + failure.case_index, + failure.desc, + error.name() + ); + println!("{}", error.message()); + } + println!(""); +} diff --git a/tests/ef_tests/src/doc_header.rs b/tests/ef_tests/src/doc_header.rs new file mode 100644 index 000000000..c0d6d3276 --- /dev/null +++ b/tests/ef_tests/src/doc_header.rs @@ -0,0 +1,12 @@ +use serde_derive::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct DocHeader { + pub title: String, + pub summary: String, + pub forks_timeline: String, + pub forks: Vec, + pub config: String, + pub runner: String, + pub handler: String, +} diff --git a/tests/ef_tests/src/error.rs b/tests/ef_tests/src/error.rs new file mode 100644 index 000000000..cd812d2fd --- /dev/null +++ b/tests/ef_tests/src/error.rs @@ -0,0 +1,27 @@ +#[derive(Debug, PartialEq, Clone)] +pub enum Error { + /// The value in the test didn't match our value. + NotEqual(String), + /// The test specified a failure and we did not experience one. + DidntFail(String), + /// Failed to parse the test (internal error). + FailedToParseTest(String), +} + +impl Error { + pub fn name(&self) -> &str { + match self { + Error::NotEqual(_) => "NotEqual", + Error::DidntFail(_) => "DidntFail", + Error::FailedToParseTest(_) => "FailedToParseTest", + } + } + + pub fn message(&self) -> &str { + match self { + Error::NotEqual(m) => m.as_str(), + Error::DidntFail(m) => m.as_str(), + Error::FailedToParseTest(m) => m.as_str(), + } + } +} diff --git a/tests/ef_tests/src/eth_specs.rs b/tests/ef_tests/src/eth_specs.rs new file mode 100644 index 000000000..cdf8b94e8 --- /dev/null +++ b/tests/ef_tests/src/eth_specs.rs @@ -0,0 +1,30 @@ +use serde_derive::{Deserialize, Serialize}; +use types::{ + typenum::{U64, U8}, + ChainSpec, EthSpec, FewValidatorsEthSpec, FoundationEthSpec, +}; + +/// "Minimal" testing specification, as defined here: +/// +/// https://github.com/ethereum/eth2.0-specs/blob/v0.6.1/configs/constant_presets/minimal.yaml +/// +/// Spec v0.6.1 +#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)] +pub struct MinimalEthSpec; + +impl EthSpec for MinimalEthSpec { + type ShardCount = U8; + type SlotsPerHistoricalRoot = U64; + type LatestRandaoMixesLength = U64; + type LatestActiveIndexRootsLength = U64; + type LatestSlashedExitLength = U64; + + fn spec() -> ChainSpec { + // TODO: this spec is likely incorrect! + let mut spec = FewValidatorsEthSpec::spec(); + spec.shuffle_round_count = 10; + spec + } +} + +pub type MainnetEthSpec = FoundationEthSpec; diff --git a/tests/ef_tests/src/lib.rs b/tests/ef_tests/src/lib.rs new file mode 100644 index 000000000..942a6dbb7 --- /dev/null +++ b/tests/ef_tests/src/lib.rs @@ -0,0 +1,22 @@ +use types::EthSpec; + +pub use case_result::CaseResult; +pub use cases::Case; +pub use doc::Doc; +pub use error::Error; +pub use yaml_decode::YamlDecode; + +mod case_result; +mod cases; +mod doc; +mod doc_header; +mod error; +mod eth_specs; +mod yaml_decode; + +/// Defined where an object can return the results of some test(s) adhering to the Ethereum +/// Foundation testing format. +pub trait EfTest { + /// Returns the results of executing one or more tests. + fn test_results(&self) -> Vec; +} diff --git a/tests/ef_tests/src/yaml_decode.rs b/tests/ef_tests/src/yaml_decode.rs new file mode 100644 index 000000000..974df8311 --- /dev/null +++ b/tests/ef_tests/src/yaml_decode.rs @@ -0,0 +1,59 @@ +use super::*; +use ethereum_types::{U128, U256}; +use types::Fork; + +mod utils; + +pub use utils::*; + +pub trait YamlDecode: Sized { + /// Decode an object from the test specification YAML. + fn yaml_decode(string: &String) -> Result; +} + +/// Basic types can general be decoded with the `parse` fn if they implement `str::FromStr`. +macro_rules! impl_via_parse { + ($ty: ty) => { + impl YamlDecode for $ty { + fn yaml_decode(string: &String) -> Result { + string + .parse::() + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e))) + } + } + }; +} + +impl_via_parse!(u8); +impl_via_parse!(u16); +impl_via_parse!(u32); +impl_via_parse!(u64); + +/// Some `ethereum-types` methods have a `str::FromStr` implementation that expects `0x`-prefixed: +/// hex, so we use `from_dec_str` instead. +macro_rules! impl_via_from_dec_str { + ($ty: ty) => { + impl YamlDecode for $ty { + fn yaml_decode(string: &String) -> Result { + Self::from_dec_str(string).map_err(|e| Error::FailedToParseTest(format!("{:?}", e))) + } + } + }; +} + +impl_via_from_dec_str!(U128); +impl_via_from_dec_str!(U256); + +/// Types that already implement `serde::Deserialize` can be decoded using `serde_yaml`. +macro_rules! impl_via_serde_yaml { + ($ty: ty) => { + impl YamlDecode for $ty { + fn yaml_decode(string: &String) -> Result { + serde_yaml::from_str(string) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e))) + } + } + }; +} + +impl_via_serde_yaml!(Fork); diff --git a/tests/ef_tests/src/yaml_decode/utils.rs b/tests/ef_tests/src/yaml_decode/utils.rs new file mode 100644 index 000000000..059d3b5d2 --- /dev/null +++ b/tests/ef_tests/src/yaml_decode/utils.rs @@ -0,0 +1,10 @@ +pub fn yaml_split_header_and_cases(mut yaml: String) -> (String, String) { + let test_cases_start = yaml.find("\ntest_cases:\n").unwrap(); + // + 1 to skip the \n we used for matching. + let mut test_cases = yaml.split_off(test_cases_start + 1); + + let end_of_first_line = test_cases.find("\n").unwrap(); + let test_cases = test_cases.split_off(end_of_first_line + 1); + + (yaml, test_cases) +} diff --git a/tests/ef_tests/tests/tests.rs b/tests/ef_tests/tests/tests.rs new file mode 100644 index 000000000..ddf13388f --- /dev/null +++ b/tests/ef_tests/tests/tests.rs @@ -0,0 +1,144 @@ +use ef_tests::*; +use rayon::prelude::*; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; + +fn yaml_files_in_test_dir(dir: &Path) -> Vec { + let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("eth2.0-spec-tests") + .join("tests") + .join(dir); + + assert!( + base_path.exists(), + format!( + "Unable to locate {:?}. Did you init git submoules?", + base_path + ) + ); + + let mut paths: Vec = WalkDir::new(base_path) + .into_iter() + .filter_map(|e| e.ok()) + .filter_map(|entry| { + if entry.file_type().is_file() { + match entry.file_name().to_str() { + Some(f) if f.ends_with(".yaml") => Some(entry.path().to_path_buf()), + Some(f) if f.ends_with(".yml") => Some(entry.path().to_path_buf()), + _ => None, + } + } else { + None + } + }) + .collect(); + + // Reverse the file order. Assuming files come in lexicographical order, executing tests in + // reverse means we get the "minimal" tests before the "mainnet" tests. This makes life easier + // for debugging. + paths.reverse(); + paths +} + +#[test] +#[cfg(feature = "fake_crypto")] +fn ssz_generic() { + yaml_files_in_test_dir(&Path::new("ssz_generic")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +#[cfg(feature = "fake_crypto")] +fn ssz_static() { + yaml_files_in_test_dir(&Path::new("ssz_static")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +fn shuffling() { + yaml_files_in_test_dir(&Path::new("shuffling").join("core")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +fn operations_deposit() { + yaml_files_in_test_dir(&Path::new("operations").join("deposit")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +fn operations_transfer() { + yaml_files_in_test_dir(&Path::new("operations").join("transfer")) + .into_par_iter() + .rev() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +fn operations_exit() { + yaml_files_in_test_dir(&Path::new("operations").join("voluntary_exit")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +fn operations_proposer_slashing() { + yaml_files_in_test_dir(&Path::new("operations").join("proposer_slashing")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +fn operations_attester_slashing() { + yaml_files_in_test_dir(&Path::new("operations").join("attester_slashing")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +#[cfg(not(feature = "fake_crypto"))] +fn bls() { + yaml_files_in_test_dir(&Path::new("bls")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +fn epoch_processing_crosslinks() { + yaml_files_in_test_dir(&Path::new("epoch_processing").join("crosslinks")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} + +#[test] +fn epoch_processing_registry_updates() { + yaml_files_in_test_dir(&Path::new("epoch_processing").join("registry_updates")) + .into_par_iter() + .for_each(|file| { + Doc::assert_tests_pass(file); + }); +} diff --git a/validator_client/src/attestation_producer/mod.rs b/validator_client/src/attestation_producer/mod.rs index d2dbdf2e2..0a65c1f1e 100644 --- a/validator_client/src/attestation_producer/mod.rs +++ b/validator_client/src/attestation_producer/mod.rs @@ -52,7 +52,7 @@ impl<'a, B: BeaconNodeAttestation, S: Signer> AttestationProducer<'a, B, S> { Ok(ValidatorEvent::SignerRejection(_slot)) => { error!(log, "Attestation production error"; "Error" => "Signer could not sign the attestation".to_string()) } - Ok(ValidatorEvent::SlashableAttestationNotProduced(_slot)) => { + Ok(ValidatorEvent::IndexedAttestationNotProduced(_slot)) => { error!(log, "Attestation production error"; "Error" => "Rejected the attestation as it could have been slashed".to_string()) } Ok(ValidatorEvent::PublishAttestationFailed) => { @@ -99,7 +99,7 @@ impl<'a, B: BeaconNodeAttestation, S: Signer> AttestationProducer<'a, B, S> { Ok(ValidatorEvent::SignerRejection(self.duty.slot)) } } else { - Ok(ValidatorEvent::SlashableAttestationNotProduced( + Ok(ValidatorEvent::IndexedAttestationNotProduced( self.duty.slot, )) } @@ -140,7 +140,7 @@ impl<'a, B: BeaconNodeAttestation, S: Signer> AttestationProducer<'a, B, S> { aggregation_bitfield, data: attestation, custody_bitfield, - aggregate_signature, + signature: aggregate_signature, }) } diff --git a/validator_client/src/block_producer/mod.rs b/validator_client/src/block_producer/mod.rs index 2689b302d..fc01b8126 100644 --- a/validator_client/src/block_producer/mod.rs +++ b/validator_client/src/block_producer/mod.rs @@ -24,7 +24,7 @@ pub enum ValidatorEvent { /// A block was not produced as it would have been slashable. SlashableBlockNotProduced(Slot), /// An attestation was not produced as it would have been slashable. - SlashableAttestationNotProduced(Slot), + IndexedAttestationNotProduced(Slot), /// The Beacon Node was unable to produce a block at that slot. BeaconNodeUnableToProduceBlock(Slot), /// The signer failed to sign the message. @@ -100,7 +100,9 @@ impl<'a, B: BeaconNodeBlock, S: Signer> BlockProducer<'a, B, S> { .produce_beacon_block(self.slot, &randao_reveal)? { if self.safe_to_produce(&block) { - let domain = self.spec.get_domain(epoch, Domain::BeaconBlock, &self.fork); + let domain = self + .spec + .get_domain(epoch, Domain::BeaconProposer, &self.fork); if let Some(block) = self.sign_block(block, domain) { self.beacon_node.publish_beacon_block(block)?; Ok(ValidatorEvent::BlockProduced(self.slot))