diff --git a/Cargo.toml b/Cargo.toml index 42d69489b..c5aae7f43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "eth2/utils/boolean-bitfield", "eth2/utils/hashing", "eth2/utils/honey-badger-split", + "eth2/utils/merkle_proof", "eth2/utils/int_to_bytes", "eth2/utils/slot_clock", "eth2/utils/ssz", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e33912f7a..ddb9514db 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -65,6 +65,7 @@ pub struct BeaconChain { pub slot_clock: U, pub attestation_aggregator: RwLock, pub deposits_for_inclusion: RwLock>, + pub exits_for_inclusion: RwLock>, pub proposer_slashings_for_inclusion: RwLock>, pub attester_slashings_for_inclusion: RwLock>, canonical_head: RwLock, @@ -134,6 +135,7 @@ where slot_clock, attestation_aggregator, deposits_for_inclusion: RwLock::new(vec![]), + exits_for_inclusion: RwLock::new(vec![]), proposer_slashings_for_inclusion: RwLock::new(vec![]), attester_slashings_for_inclusion: RwLock::new(vec![]), state: RwLock::new(genesis_state), @@ -370,13 +372,17 @@ where /// Accept some deposit and queue it for inclusion in an appropriate block. pub fn receive_deposit_for_inclusion(&self, deposit: Deposit) { - // TODO: deposits are not check for validity; check them. + // TODO: deposits are not checked for validity; check them. + // + // https://github.com/sigp/lighthouse/issues/276 self.deposits_for_inclusion.write().push(deposit); } /// Return a vec of deposits suitable for inclusion in some block. pub fn get_deposits_for_block(&self) -> Vec { // TODO: deposits are indiscriminately included; check them for validity. + // + // https://github.com/sigp/lighthouse/issues/275 self.deposits_for_inclusion.read().clone() } @@ -386,6 +392,8 @@ where /// This ensures that `Deposits` are not included twice in successive blocks. pub fn set_deposits_as_included(&self, included_deposits: &[Deposit]) { // TODO: method does not take forks into account; consider this. + // + // https://github.com/sigp/lighthouse/issues/275 let mut indices_to_delete = vec![]; for included in included_deposits { @@ -402,9 +410,49 @@ where } } + /// Accept some exit and queue it for inclusion in an appropriate block. + pub fn receive_exit_for_inclusion(&self, exit: Exit) { + // TODO: exits are not checked for validity; check them. + // + // https://github.com/sigp/lighthouse/issues/276 + self.exits_for_inclusion.write().push(exit); + } + + /// Return a vec of exits suitable for inclusion in some block. + pub fn get_exits_for_block(&self) -> Vec { + // TODO: exits are indiscriminately included; check them for validity. + // + // https://github.com/sigp/lighthouse/issues/275 + self.exits_for_inclusion.read().clone() + } + + /// Takes a list of `Deposits` that were included in recent blocks and removes them from the + /// inclusion queue. + /// + /// This ensures that `Deposits` are not included twice in successive blocks. + pub fn set_exits_as_included(&self, included_exits: &[Exit]) { + // TODO: method does not take forks into account; consider this. + let mut indices_to_delete = vec![]; + + for included in included_exits { + for (i, for_inclusion) in self.exits_for_inclusion.read().iter().enumerate() { + if included == for_inclusion { + indices_to_delete.push(i); + } + } + } + + let exits_for_inclusion = &mut self.exits_for_inclusion.write(); + for i in indices_to_delete { + exits_for_inclusion.remove(i); + } + } + /// Accept some proposer slashing and queue it for inclusion in an appropriate block. pub fn receive_proposer_slashing_for_inclusion(&self, proposer_slashing: ProposerSlashing) { - // TODO: proposer_slashings are not check for validity; check them. + // TODO: proposer_slashings are not checked for validity; check them. + // + // https://github.com/sigp/lighthouse/issues/276 self.proposer_slashings_for_inclusion .write() .push(proposer_slashing); @@ -413,6 +461,8 @@ where /// Return a vec of proposer slashings suitable for inclusion in some block. pub fn get_proposer_slashings_for_block(&self) -> Vec { // TODO: proposer_slashings are indiscriminately included; check them for validity. + // + // https://github.com/sigp/lighthouse/issues/275 self.proposer_slashings_for_inclusion.read().clone() } @@ -425,6 +475,8 @@ where included_proposer_slashings: &[ProposerSlashing], ) { // TODO: method does not take forks into account; consider this. + // + // https://github.com/sigp/lighthouse/issues/275 let mut indices_to_delete = vec![]; for included in included_proposer_slashings { @@ -448,7 +500,9 @@ where /// Accept some attester slashing and queue it for inclusion in an appropriate block. pub fn receive_attester_slashing_for_inclusion(&self, attester_slashing: AttesterSlashing) { - // TODO: attester_slashings are not check for validity; check them. + // TODO: attester_slashings are not checked for validity; check them. + // + // https://github.com/sigp/lighthouse/issues/276 self.attester_slashings_for_inclusion .write() .push(attester_slashing); @@ -457,6 +511,8 @@ where /// Return a vec of attester slashings suitable for inclusion in some block. pub fn get_attester_slashings_for_block(&self) -> Vec { // TODO: attester_slashings are indiscriminately included; check them for validity. + // + // https://github.com/sigp/lighthouse/issues/275 self.attester_slashings_for_inclusion.read().clone() } @@ -469,6 +525,8 @@ where included_attester_slashings: &[AttesterSlashing], ) { // TODO: method does not take forks into account; consider this. + // + // https://github.com/sigp/lighthouse/issues/275 let mut indices_to_delete = vec![]; for included in included_attester_slashings { @@ -678,7 +736,7 @@ where attester_slashings: self.get_attester_slashings_for_block(), attestations, deposits: self.get_deposits_for_block(), - exits: vec![], + exits: self.get_exits_for_block(), }, }; diff --git a/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml b/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml index 56dd66558..a6021dbe5 100644 --- a/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml +++ b/beacon_node/beacon_chain/test_harness/specs/validator_registry.yaml @@ -18,6 +18,10 @@ test_cases: amount: 32 - slot: 5 amount: 32 + exits: + # At slot 10, submit an exit for validator #50. + - slot: 10 + validator_index: 50 proposer_slashings: # At slot 2, trigger a proposer slashing for validator #42. - slot: 2 @@ -39,4 +43,5 @@ test_cases: num_validators: 1003 slashed_validators: [11, 12, 13, 14, 42] exited_validators: [] + exit_initiated_validators: [50] diff --git a/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs b/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs index ed5fddabf..105a6d6e1 100644 --- a/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs +++ b/beacon_node/beacon_chain/test_harness/src/beacon_chain_harness.rs @@ -280,6 +280,15 @@ impl BeaconChainHarness { } } + /// Submit an exit to the `BeaconChain` for inclusion in some block. + /// + /// Note: the `ValidatorHarness` for this validator continues to exist. Once it is exited it + /// will stop receiving duties from the beacon chain and just do nothing when prompted to + /// produce/attest. + pub fn add_exit(&mut self, exit: Exit) { + self.beacon_chain.receive_exit_for_inclusion(exit); + } + /// Submit a proposer slashing to the `BeaconChain` for inclusion in some block. pub fn add_proposer_slashing(&mut self, proposer_slashing: ProposerSlashing) { self.beacon_chain diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/mod.rs b/beacon_node/beacon_chain/test_harness/src/test_case.rs similarity index 86% rename from beacon_node/beacon_chain/test_harness/src/test_case/mod.rs rename to beacon_node/beacon_chain/test_harness/src/test_case.rs index ae438a5d5..1a5ebfbe1 100644 --- a/beacon_node/beacon_chain/test_harness/src/test_case/mod.rs +++ b/beacon_node/beacon_chain/test_harness/src/test_case.rs @@ -4,6 +4,7 @@ use crate::beacon_chain_harness::BeaconChainHarness; use beacon_chain::CheckPoint; use log::{info, warn}; +use ssz::TreeHash; use types::*; use types::{ attester_slashing::AttesterSlashingBuilder, proposer_slashing::ProposerSlashingBuilder, @@ -121,6 +122,20 @@ impl TestCase { } } + // Feed exits to the BeaconChain. + if let Some(ref exits) = self.config.exits { + for (slot, validator_index) in exits { + if *slot == slot_height { + info!( + "Including exit at slot height {} for validator {}.", + slot_height, validator_index + ); + let exit = build_exit(&harness, *validator_index); + harness.add_exit(exit); + } + } + } + // Build a block or skip a slot. match self.config.skip_slots { Some(ref skip_slots) if skip_slots.contains(&slot_height) => { @@ -185,6 +200,33 @@ impl TestCase { } } +fn build_exit(harness: &BeaconChainHarness, validator_index: u64) -> Exit { + let epoch = harness + .beacon_chain + .state + .read() + .current_epoch(&harness.spec); + + let mut exit = Exit { + epoch, + validator_index, + signature: Signature::empty_signature(), + }; + + let message = exit.hash_tree_root(); + + exit.signature = harness + .validator_sign( + validator_index as usize, + &message[..], + epoch, + harness.spec.domain_exit, + ) + .expect("Unable to sign Exit"); + + exit +} + /// Builds an `AttesterSlashing` for some `validator_indices`. /// /// Signs the message using a `BeaconChainHarness`. diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/config.rs b/beacon_node/beacon_chain/test_harness/src/test_case/config.rs index 440c76cd2..4034c8ddc 100644 --- a/beacon_node/beacon_chain/test_harness/src/test_case/config.rs +++ b/beacon_node/beacon_chain/test_harness/src/test_case/config.rs @@ -3,9 +3,13 @@ use bls::create_proof_of_possession; use types::*; use yaml_rust::Yaml; -pub type DepositTuple = (u64, Deposit, Keypair); -pub type ProposerSlashingTuple = (u64, u64); -pub type AttesterSlashingTuple = (u64, Vec); +pub type ValidatorIndex = u64; +pub type ValidatorIndices = Vec; + +pub type DepositTuple = (SlotHeight, Deposit, Keypair); +pub type ExitTuple = (SlotHeight, ValidatorIndex); +pub type ProposerSlashingTuple = (SlotHeight, ValidatorIndex); +pub type AttesterSlashingTuple = (SlotHeight, ValidatorIndices); /// Defines the execution of a `BeaconStateHarness` across a series of slots. #[derive(Debug)] @@ -24,6 +28,8 @@ pub struct Config { pub proposer_slashings: Option>, /// Attester slashings to be including during execution. pub attester_slashings: Option>, + /// Exits to be including during execution. + pub exits: Option>, } impl Config { @@ -40,10 +46,26 @@ impl Config { deposits: parse_deposits(&yaml), proposer_slashings: parse_proposer_slashings(&yaml), attester_slashings: parse_attester_slashings(&yaml), + exits: parse_exits(&yaml), } } } +/// Parse the `attester_slashings` section of the YAML document. +fn parse_exits(yaml: &Yaml) -> Option> { + let mut tuples = vec![]; + + for exit in yaml["exits"].as_vec()? { + let slot = as_u64(exit, "slot").expect("Incomplete exit (slot)"); + let validator_index = + as_u64(exit, "validator_index").expect("Incomplete exit (validator_index)"); + + tuples.push((SlotHeight::from(slot), validator_index)); + } + + Some(tuples) +} + /// Parse the `attester_slashings` section of the YAML document. fn parse_attester_slashings(yaml: &Yaml) -> Option> { let mut slashings = vec![]; @@ -53,7 +75,7 @@ fn parse_attester_slashings(yaml: &Yaml) -> Option> { let validator_indices = as_vec_u64(slashing, "validator_indices") .expect("Incomplete attester_slashing (validator_indices)"); - slashings.push((slot, validator_indices)); + slashings.push((SlotHeight::from(slot), validator_indices)); } Some(slashings) @@ -68,7 +90,7 @@ fn parse_proposer_slashings(yaml: &Yaml) -> Option> { let validator_index = as_u64(slashing, "validator_index") .expect("Incomplete proposer slashing (validator_index)"); - slashings.push((slot, validator_index)); + slashings.push((SlotHeight::from(slot), validator_index)); } Some(slashings) @@ -102,7 +124,7 @@ fn parse_deposits(yaml: &Yaml) -> Option> { }, }; - deposits.push((slot, deposit, keypair)); + deposits.push((SlotHeight::from(slot), deposit, keypair)); } Some(deposits) diff --git a/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs b/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs index 3126dcfea..b52dc7d6b 100644 --- a/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs +++ b/beacon_node/beacon_chain/test_harness/src/test_case/state_check.rs @@ -13,8 +13,10 @@ pub struct StateCheck { pub num_validators: Option, /// A list of validator indices which have been penalized. Must be in ascending order. pub slashed_validators: Option>, - /// A list of validator indices which have been exited. Must be in ascending order. + /// A list of validator indices which have been fully exited. Must be in ascending order. pub exited_validators: Option>, + /// A list of validator indices which have had an exit initiated. Must be in ascending order. + pub exit_initiated_validators: Option>, } impl StateCheck { @@ -27,6 +29,7 @@ impl StateCheck { num_validators: as_usize(&yaml, "num_validators"), slashed_validators: as_vec_u64(&yaml, "slashed_validators"), exited_validators: as_vec_u64(&yaml, "exited_validators"), + exit_initiated_validators: as_vec_u64(&yaml, "exit_initiated_validators"), } } @@ -40,6 +43,7 @@ impl StateCheck { info!("Running state check for slot height {}.", self.slot); + // Check the state slot. assert_eq!( self.slot, state.slot - spec.genesis_epoch.start_slot(spec.slots_per_epoch), @@ -55,6 +59,7 @@ impl StateCheck { info!("OK: num_validators = {}.", num_validators); } + // Check for slashed validators. if let Some(ref slashed_validators) = self.slashed_validators { let actually_slashed_validators: Vec = state .validator_registry @@ -75,6 +80,7 @@ impl StateCheck { info!("OK: slashed_validators = {:?}.", slashed_validators); } + // Check for exited validators. if let Some(ref exited_validators) = self.exited_validators { let actually_exited_validators: Vec = state .validator_registry @@ -94,5 +100,29 @@ impl StateCheck { ); info!("OK: exited_validators = {:?}.", exited_validators); } + + // Check for validators that have initiated exit. + if let Some(ref exit_initiated_validators) = self.exit_initiated_validators { + let actual: Vec = state + .validator_registry + .iter() + .enumerate() + .filter_map(|(i, validator)| { + if validator.has_initiated_exit() { + Some(i as u64) + } else { + None + } + }) + .collect(); + assert_eq!( + actual, *exit_initiated_validators, + "Exit initiated validators != expected." + ); + info!( + "OK: exit_initiated_validators = {:?}.", + exit_initiated_validators + ); + } } } diff --git a/beacon_node/db/src/stores/beacon_block_store.rs b/beacon_node/db/src/stores/beacon_block_store.rs index 8a1fc2b0d..bd5149cfd 100644 --- a/beacon_node/db/src/stores/beacon_block_store.rs +++ b/beacon_node/db/src/stores/beacon_block_store.rs @@ -134,9 +134,9 @@ mod tests { let store = BeaconBlockStore::new(db.clone()); let ssz = "definitly not a valid block".as_bytes(); - let hash = &Hash256::from("some hash".as_bytes()); + let hash = &Hash256::from([0xAA; 32]); - db.put(DB_COLUMN, hash, ssz).unwrap(); + db.put(DB_COLUMN, hash.as_bytes(), ssz).unwrap(); assert_eq!( store.block_at_slot(hash, Slot::from(42_u64)), Err(BeaconBlockAtSlotError::DBError( @@ -151,10 +151,10 @@ mod tests { let store = BeaconBlockStore::new(db.clone()); let ssz = "some bytes".as_bytes(); - let hash = &Hash256::from("some hash".as_bytes()); - let other_hash = &Hash256::from("another hash".as_bytes()); + let hash = &Hash256::from([0xAA; 32]); + let other_hash = &Hash256::from([0xBB; 32]); - db.put(DB_COLUMN, hash, ssz).unwrap(); + db.put(DB_COLUMN, hash.as_bytes(), ssz).unwrap(); assert_eq!( store.block_at_slot(other_hash, Slot::from(42_u64)), Err(BeaconBlockAtSlotError::UnknownBeaconBlock(*other_hash)) @@ -169,18 +169,15 @@ mod tests { let thread_count = 10; let write_count = 10; - // We're expecting the product of these numbers to fit in one byte. - assert!(thread_count * write_count <= 255); - let mut handles = vec![]; for t in 0..thread_count { let wc = write_count; let bs = bs.clone(); let handle = thread::spawn(move || { for w in 0..wc { - let key = (t * w) as u8; + let key = t * w; let val = 42; - bs.put(&[key][..].into(), &vec![val]).unwrap(); + bs.put(&Hash256::from_low_u64_le(key), &vec![val]).unwrap(); } }); handles.push(handle); @@ -192,9 +189,9 @@ mod tests { for t in 0..thread_count { for w in 0..write_count { - let key = (t * w) as u8; - assert!(bs.exists(&[key][..].into()).unwrap()); - let val = bs.get(&[key][..].into()).unwrap().unwrap(); + let key = t * w; + assert!(bs.exists(&Hash256::from_low_u64_le(key)).unwrap()); + let val = bs.get(&Hash256::from_low_u64_le(key)).unwrap().unwrap(); assert_eq!(vec![42], val); } } @@ -208,19 +205,20 @@ mod tests { // Specify test block parameters. let hashes = [ - Hash256::from(&[0; 32][..]), - Hash256::from(&[1; 32][..]), - Hash256::from(&[2; 32][..]), - Hash256::from(&[3; 32][..]), - Hash256::from(&[4; 32][..]), + Hash256::from([0; 32]), + Hash256::from([1; 32]), + Hash256::from([2; 32]), + Hash256::from([3; 32]), + Hash256::from([4; 32]), ]; let parent_hashes = [ - Hash256::from(&[255; 32][..]), // Genesis block. - Hash256::from(&[0; 32][..]), - Hash256::from(&[1; 32][..]), - Hash256::from(&[2; 32][..]), - Hash256::from(&[3; 32][..]), + Hash256::from([255; 32]), // Genesis block. + Hash256::from([0; 32]), + Hash256::from([1; 32]), + Hash256::from([2; 32]), + Hash256::from([3; 32]), ]; + let unknown_hash = Hash256::from([101; 32]); // different from all above let slots: Vec = vec![0, 1, 3, 4, 5].iter().map(|x| Slot::new(*x)).collect(); // Generate a vec of random blocks and store them in the DB. @@ -233,7 +231,7 @@ mod tests { block.slot = slots[i]; let ssz = ssz_encode(&block); - db.put(DB_COLUMN, &hashes[i], &ssz).unwrap(); + db.put(DB_COLUMN, hashes[i].as_bytes(), &ssz).unwrap(); blocks.push(block); } @@ -255,11 +253,10 @@ mod tests { let ssz = bs.block_at_slot(&hashes[4], Slot::new(6)).unwrap(); assert_eq!(ssz, None); - let bad_hash = &Hash256::from("unknown".as_bytes()); - let ssz = bs.block_at_slot(bad_hash, Slot::new(2)); + let ssz = bs.block_at_slot(&unknown_hash, Slot::new(2)); assert_eq!( ssz, - Err(BeaconBlockAtSlotError::UnknownBeaconBlock(*bad_hash)) + Err(BeaconBlockAtSlotError::UnknownBeaconBlock(unknown_hash)) ); } } diff --git a/beacon_node/db/src/stores/macros.rs b/beacon_node/db/src/stores/macros.rs index 36b8aef8e..6c53e40ee 100644 --- a/beacon_node/db/src/stores/macros.rs +++ b/beacon_node/db/src/stores/macros.rs @@ -2,25 +2,25 @@ macro_rules! impl_crud_for_store { ($store: ident, $db_column: expr) => { impl $store { pub fn put(&self, hash: &Hash256, ssz: &[u8]) -> Result<(), DBError> { - self.db.put($db_column, hash, ssz) + self.db.put($db_column, hash.as_bytes(), ssz) } pub fn get(&self, hash: &Hash256) -> Result>, DBError> { - self.db.get($db_column, hash) + self.db.get($db_column, hash.as_bytes()) } pub fn exists(&self, hash: &Hash256) -> Result { - self.db.exists($db_column, hash) + self.db.exists($db_column, hash.as_bytes()) } pub fn delete(&self, hash: &Hash256) -> Result<(), DBError> { - self.db.delete($db_column, hash) + self.db.delete($db_column, hash.as_bytes()) } } }; } -#[allow(unused_macros)] +#[cfg(test)] macro_rules! test_crud_for_store { ($store: ident, $db_column: expr) => { #[test] @@ -29,10 +29,10 @@ macro_rules! test_crud_for_store { let store = $store::new(db.clone()); let ssz = "some bytes".as_bytes(); - let hash = &Hash256::from("some hash".as_bytes()); + let hash = &Hash256::from([0xAA; 32]); store.put(hash, ssz).unwrap(); - assert_eq!(db.get(DB_COLUMN, hash).unwrap().unwrap(), ssz); + assert_eq!(db.get(DB_COLUMN, hash.as_bytes()).unwrap().unwrap(), ssz); } #[test] @@ -41,9 +41,9 @@ macro_rules! test_crud_for_store { let store = $store::new(db.clone()); let ssz = "some bytes".as_bytes(); - let hash = &Hash256::from("some hash".as_bytes()); + let hash = &Hash256::from([0xAA; 32]); - db.put(DB_COLUMN, hash, ssz).unwrap(); + db.put(DB_COLUMN, hash.as_bytes(), ssz).unwrap(); assert_eq!(store.get(hash).unwrap().unwrap(), ssz); } @@ -53,10 +53,10 @@ macro_rules! test_crud_for_store { let store = $store::new(db.clone()); let ssz = "some bytes".as_bytes(); - let hash = &Hash256::from("some hash".as_bytes()); - let other_hash = &Hash256::from("another hash".as_bytes()); + let hash = &Hash256::from([0xAA; 32]); + let other_hash = &Hash256::from([0xBB; 32]); - db.put(DB_COLUMN, other_hash, ssz).unwrap(); + db.put(DB_COLUMN, other_hash.as_bytes(), ssz).unwrap(); assert_eq!(store.get(hash).unwrap(), None); } @@ -66,9 +66,9 @@ macro_rules! test_crud_for_store { let store = $store::new(db.clone()); let ssz = "some bytes".as_bytes(); - let hash = &Hash256::from("some hash".as_bytes()); + let hash = &Hash256::from([0xAA; 32]); - db.put(DB_COLUMN, hash, ssz).unwrap(); + db.put(DB_COLUMN, hash.as_bytes(), ssz).unwrap(); assert!(store.exists(hash).unwrap()); } @@ -78,10 +78,10 @@ macro_rules! test_crud_for_store { let store = $store::new(db.clone()); let ssz = "some bytes".as_bytes(); - let hash = &Hash256::from("some hash".as_bytes()); - let other_hash = &Hash256::from("another hash".as_bytes()); + let hash = &Hash256::from([0xAA; 32]); + let other_hash = &Hash256::from([0xBB; 32]); - db.put(DB_COLUMN, hash, ssz).unwrap(); + db.put(DB_COLUMN, hash.as_bytes(), ssz).unwrap(); assert!(!store.exists(other_hash).unwrap()); } @@ -91,13 +91,13 @@ macro_rules! test_crud_for_store { let store = $store::new(db.clone()); let ssz = "some bytes".as_bytes(); - let hash = &Hash256::from("some hash".as_bytes()); + let hash = &Hash256::from([0xAA; 32]); - db.put(DB_COLUMN, hash, ssz).unwrap(); - assert!(db.exists(DB_COLUMN, hash).unwrap()); + db.put(DB_COLUMN, hash.as_bytes(), ssz).unwrap(); + assert!(db.exists(DB_COLUMN, hash.as_bytes()).unwrap()); store.delete(hash).unwrap(); - assert!(!db.exists(DB_COLUMN, hash).unwrap()); + assert!(!db.exists(DB_COLUMN, hash.as_bytes()).unwrap()); } }; } diff --git a/beacon_node/db/src/stores/pow_chain_store.rs b/beacon_node/db/src/stores/pow_chain_store.rs index a7c77bab5..5c8b97907 100644 --- a/beacon_node/db/src/stores/pow_chain_store.rs +++ b/beacon_node/db/src/stores/pow_chain_store.rs @@ -37,7 +37,7 @@ mod tests { let db = Arc::new(MemoryDB::open()); let store = PoWChainStore::new(db.clone()); - let hash = &Hash256::from("some hash".as_bytes()).to_vec(); + let hash = &Hash256::from([0xAA; 32]).as_bytes().to_vec(); store.put_block_hash(hash).unwrap(); assert!(db.exists(DB_COLUMN, hash).unwrap()); @@ -48,7 +48,7 @@ mod tests { let db = Arc::new(MemoryDB::open()); let store = PoWChainStore::new(db.clone()); - let hash = &Hash256::from("some hash".as_bytes()).to_vec(); + let hash = &Hash256::from([0xAA; 32]).as_bytes().to_vec(); db.put(DB_COLUMN, hash, &[0]).unwrap(); assert!(store.block_hash_exists(hash).unwrap()); @@ -59,8 +59,8 @@ mod tests { let db = Arc::new(MemoryDB::open()); let store = PoWChainStore::new(db.clone()); - let hash = &Hash256::from("some hash".as_bytes()).to_vec(); - let other_hash = &Hash256::from("another hash".as_bytes()).to_vec(); + let hash = &Hash256::from([0xAA; 32]).as_bytes().to_vec(); + let other_hash = &Hash256::from([0xBB; 32]).as_bytes().to_vec(); db.put(DB_COLUMN, hash, &[0]).unwrap(); assert!(!store.block_hash_exists(other_hash).unwrap()); diff --git a/docs/lighthouse.md b/docs/lighthouse.md index 8ca2387f8..16da13b56 100644 --- a/docs/lighthouse.md +++ b/docs/lighthouse.md @@ -67,7 +67,7 @@ into individual crates wherever possible. Generally, tests can be kept in the same file, as is typical in Rust. Integration tests should be placed in the `tests` directory in the crate's -root. Particularity large (line-count) tests should be placed into a separate +root. Particularly large (line-count) tests should be placed into a separate file. A function is not considered complete until a test exists for it. We produce diff --git a/docs/onboarding.md b/docs/onboarding.md index 8af3b0a83..275f95484 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -122,7 +122,7 @@ project. * **Module**: A collection of items: functions, structs, traits, and even other modules. Modules allow you to hierarchically split code into logical units and manage visibility. -* **Attribute**: Metadaata applied to some module, crate or item. +* **Attribute**: Metadata applied to some module, crate or item. * **Macros**: Macros are powerful meta-programming statements that get expanded into source code that gets compiled with the rest of the code (Unlike `C` macros that are pre-processed, Rust macros form an Abstract Syntax Tree). @@ -185,7 +185,7 @@ check your code. | Function / Method | ``snake_case`` | | Macro Names | ``snake_case`` | | Constants | ``SCREAMING_SNAKE_CASE`` | -| Forbidden name | Trialing Underscore: ``name_`` | +| Forbidden name | Trailing Underscore: ``name_`` | Other general rust docs: diff --git a/eth2/fork_choice/src/bitwise_lmd_ghost.rs b/eth2/fork_choice/src/bitwise_lmd_ghost.rs index c13aacbf4..d50471f56 100644 --- a/eth2/fork_choice/src/bitwise_lmd_ghost.rs +++ b/eth2/fork_choice/src/bitwise_lmd_ghost.rs @@ -210,7 +210,7 @@ where trace!("Child vote length: {}", votes.len()); for (candidate, votes) in votes.iter() { - let candidate_bit: BitVec = BitVec::from_bytes(&candidate); + let candidate_bit: BitVec = BitVec::from_bytes(candidate.as_bytes()); // if the bitmasks don't match, exclude candidate if !bitmask.iter().eq(candidate_bit.iter().take(bit)) { diff --git a/eth2/state_processing/src/block_processable.rs b/eth2/state_processing/src/block_processable.rs new file mode 100644 index 000000000..a54bb96d4 --- /dev/null +++ b/eth2/state_processing/src/block_processable.rs @@ -0,0 +1,455 @@ +use self::verify_slashable_attestation::verify_slashable_attestation; +use crate::SlotProcessingError; +use hashing::hash; +use int_to_bytes::int_to_bytes32; +use log::{debug, trace}; +use ssz::{ssz_encode, TreeHash}; +use types::*; + +mod verify_slashable_attestation; + +const PHASE_0_CUSTODY_BIT: bool = false; + +#[derive(Debug, PartialEq)] +pub enum Error { + DBError(String), + StateAlreadyTransitioned, + PresentSlotIsNone, + UnableToDecodeBlock, + MissingParentState(Hash256), + InvalidParentState(Hash256), + MissingBeaconBlock(Hash256), + InvalidBeaconBlock(Hash256), + MissingParentBlock(Hash256), + StateSlotMismatch, + BadBlockSignature, + BadRandaoSignature, + MaxProposerSlashingsExceeded, + BadProposerSlashing, + MaxAttesterSlashingsExceed, + MaxAttestationsExceeded, + BadAttesterSlashing, + InvalidAttestation(AttestationValidationError), + NoBlockRoot, + MaxDepositsExceeded, + BadDeposit, + MaxExitsExceeded, + BadExit, + BadCustodyReseeds, + BadCustodyChallenges, + BadCustodyResponses, + BeaconStateError(BeaconStateError), + SlotProcessingError(SlotProcessingError), +} + +#[derive(Debug, PartialEq)] +pub enum AttestationValidationError { + IncludedTooEarly, + IncludedTooLate, + WrongJustifiedSlot, + WrongJustifiedRoot, + BadLatestCrosslinkRoot, + BadSignature, + ShardBlockRootNotZero, + NoBlockRoot, + BeaconStateError(BeaconStateError), +} + +macro_rules! ensure { + ($condition: expr, $result: expr) => { + if !$condition { + return Err($result); + } + }; +} + +pub trait BlockProcessable { + fn per_block_processing(&mut self, block: &BeaconBlock, spec: &ChainSpec) -> Result<(), Error>; + fn per_block_processing_without_verifying_block_signature( + &mut self, + block: &BeaconBlock, + spec: &ChainSpec, + ) -> Result<(), Error>; +} + +impl BlockProcessable for BeaconState { + fn per_block_processing(&mut self, block: &BeaconBlock, spec: &ChainSpec) -> Result<(), Error> { + per_block_processing_signature_optional(self, block, true, spec) + } + + fn per_block_processing_without_verifying_block_signature( + &mut self, + block: &BeaconBlock, + spec: &ChainSpec, + ) -> Result<(), Error> { + per_block_processing_signature_optional(self, block, false, spec) + } +} + +fn per_block_processing_signature_optional( + mut state: &mut BeaconState, + block: &BeaconBlock, + verify_block_signature: bool, + spec: &ChainSpec, +) -> Result<(), Error> { + ensure!(block.slot == state.slot, Error::StateSlotMismatch); + + // Building the previous epoch could be delayed until an attestation from a previous epoch is + // included. This is left for future optimisation. + state.build_epoch_cache(RelativeEpoch::Previous, spec)?; + state.build_epoch_cache(RelativeEpoch::Current, spec)?; + + /* + * Proposer Signature + */ + let block_proposer_index = state.get_beacon_proposer_index(block.slot, spec)?; + let block_proposer = &state.validator_registry[block_proposer_index]; + + if verify_block_signature { + ensure!( + bls_verify( + &block_proposer.pubkey, + &block.proposal_root(spec)[..], + &block.signature, + get_domain(&state.fork, state.current_epoch(spec), spec.domain_proposal) + ), + Error::BadBlockSignature + ); + } + + /* + * RANDAO + */ + ensure!( + bls_verify( + &block_proposer.pubkey, + &int_to_bytes32(state.current_epoch(spec).as_u64()), + &block.randao_reveal, + get_domain(&state.fork, state.current_epoch(spec), spec.domain_randao) + ), + Error::BadRandaoSignature + ); + + // TODO: check this is correct. + let new_mix = { + let mut mix = state.latest_randao_mixes + [state.slot.as_usize() % spec.latest_randao_mixes_length] + .as_bytes() + .to_vec(); + mix.append(&mut ssz_encode(&block.randao_reveal)); + Hash256::from_slice(&hash(&mix)[..]) + }; + + state.latest_randao_mixes[state.slot.as_usize() % spec.latest_randao_mixes_length] = new_mix; + + /* + * Eth1 data + */ + // TODO: Eth1 data processing. + + /* + * Proposer slashings + */ + ensure!( + block.body.proposer_slashings.len() as u64 <= spec.max_proposer_slashings, + Error::MaxProposerSlashingsExceeded + ); + for proposer_slashing in &block.body.proposer_slashings { + let proposer = state + .validator_registry + .get(proposer_slashing.proposer_index as usize) + .ok_or(Error::BadProposerSlashing)?; + ensure!( + proposer_slashing.proposal_data_1.slot == proposer_slashing.proposal_data_2.slot, + Error::BadProposerSlashing + ); + ensure!( + proposer_slashing.proposal_data_1.shard == proposer_slashing.proposal_data_2.shard, + Error::BadProposerSlashing + ); + ensure!( + proposer_slashing.proposal_data_1.block_root + != proposer_slashing.proposal_data_2.block_root, + Error::BadProposerSlashing + ); + ensure!( + proposer.penalized_epoch > state.current_epoch(spec), + Error::BadProposerSlashing + ); + ensure!( + bls_verify( + &proposer.pubkey, + &proposer_slashing.proposal_data_1.hash_tree_root(), + &proposer_slashing.proposal_signature_1, + get_domain( + &state.fork, + proposer_slashing + .proposal_data_1 + .slot + .epoch(spec.epoch_length), + spec.domain_proposal + ) + ), + Error::BadProposerSlashing + ); + ensure!( + bls_verify( + &proposer.pubkey, + &proposer_slashing.proposal_data_2.hash_tree_root(), + &proposer_slashing.proposal_signature_2, + get_domain( + &state.fork, + proposer_slashing + .proposal_data_2 + .slot + .epoch(spec.epoch_length), + spec.domain_proposal + ) + ), + Error::BadProposerSlashing + ); + state.penalize_validator(proposer_slashing.proposer_index as usize, spec)?; + } + + /* + * Attester slashings + */ + ensure!( + block.body.attester_slashings.len() as u64 <= spec.max_attester_slashings, + Error::MaxAttesterSlashingsExceed + ); + for attester_slashing in &block.body.attester_slashings { + verify_slashable_attestation(&mut state, &attester_slashing, spec)?; + } + + /* + * Attestations + */ + ensure!( + block.body.attestations.len() as u64 <= spec.max_attestations, + Error::MaxAttestationsExceeded + ); + + debug!("Verifying {} attestations.", block.body.attestations.len()); + + for attestation in &block.body.attestations { + validate_attestation(&state, attestation, spec)?; + + let pending_attestation = PendingAttestation { + data: attestation.data.clone(), + aggregation_bitfield: attestation.aggregation_bitfield.clone(), + custody_bitfield: attestation.custody_bitfield.clone(), + inclusion_slot: state.slot, + }; + state.latest_attestations.push(pending_attestation); + } + + /* + * Deposits + */ + ensure!( + block.body.deposits.len() as u64 <= spec.max_deposits, + Error::MaxDepositsExceeded + ); + + // TODO: verify deposit merkle branches. + for deposit in &block.body.deposits { + debug!( + "Processing deposit for pubkey {:?}", + deposit.deposit_data.deposit_input.pubkey + ); + state + .process_deposit( + deposit.deposit_data.deposit_input.pubkey.clone(), + deposit.deposit_data.amount, + deposit + .deposit_data + .deposit_input + .proof_of_possession + .clone(), + deposit.deposit_data.deposit_input.withdrawal_credentials, + None, + spec, + ) + .map_err(|_| Error::BadDeposit)?; + } + + /* + * Exits + */ + ensure!( + block.body.exits.len() as u64 <= spec.max_exits, + Error::MaxExitsExceeded + ); + + for exit in &block.body.exits { + let validator = state + .validator_registry + .get(exit.validator_index as usize) + .ok_or(Error::BadExit)?; + ensure!( + validator.exit_epoch + > state.get_entry_exit_effect_epoch(state.current_epoch(spec), spec), + Error::BadExit + ); + ensure!(state.current_epoch(spec) >= exit.epoch, Error::BadExit); + let exit_message = { + let exit_struct = Exit { + epoch: exit.epoch, + validator_index: exit.validator_index, + signature: spec.empty_signature.clone(), + }; + exit_struct.hash_tree_root() + }; + ensure!( + bls_verify( + &validator.pubkey, + &exit_message, + &exit.signature, + get_domain(&state.fork, exit.epoch, spec.domain_exit) + ), + Error::BadProposerSlashing + ); + state.initiate_validator_exit(exit.validator_index as usize); + } + + debug!("State transition complete."); + + Ok(()) +} + +pub fn validate_attestation( + state: &BeaconState, + attestation: &Attestation, + spec: &ChainSpec, +) -> Result<(), AttestationValidationError> { + validate_attestation_signature_optional(state, attestation, spec, true) +} + +pub fn validate_attestation_without_signature( + state: &BeaconState, + attestation: &Attestation, + spec: &ChainSpec, +) -> Result<(), AttestationValidationError> { + validate_attestation_signature_optional(state, attestation, spec, false) +} + +fn validate_attestation_signature_optional( + state: &BeaconState, + attestation: &Attestation, + spec: &ChainSpec, + verify_signature: bool, +) -> Result<(), AttestationValidationError> { + trace!( + "validate_attestation_signature_optional: attestation epoch: {}", + attestation.data.slot.epoch(spec.epoch_length) + ); + ensure!( + attestation.data.slot + spec.min_attestation_inclusion_delay <= state.slot, + AttestationValidationError::IncludedTooEarly + ); + ensure!( + attestation.data.slot + spec.epoch_length >= state.slot, + AttestationValidationError::IncludedTooLate + ); + if attestation.data.slot >= state.current_epoch_start_slot(spec) { + ensure!( + attestation.data.justified_epoch == state.justified_epoch, + AttestationValidationError::WrongJustifiedSlot + ); + } else { + ensure!( + attestation.data.justified_epoch == state.previous_justified_epoch, + AttestationValidationError::WrongJustifiedSlot + ); + } + ensure!( + attestation.data.justified_block_root + == *state + .get_block_root( + attestation + .data + .justified_epoch + .start_slot(spec.epoch_length), + &spec + ) + .ok_or(AttestationValidationError::NoBlockRoot)?, + AttestationValidationError::WrongJustifiedRoot + ); + let potential_crosslink = Crosslink { + shard_block_root: attestation.data.shard_block_root, + epoch: attestation.data.slot.epoch(spec.epoch_length), + }; + ensure!( + (attestation.data.latest_crosslink + == state.latest_crosslinks[attestation.data.shard as usize]) + | (attestation.data.latest_crosslink == potential_crosslink), + AttestationValidationError::BadLatestCrosslinkRoot + ); + if verify_signature { + let participants = state.get_attestation_participants( + &attestation.data, + &attestation.aggregation_bitfield, + spec, + )?; + trace!( + "slot: {}, shard: {}, participants: {:?}", + attestation.data.slot, + attestation.data.shard, + participants + ); + let mut group_public_key = AggregatePublicKey::new(); + for participant in participants { + group_public_key.add(&state.validator_registry[participant as usize].pubkey) + } + ensure!( + attestation.verify_signature( + &group_public_key, + PHASE_0_CUSTODY_BIT, + get_domain( + &state.fork, + attestation.data.slot.epoch(spec.epoch_length), + spec.domain_attestation, + ) + ), + AttestationValidationError::BadSignature + ); + } + ensure!( + attestation.data.shard_block_root == spec.zero_hash, + AttestationValidationError::ShardBlockRootNotZero + ); + Ok(()) +} + +fn get_domain(fork: &Fork, epoch: Epoch, domain_type: u64) -> u64 { + fork.get_domain(epoch, domain_type) +} + +fn bls_verify(pubkey: &PublicKey, message: &[u8], signature: &Signature, domain: u64) -> bool { + signature.verify(message, domain, pubkey) +} + +impl From for Error { + fn from(e: AttestationValidationError) -> Error { + Error::InvalidAttestation(e) + } +} + +impl From for Error { + fn from(e: BeaconStateError) -> Error { + Error::BeaconStateError(e) + } +} + +impl From for Error { + fn from(e: SlotProcessingError) -> Error { + Error::SlotProcessingError(e) + } +} + +impl From for AttestationValidationError { + fn from(e: BeaconStateError) -> AttestationValidationError { + AttestationValidationError::BeaconStateError(e) + } +} diff --git a/eth2/state_processing/src/epoch_processable.rs b/eth2/state_processing/src/epoch_processable.rs new file mode 100644 index 000000000..ff6f18113 --- /dev/null +++ b/eth2/state_processing/src/epoch_processable.rs @@ -0,0 +1,723 @@ +use integer_sqrt::IntegerSquareRoot; +use log::{debug, trace}; +use rayon::prelude::*; +use ssz::TreeHash; +use std::collections::{HashMap, HashSet}; +use std::iter::FromIterator; +use types::{ + validator_registry::get_active_validator_indices, BeaconState, BeaconStateError, ChainSpec, + Crosslink, Epoch, Hash256, InclusionError, PendingAttestation, RelativeEpoch, +}; + +mod tests; + +macro_rules! safe_add_assign { + ($a: expr, $b: expr) => { + $a = $a.saturating_add($b); + }; +} +macro_rules! safe_sub_assign { + ($a: expr, $b: expr) => { + $a = $a.saturating_sub($b); + }; +} + +#[derive(Debug, PartialEq)] +pub enum Error { + UnableToDetermineProducer, + NoBlockRoots, + BaseRewardQuotientIsZero, + NoRandaoSeed, + BeaconStateError(BeaconStateError), + InclusionError(InclusionError), + WinningRootError(WinningRootError), +} + +#[derive(Debug, PartialEq)] +pub enum WinningRootError { + NoWinningRoot, + BeaconStateError(BeaconStateError), +} + +#[derive(Clone)] +pub struct WinningRoot { + pub shard_block_root: Hash256, + pub attesting_validator_indices: Vec, + pub total_balance: u64, + pub total_attesting_balance: u64, +} + +pub trait EpochProcessable { + fn per_epoch_processing(&mut self, spec: &ChainSpec) -> Result<(), Error>; +} + +impl EpochProcessable for BeaconState { + // Cyclomatic complexity is ignored. It would be ideal to split this function apart, however it + // remains monolithic to allow for easier spec updates. Once the spec is more stable we can + // optimise. + #[allow(clippy::cyclomatic_complexity)] + fn per_epoch_processing(&mut self, spec: &ChainSpec) -> Result<(), Error> { + let current_epoch = self.current_epoch(spec); + let previous_epoch = self.previous_epoch(spec); + let next_epoch = self.next_epoch(spec); + + debug!( + "Starting per-epoch processing on epoch {}...", + self.current_epoch(spec) + ); + + // Ensure all of the caches are built. + self.build_epoch_cache(RelativeEpoch::Previous, spec)?; + self.build_epoch_cache(RelativeEpoch::Current, spec)?; + self.build_epoch_cache(RelativeEpoch::Next, spec)?; + + /* + * Validators attesting during the current epoch. + */ + let active_validator_indices = get_active_validator_indices( + &self.validator_registry, + self.slot.epoch(spec.epoch_length), + ); + let current_total_balance = self.get_total_balance(&active_validator_indices[..], spec); + + trace!( + "{} validators with a total balance of {} wei.", + active_validator_indices.len(), + current_total_balance + ); + + let current_epoch_attestations: Vec<&PendingAttestation> = self + .latest_attestations + .par_iter() + .filter(|a| { + (a.data.slot / spec.epoch_length).epoch(spec.epoch_length) + == self.current_epoch(spec) + }) + .collect(); + + trace!( + "Current epoch attestations: {}", + current_epoch_attestations.len() + ); + + let current_epoch_boundary_attestations: Vec<&PendingAttestation> = + current_epoch_attestations + .par_iter() + .filter( + |a| match self.get_block_root(self.current_epoch_start_slot(spec), spec) { + Some(block_root) => { + (a.data.epoch_boundary_root == *block_root) + && (a.data.justified_epoch == self.justified_epoch) + } + None => unreachable!(), + }, + ) + .cloned() + .collect(); + + let current_epoch_boundary_attester_indices = self + .get_attestation_participants_union(¤t_epoch_boundary_attestations[..], spec)?; + let current_epoch_boundary_attesting_balance = + self.get_total_balance(¤t_epoch_boundary_attester_indices[..], spec); + + trace!( + "Current epoch boundary attesters: {}", + current_epoch_boundary_attester_indices.len() + ); + + /* + * Validators attesting during the previous epoch + */ + + /* + * Validators that made an attestation during the previous epoch + */ + let previous_epoch_attestations: Vec<&PendingAttestation> = self + .latest_attestations + .par_iter() + .filter(|a| { + //TODO: ensure these saturating subs are correct. + (a.data.slot / spec.epoch_length).epoch(spec.epoch_length) + == self.previous_epoch(spec) + }) + .collect(); + + debug!( + "previous epoch attestations: {}", + previous_epoch_attestations.len() + ); + + let previous_epoch_attester_indices = + self.get_attestation_participants_union(&previous_epoch_attestations[..], spec)?; + let previous_total_balance = self.get_total_balance( + &get_active_validator_indices(&self.validator_registry, previous_epoch), + spec, + ); + + /* + * Validators targetting the previous justified slot + */ + let previous_epoch_justified_attestations: Vec<&PendingAttestation> = { + let mut a: Vec<&PendingAttestation> = current_epoch_attestations + .iter() + .filter(|a| a.data.justified_epoch == self.previous_justified_epoch) + .cloned() + .collect(); + let mut b: Vec<&PendingAttestation> = previous_epoch_attestations + .iter() + .filter(|a| a.data.justified_epoch == self.previous_justified_epoch) + .cloned() + .collect(); + a.append(&mut b); + a + }; + + let previous_epoch_justified_attester_indices = self + .get_attestation_participants_union(&previous_epoch_justified_attestations[..], spec)?; + let previous_epoch_justified_attesting_balance = + self.get_total_balance(&previous_epoch_justified_attester_indices[..], spec); + + /* + * Validators justifying the epoch boundary block at the start of the previous epoch + */ + let previous_epoch_boundary_attestations: Vec<&PendingAttestation> = + previous_epoch_justified_attestations + .iter() + .filter( + |a| match self.get_block_root(self.previous_epoch_start_slot(spec), spec) { + Some(block_root) => a.data.epoch_boundary_root == *block_root, + None => unreachable!(), + }, + ) + .cloned() + .collect(); + + let previous_epoch_boundary_attester_indices = self + .get_attestation_participants_union(&previous_epoch_boundary_attestations[..], spec)?; + let previous_epoch_boundary_attesting_balance = + self.get_total_balance(&previous_epoch_boundary_attester_indices[..], spec); + + /* + * Validators attesting to the expected beacon chain head during the previous epoch. + */ + let previous_epoch_head_attestations: Vec<&PendingAttestation> = + previous_epoch_attestations + .iter() + .filter(|a| match self.get_block_root(a.data.slot, spec) { + Some(block_root) => a.data.beacon_block_root == *block_root, + None => unreachable!(), + }) + .cloned() + .collect(); + + let previous_epoch_head_attester_indices = + self.get_attestation_participants_union(&previous_epoch_head_attestations[..], spec)?; + let previous_epoch_head_attesting_balance = + self.get_total_balance(&previous_epoch_head_attester_indices[..], spec); + + debug!( + "previous_epoch_head_attester_balance of {} wei.", + previous_epoch_head_attesting_balance + ); + + /* + * Eth1 Data + */ + if self.next_epoch(spec) % spec.eth1_data_voting_period == 0 { + for eth1_data_vote in &self.eth1_data_votes { + if eth1_data_vote.vote_count * 2 > spec.eth1_data_voting_period { + self.latest_eth1_data = eth1_data_vote.eth1_data.clone(); + } + } + self.eth1_data_votes = vec![]; + } + + /* + * Justification + */ + + let mut new_justified_epoch = self.justified_epoch; + self.justification_bitfield <<= 1; + + // If > 2/3 of the total balance attested to the previous epoch boundary + // + // - Set the 2nd bit of the bitfield. + // - Set the previous epoch to be justified. + if (3 * previous_epoch_boundary_attesting_balance) >= (2 * current_total_balance) { + self.justification_bitfield |= 2; + new_justified_epoch = previous_epoch; + trace!(">= 2/3 voted for previous epoch boundary"); + } + // If > 2/3 of the total balance attested to the previous epoch boundary + // + // - Set the 1st bit of the bitfield. + // - Set the current epoch to be justified. + if (3 * current_epoch_boundary_attesting_balance) >= (2 * current_total_balance) { + self.justification_bitfield |= 1; + new_justified_epoch = current_epoch; + trace!(">= 2/3 voted for current epoch boundary"); + } + + // If: + // + // - All three epochs prior to this epoch have been justified. + // - The previous justified justified epoch was three epochs ago. + // + // Then, set the finalized epoch to be three epochs ago. + if ((self.justification_bitfield >> 1) % 8 == 0b111) + & (self.previous_justified_epoch == previous_epoch - 2) + { + self.finalized_epoch = self.previous_justified_epoch; + trace!("epoch - 3 was finalized (1st condition)."); + } + // If: + // + // - Both two epochs prior to this epoch have been justified. + // - The previous justified epoch was two epochs ago. + // + // Then, set the finalized epoch to two epochs ago. + if ((self.justification_bitfield >> 1) % 4 == 0b11) + & (self.previous_justified_epoch == previous_epoch - 1) + { + self.finalized_epoch = self.previous_justified_epoch; + trace!("epoch - 2 was finalized (2nd condition)."); + } + // If: + // + // - This epoch and the two prior have been justified. + // - The presently justified epoch was two epochs ago. + // + // Then, set the finalized epoch to two epochs ago. + if (self.justification_bitfield % 8 == 0b111) & (self.justified_epoch == previous_epoch - 1) + { + self.finalized_epoch = self.justified_epoch; + trace!("epoch - 2 was finalized (3rd condition)."); + } + // If: + // + // - This epoch and the epoch prior to it have been justified. + // - Set the previous epoch to be justified. + // + // Then, set the finalized epoch to be the previous epoch. + if (self.justification_bitfield % 4 == 0b11) & (self.justified_epoch == previous_epoch) { + self.finalized_epoch = self.justified_epoch; + trace!("epoch - 1 was finalized (4th condition)."); + } + + self.previous_justified_epoch = self.justified_epoch; + self.justified_epoch = new_justified_epoch; + + debug!( + "Finalized epoch {}, justified epoch {}.", + self.finalized_epoch, self.justified_epoch + ); + + /* + * Crosslinks + */ + + // Cached for later lookups. + let mut winning_root_for_shards: HashMap> = + HashMap::new(); + + // for slot in self.slot.saturating_sub(2 * spec.epoch_length)..self.slot { + for slot in self.previous_epoch(spec).slot_iter(spec.epoch_length) { + trace!( + "Finding winning root for slot: {} (epoch: {})", + slot, + slot.epoch(spec.epoch_length) + ); + + // Clone is used to remove the borrow. It becomes an issue later when trying to mutate + // `self.balances`. + let crosslink_committees_at_slot = + self.get_crosslink_committees_at_slot(slot, spec)?.clone(); + + for (crosslink_committee, shard) in crosslink_committees_at_slot { + let shard = shard as u64; + + let winning_root = winning_root( + self, + shard, + ¤t_epoch_attestations, + &previous_epoch_attestations, + spec, + ); + + if let Ok(winning_root) = &winning_root { + let total_committee_balance = + self.get_total_balance(&crosslink_committee[..], spec); + + if (3 * winning_root.total_attesting_balance) >= (2 * total_committee_balance) { + self.latest_crosslinks[shard as usize] = Crosslink { + epoch: current_epoch, + shard_block_root: winning_root.shard_block_root, + } + } + } + winning_root_for_shards.insert(shard, winning_root); + } + } + + trace!( + "Found {} winning shard roots.", + winning_root_for_shards.len() + ); + + /* + * Rewards and Penalities + */ + let base_reward_quotient = + previous_total_balance.integer_sqrt() / spec.base_reward_quotient; + if base_reward_quotient == 0 { + return Err(Error::BaseRewardQuotientIsZero); + } + + /* + * Justification and finalization + */ + let epochs_since_finality = next_epoch - self.finalized_epoch; + + let previous_epoch_justified_attester_indices_hashset: HashSet = + HashSet::from_iter(previous_epoch_justified_attester_indices.iter().cloned()); + let previous_epoch_boundary_attester_indices_hashset: HashSet = + HashSet::from_iter(previous_epoch_boundary_attester_indices.iter().cloned()); + let previous_epoch_head_attester_indices_hashset: HashSet = + HashSet::from_iter(previous_epoch_head_attester_indices.iter().cloned()); + let previous_epoch_attester_indices_hashset: HashSet = + HashSet::from_iter(previous_epoch_attester_indices.iter().cloned()); + let active_validator_indices_hashset: HashSet = + HashSet::from_iter(active_validator_indices.iter().cloned()); + + debug!("previous epoch justified attesters: {}, previous epoch boundary attesters: {}, previous epoch head attesters: {}, previous epoch attesters: {}", previous_epoch_justified_attester_indices.len(), previous_epoch_boundary_attester_indices.len(), previous_epoch_head_attester_indices.len(), previous_epoch_attester_indices.len()); + + debug!("{} epochs since finality.", epochs_since_finality); + + if epochs_since_finality <= 4 { + for index in 0..self.validator_balances.len() { + let base_reward = self.base_reward(index, base_reward_quotient, spec); + + if previous_epoch_justified_attester_indices_hashset.contains(&index) { + safe_add_assign!( + self.validator_balances[index], + base_reward * previous_epoch_justified_attesting_balance + / previous_total_balance + ); + } else if active_validator_indices_hashset.contains(&index) { + safe_sub_assign!(self.validator_balances[index], base_reward); + } + + if previous_epoch_boundary_attester_indices_hashset.contains(&index) { + safe_add_assign!( + self.validator_balances[index], + base_reward * previous_epoch_boundary_attesting_balance + / previous_total_balance + ); + } else if active_validator_indices_hashset.contains(&index) { + safe_sub_assign!(self.validator_balances[index], base_reward); + } + + if previous_epoch_head_attester_indices_hashset.contains(&index) { + safe_add_assign!( + self.validator_balances[index], + base_reward * previous_epoch_head_attesting_balance + / previous_total_balance + ); + } else if active_validator_indices_hashset.contains(&index) { + safe_sub_assign!(self.validator_balances[index], base_reward); + } + } + + for index in previous_epoch_attester_indices { + let base_reward = self.base_reward(index, base_reward_quotient, spec); + let inclusion_distance = + self.inclusion_distance(&previous_epoch_attestations, index, spec)?; + + safe_add_assign!( + self.validator_balances[index], + base_reward * spec.min_attestation_inclusion_delay / inclusion_distance + ) + } + } else { + for index in 0..self.validator_balances.len() { + let inactivity_penalty = self.inactivity_penalty( + index, + epochs_since_finality, + base_reward_quotient, + spec, + ); + if active_validator_indices_hashset.contains(&index) { + if !previous_epoch_justified_attester_indices_hashset.contains(&index) { + safe_sub_assign!(self.validator_balances[index], inactivity_penalty); + } + if !previous_epoch_boundary_attester_indices_hashset.contains(&index) { + safe_sub_assign!(self.validator_balances[index], inactivity_penalty); + } + if !previous_epoch_head_attester_indices_hashset.contains(&index) { + safe_sub_assign!(self.validator_balances[index], inactivity_penalty); + } + + if self.validator_registry[index].penalized_epoch <= current_epoch { + let base_reward = self.base_reward(index, base_reward_quotient, spec); + safe_sub_assign!( + self.validator_balances[index], + 2 * inactivity_penalty + base_reward + ); + } + } + } + + for index in previous_epoch_attester_indices { + let base_reward = self.base_reward(index, base_reward_quotient, spec); + let inclusion_distance = + self.inclusion_distance(&previous_epoch_attestations, index, spec)?; + + safe_sub_assign!( + self.validator_balances[index], + base_reward + - base_reward * spec.min_attestation_inclusion_delay / inclusion_distance + ); + } + } + + trace!("Processed validator justification and finalization rewards/penalities."); + + /* + * Attestation inclusion + */ + for &index in &previous_epoch_attester_indices_hashset { + let inclusion_slot = + self.inclusion_slot(&previous_epoch_attestations[..], index, spec)?; + let proposer_index = self + .get_beacon_proposer_index(inclusion_slot, spec) + .map_err(|_| Error::UnableToDetermineProducer)?; + let base_reward = self.base_reward(proposer_index, base_reward_quotient, spec); + safe_add_assign!( + self.validator_balances[proposer_index], + base_reward / spec.includer_reward_quotient + ); + } + + trace!( + "Previous epoch attesters: {}.", + previous_epoch_attester_indices_hashset.len() + ); + + /* + * Crosslinks + */ + for slot in self.previous_epoch(spec).slot_iter(spec.epoch_length) { + // Clone is used to remove the borrow. It becomes an issue later when trying to mutate + // `self.balances`. + let crosslink_committees_at_slot = + self.get_crosslink_committees_at_slot(slot, spec)?.clone(); + + for (_crosslink_committee, shard) in crosslink_committees_at_slot { + let shard = shard as u64; + + if let Some(Ok(winning_root)) = winning_root_for_shards.get(&shard) { + // TODO: remove the map. + let attesting_validator_indices: HashSet = HashSet::from_iter( + winning_root.attesting_validator_indices.iter().cloned(), + ); + + for index in 0..self.validator_balances.len() { + let base_reward = self.base_reward(index, base_reward_quotient, spec); + + if attesting_validator_indices.contains(&index) { + safe_add_assign!( + self.validator_balances[index], + base_reward * winning_root.total_attesting_balance + / winning_root.total_balance + ); + } else { + safe_sub_assign!(self.validator_balances[index], base_reward); + } + } + + for index in &winning_root.attesting_validator_indices { + let base_reward = self.base_reward(*index, base_reward_quotient, spec); + safe_add_assign!( + self.validator_balances[*index], + base_reward * winning_root.total_attesting_balance + / winning_root.total_balance + ); + } + } + } + } + + /* + * Ejections + */ + self.process_ejections(spec); + + /* + * Validator Registry + */ + self.previous_calculation_epoch = self.current_calculation_epoch; + self.previous_epoch_start_shard = self.current_epoch_start_shard; + + debug!( + "setting previous_epoch_seed to : {}", + self.current_epoch_seed + ); + + self.previous_epoch_seed = self.current_epoch_seed; + + let should_update_validator_registy = if self.finalized_epoch + > self.validator_registry_update_epoch + { + (0..self.get_current_epoch_committee_count(spec)).all(|i| { + let shard = (self.current_epoch_start_shard + i as u64) % spec.shard_count; + self.latest_crosslinks[shard as usize].epoch > self.validator_registry_update_epoch + }) + } else { + false + }; + + if should_update_validator_registy { + trace!("updating validator registry."); + self.update_validator_registry(spec); + + self.current_calculation_epoch = next_epoch; + self.current_epoch_start_shard = (self.current_epoch_start_shard + + self.get_current_epoch_committee_count(spec) as u64) + % spec.shard_count; + self.current_epoch_seed = self.generate_seed(self.current_calculation_epoch, spec)? + } else { + trace!("not updating validator registry."); + let epochs_since_last_registry_update = + current_epoch - self.validator_registry_update_epoch; + if (epochs_since_last_registry_update > 1) + & epochs_since_last_registry_update.is_power_of_two() + { + self.current_calculation_epoch = next_epoch; + self.current_epoch_seed = + self.generate_seed(self.current_calculation_epoch, spec)? + } + } + + self.process_penalties_and_exits(spec); + + self.latest_index_roots[(next_epoch.as_usize() + spec.entry_exit_delay as usize) + % spec.latest_index_roots_length] = hash_tree_root(get_active_validator_indices( + &self.validator_registry, + next_epoch + Epoch::from(spec.entry_exit_delay), + )); + self.latest_penalized_balances[next_epoch.as_usize() % spec.latest_penalized_exit_length] = + self.latest_penalized_balances + [current_epoch.as_usize() % spec.latest_penalized_exit_length]; + self.latest_randao_mixes[next_epoch.as_usize() % spec.latest_randao_mixes_length] = self + .get_randao_mix(current_epoch, spec) + .and_then(|x| Some(*x)) + .ok_or_else(|| Error::NoRandaoSeed)?; + self.latest_attestations = self + .latest_attestations + .iter() + .filter(|a| a.data.slot.epoch(spec.epoch_length) >= current_epoch) + .cloned() + .collect(); + + debug!("Epoch transition complete."); + + Ok(()) + } +} + +fn hash_tree_root(input: Vec) -> Hash256 { + Hash256::from_slice(&input.hash_tree_root()[..]) +} + +fn winning_root( + state: &BeaconState, + shard: u64, + current_epoch_attestations: &[&PendingAttestation], + previous_epoch_attestations: &[&PendingAttestation], + spec: &ChainSpec, +) -> Result { + let mut attestations = current_epoch_attestations.to_vec(); + attestations.append(&mut previous_epoch_attestations.to_vec()); + + let mut candidates: HashMap = HashMap::new(); + + let mut highest_seen_balance = 0; + + for a in &attestations { + if a.data.shard != shard { + continue; + } + + let shard_block_root = &a.data.shard_block_root; + + if candidates.contains_key(shard_block_root) { + continue; + } + + let attesting_validator_indices = attestations + .iter() + .try_fold::<_, _, Result<_, BeaconStateError>>(vec![], |mut acc, a| { + if (a.data.shard == shard) && (a.data.shard_block_root == *shard_block_root) { + acc.append(&mut state.get_attestation_participants( + &a.data, + &a.aggregation_bitfield, + spec, + )?); + } + Ok(acc) + })?; + + let total_balance: u64 = attesting_validator_indices + .iter() + .fold(0, |acc, i| acc + state.get_effective_balance(*i, spec)); + + let total_attesting_balance: u64 = attesting_validator_indices + .iter() + .fold(0, |acc, i| acc + state.get_effective_balance(*i, spec)); + + if total_attesting_balance > highest_seen_balance { + highest_seen_balance = total_attesting_balance; + } + + let candidate_root = WinningRoot { + shard_block_root: *shard_block_root, + attesting_validator_indices, + total_attesting_balance, + total_balance, + }; + + candidates.insert(*shard_block_root, candidate_root); + } + + Ok(candidates + .iter() + .filter_map(|(_hash, candidate)| { + if candidate.total_attesting_balance == highest_seen_balance { + Some(candidate) + } else { + None + } + }) + .min_by_key(|candidate| candidate.shard_block_root) + .ok_or_else(|| WinningRootError::NoWinningRoot)? + // TODO: avoid clone. + .clone()) +} + +impl From for Error { + fn from(e: InclusionError) -> Error { + Error::InclusionError(e) + } +} + +impl From for Error { + fn from(e: BeaconStateError) -> Error { + Error::BeaconStateError(e) + } +} + +impl From for WinningRootError { + fn from(e: BeaconStateError) -> WinningRootError { + WinningRootError::BeaconStateError(e) + } +} diff --git a/eth2/state_processing/src/per_block_processing.rs b/eth2/state_processing/src/per_block_processing.rs index fc3a8204e..06a5f81c7 100644 --- a/eth2/state_processing/src/per_block_processing.rs +++ b/eth2/state_processing/src/per_block_processing.rs @@ -99,7 +99,7 @@ pub fn verify_block_signature( let proposal = Proposal { slot: block.slot, shard: spec.beacon_chain_shard_number, - block_root: Hash256::from(&block.signed_root()[..]), + block_root: Hash256::from_slice(&block.signed_root()[..]), signature: block.signature.clone(), }; let domain = spec.get_domain( @@ -187,7 +187,7 @@ pub fn update_randao( ) -> Result<(), BeaconStateError> { let hashed_reveal = { let encoded_signature = ssz_encode(reveal); - Hash256::from(&hash(&encoded_signature[..])[..]) + Hash256::from_slice(&hash(&encoded_signature[..])[..]) }; let current_epoch = state.slot.epoch(spec.slots_per_epoch); diff --git a/eth2/state_processing/src/per_epoch_processing.rs b/eth2/state_processing/src/per_epoch_processing.rs index 732a6e48f..bd8aca3c4 100644 --- a/eth2/state_processing/src/per_epoch_processing.rs +++ b/eth2/state_processing/src/per_epoch_processing.rs @@ -84,7 +84,7 @@ pub fn per_epoch_processing(state: &mut BeaconState, spec: &ChainSpec) -> Result .hash_tree_root(); state.latest_active_index_roots[(next_epoch.as_usize() + spec.activation_exit_delay as usize) - % spec.latest_active_index_roots_length] = Hash256::from(&active_tree_root[..]); + % spec.latest_active_index_roots_length] = Hash256::from_slice(&active_tree_root[..]); state.latest_slashed_balances[next_epoch.as_usize() % spec.latest_slashed_exit_length] = state.latest_slashed_balances[current_epoch.as_usize() % spec.latest_slashed_exit_length]; diff --git a/eth2/types/Cargo.toml b/eth2/types/Cargo.toml index f611aa3c1..ea1343dba 100644 --- a/eth2/types/Cargo.toml +++ b/eth2/types/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [dependencies] bls = { path = "../utils/bls" } boolean-bitfield = { path = "../utils/boolean-bitfield" } -ethereum-types = "0.4.0" +ethereum-types = "0.5" hashing = { path = "../utils/hashing" } honey-badger-split = { path = "../utils/honey-badger-split" } int_to_bytes = { path = "../utils/int_to_bytes" } diff --git a/eth2/types/src/attester_slashing/builder.rs b/eth2/types/src/attester_slashing/builder.rs index bdb46b154..46dcaf174 100644 --- a/eth2/types/src/attester_slashing/builder.rs +++ b/eth2/types/src/attester_slashing/builder.rs @@ -27,8 +27,8 @@ impl AttesterSlashingBuilder { let shard = 0; let justified_epoch = Epoch::new(0); let epoch = Epoch::new(0); - let hash_1 = Hash256::from(&[1][..]); - let hash_2 = Hash256::from(&[2][..]); + let hash_1 = Hash256::from_low_u64_le(1); + let hash_2 = Hash256::from_low_u64_le(2); let mut slashable_attestation_1 = SlashableAttestation { validator_indices: validator_indices.to_vec(), diff --git a/eth2/types/src/beacon_state.rs b/eth2/types/src/beacon_state.rs index bb77981ab..ac8cf164d 100644 --- a/eth2/types/src/beacon_state.rs +++ b/eth2/types/src/beacon_state.rs @@ -469,18 +469,20 @@ impl BeaconState { let mut input = self .get_randao_mix(epoch - spec.min_seed_lookahead, spec) .ok_or_else(|| Error::InsufficientRandaoMixes)? + .as_bytes() .to_vec(); input.append( &mut self .get_active_index_root(epoch, spec) .ok_or_else(|| Error::InsufficientIndexRoots)? + .as_bytes() .to_vec(), ); input.append(&mut int_to_bytes32(epoch.as_u64())); - Ok(Hash256::from(&hash(&input[..])[..])) + Ok(Hash256::from_slice(&hash(&input[..])[..])) } /// Returns the beacon proposer index for the `slot`. @@ -1155,7 +1157,7 @@ impl BeaconState { } fn hash_tree_root(input: Vec) -> Hash256 { - Hash256::from(&input.hash_tree_root()[..]) + Hash256::from_slice(&input.hash_tree_root()[..]) } impl Encodable for BeaconState { diff --git a/eth2/types/src/beacon_state/builder.rs b/eth2/types/src/beacon_state/builder.rs index dadce9cac..0be297db7 100644 --- a/eth2/types/src/beacon_state/builder.rs +++ b/eth2/types/src/beacon_state/builder.rs @@ -145,8 +145,8 @@ impl BeaconStateBuilder { state.previous_shuffling_epoch = epoch - 1; state.current_shuffling_epoch = epoch; - state.previous_shuffling_seed = Hash256::from(&b"previous_seed"[..]); - state.current_shuffling_seed = Hash256::from(&b"current_seed"[..]); + state.previous_shuffling_seed = Hash256::from_low_u64_le(0); + state.current_shuffling_seed = Hash256::from_low_u64_le(1); state.previous_justified_epoch = epoch - 2; state.justified_epoch = epoch - 1; diff --git a/eth2/types/src/proposal_signed_data.rs b/eth2/types/src/proposal_signed_data.rs new file mode 100644 index 000000000..58f45a41d --- /dev/null +++ b/eth2/types/src/proposal_signed_data.rs @@ -0,0 +1,20 @@ +use crate::test_utils::TestRandom; +use crate::{Hash256, Slot}; +use rand::RngCore; +use serde_derive::Serialize; +use ssz_derive::{Decode, Encode, TreeHash}; +use test_random_derive::TestRandom; + +#[derive(Debug, PartialEq, Clone, Default, Serialize, Encode, Decode, TreeHash, TestRandom)] +pub struct ProposalSignedData { + pub slot: Slot, + pub shard: u64, + pub block_root: Hash256, +} + +#[cfg(test)] +mod tests { + use super::*; + + ssz_tests!(ProposalSignedData); +} diff --git a/eth2/types/src/proposer_slashing/builder.rs b/eth2/types/src/proposer_slashing/builder.rs index 1110bdf30..472a76ec1 100644 --- a/eth2/types/src/proposer_slashing/builder.rs +++ b/eth2/types/src/proposer_slashing/builder.rs @@ -25,14 +25,14 @@ impl ProposerSlashingBuilder { let mut proposal_1 = Proposal { slot, shard, - block_root: Hash256::from(&[1][..]), + block_root: Hash256::from_low_u64_le(1), signature: Signature::empty_signature(), }; let mut proposal_2 = Proposal { slot, shard, - block_root: Hash256::from(&[2][..]), + block_root: Hash256::from_low_u64_le(2), signature: Signature::empty_signature(), }; diff --git a/eth2/types/src/slashable_vote_data.rs b/eth2/types/src/slashable_vote_data.rs new file mode 100644 index 000000000..73cf91c61 --- /dev/null +++ b/eth2/types/src/slashable_vote_data.rs @@ -0,0 +1,132 @@ +use super::AttestationData; +use crate::chain_spec::ChainSpec; +use crate::test_utils::TestRandom; +use bls::AggregateSignature; +use rand::RngCore; +use serde_derive::Serialize; +use ssz_derive::{Decode, Encode, TreeHash}; +use test_random_derive::TestRandom; + +#[derive(Debug, PartialEq, Clone, Serialize, Encode, Decode, TreeHash, TestRandom)] +pub struct SlashableVoteData { + pub custody_bit_0_indices: Vec, + pub custody_bit_1_indices: Vec, + pub data: AttestationData, + pub aggregate_signature: AggregateSignature, +} + +impl SlashableVoteData { + /// Check if ``attestation_data_1`` and ``attestation_data_2`` have the same target. + /// + /// Spec v0.3.0 + pub fn is_double_vote(&self, other: &SlashableVoteData, spec: &ChainSpec) -> bool { + self.data.slot.epoch(spec.epoch_length) == other.data.slot.epoch(spec.epoch_length) + } + + /// Check if ``attestation_data_1`` surrounds ``attestation_data_2``. + /// + /// Spec v0.3.0 + pub fn is_surround_vote(&self, other: &SlashableVoteData, spec: &ChainSpec) -> bool { + let source_epoch_1 = self.data.justified_epoch; + let source_epoch_2 = other.data.justified_epoch; + let target_epoch_1 = self.data.slot.epoch(spec.epoch_length); + let target_epoch_2 = other.data.slot.epoch(spec.epoch_length); + + (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_vote_data(1, 1, &spec); + let slashable_vote_second = create_slashable_vote_data(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_vote_data(1, 1, &spec); + let slashable_vote_second = create_slashable_vote_data(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_vote_data(2, 1, &spec); + let slashable_vote_second = create_slashable_vote_data(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_vote_data(4, 1, &spec); + let slashable_vote_second = create_slashable_vote_data(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_vote_data(2, 2, &spec); + let slashable_vote_second = create_slashable_vote_data(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_vote_data(1, 1, &spec); + let slashable_vote_second = create_slashable_vote_data(2, 2, &spec); + + assert_eq!( + slashable_vote_first.is_surround_vote(&slashable_vote_second, &spec), + false + ); + } + + ssz_tests!(SlashableVoteData); + + fn create_slashable_vote_data( + slot_factor: u64, + justified_epoch: u64, + spec: &ChainSpec, + ) -> SlashableVoteData { + let mut rng = XorShiftRng::from_seed([42; 16]); + let mut slashable_vote = SlashableVoteData::random_for_test(&mut rng); + + slashable_vote.data.slot = Slot::new(slot_factor * spec.epoch_length); + slashable_vote.data.justified_epoch = Epoch::new(justified_epoch); + slashable_vote + } +} diff --git a/eth2/types/src/test_utils/address.rs b/eth2/types/src/test_utils/address.rs index 2d60b72da..13de2dec9 100644 --- a/eth2/types/src/test_utils/address.rs +++ b/eth2/types/src/test_utils/address.rs @@ -6,6 +6,6 @@ impl TestRandom for Address { fn random_for_test(rng: &mut T) -> Self { let mut key_bytes = vec![0; 20]; rng.fill_bytes(&mut key_bytes); - Address::from(&key_bytes[..]) + Address::from_slice(&key_bytes[..]) } } diff --git a/eth2/types/src/test_utils/hash256.rs b/eth2/types/src/test_utils/hash256.rs index 98f5e7899..a227679da 100644 --- a/eth2/types/src/test_utils/hash256.rs +++ b/eth2/types/src/test_utils/hash256.rs @@ -6,6 +6,6 @@ impl TestRandom for Hash256 { fn random_for_test(rng: &mut T) -> Self { let mut key_bytes = vec![0; 32]; rng.fill_bytes(&mut key_bytes); - Hash256::from(&key_bytes[..]) + Hash256::from_slice(&key_bytes[..]) } } diff --git a/eth2/types/src/test_utils/macros.rs b/eth2/types/src/test_utils/macros.rs new file mode 100644 index 000000000..b7c0a6522 --- /dev/null +++ b/eth2/types/src/test_utils/macros.rs @@ -0,0 +1,34 @@ +#[cfg(test)] +#[macro_export] +macro_rules! ssz_tests { + ($type: ident) => { + #[test] + pub fn test_ssz_round_trip() { + use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng}; + use ssz::{ssz_encode, Decodable}; + + let mut rng = XorShiftRng::from_seed([42; 16]); + let original = $type::random_for_test(&mut rng); + + let bytes = ssz_encode(&original); + let (decoded, _) = $type::ssz_decode(&bytes, 0).unwrap(); + + assert_eq!(original, decoded); + } + + #[test] + pub fn test_hash_tree_root_internal() { + use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng}; + use ssz::TreeHash; + + let mut rng = XorShiftRng::from_seed([42; 16]); + let original = $type::random_for_test(&mut rng); + + let result = original.hash_tree_root_internal(); + + assert_eq!(result.len(), 32); + // TODO: Add further tests + // https://github.com/sigp/lighthouse/issues/170 + } + }; +} diff --git a/eth2/types/src/validator_registry_delta_block.rs b/eth2/types/src/validator_registry_delta_block.rs new file mode 100644 index 000000000..e9a075052 --- /dev/null +++ b/eth2/types/src/validator_registry_delta_block.rs @@ -0,0 +1,36 @@ +use crate::{test_utils::TestRandom, Hash256, Slot}; +use bls::PublicKey; +use rand::RngCore; +use serde_derive::Serialize; +use ssz_derive::{Decode, Encode, TreeHash}; +use test_random_derive::TestRandom; + +// The information gathered from the PoW chain validator registration function. +#[derive(Debug, Clone, PartialEq, Serialize, Encode, Decode, TreeHash, TestRandom)] +pub struct ValidatorRegistryDeltaBlock { + pub latest_registry_delta_root: Hash256, + pub validator_index: u32, + pub pubkey: PublicKey, + pub slot: Slot, + pub flag: u64, +} + +impl Default for ValidatorRegistryDeltaBlock { + /// Yields a "default" `Validator`. Primarily used for testing. + fn default() -> Self { + Self { + latest_registry_delta_root: Hash256::zero(), + validator_index: std::u32::MAX, + pubkey: PublicKey::default(), + slot: Slot::from(std::u64::MAX), + flag: std::u64::MAX, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + ssz_tests!(ValidatorRegistryDeltaBlock); +} diff --git a/eth2/utils/merkle_proof/Cargo.toml b/eth2/utils/merkle_proof/Cargo.toml new file mode 100644 index 000000000..b7cd81216 --- /dev/null +++ b/eth2/utils/merkle_proof/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "merkle_proof" +version = "0.1.0" +authors = ["Michael Sproul "] +edition = "2018" + +[dependencies] +ethereum-types = "0.5" +hashing = { path = "../hashing" } diff --git a/eth2/utils/merkle_proof/src/lib.rs b/eth2/utils/merkle_proof/src/lib.rs new file mode 100644 index 000000000..4d0abcea3 --- /dev/null +++ b/eth2/utils/merkle_proof/src/lib.rs @@ -0,0 +1,148 @@ +use ethereum_types::H256; +use hashing::hash; + +/// Verify a proof that `leaf` exists at `index` in a Merkle tree rooted at `root`. +/// +/// The `branch` argument is the main component of the proof: it should be a list of internal +/// node hashes such that the root can be reconstructed (in bottom-up order). +pub fn verify_merkle_proof( + leaf: H256, + branch: &[H256], + depth: usize, + index: usize, + root: H256, +) -> bool { + if branch.len() == depth { + merkle_root_from_branch(leaf, branch, depth, index) == root + } else { + false + } +} + +/// Compute a root hash from a leaf and a Merkle proof. +fn merkle_root_from_branch(leaf: H256, branch: &[H256], depth: usize, index: usize) -> H256 { + assert_eq!(branch.len(), depth, "proof length should equal depth"); + + let mut merkle_root = leaf.as_bytes().to_vec(); + + for i in 0..depth { + let ith_bit = (index >> i) & 0x01; + if ith_bit == 1 { + let input = concat(branch[i].as_bytes().to_vec(), merkle_root); + merkle_root = hash(&input); + } else { + let mut input = merkle_root; + input.extend_from_slice(branch[i].as_bytes()); + merkle_root = hash(&input); + } + } + + H256::from_slice(&merkle_root) +} + +/// Concatenate two vectors. +fn concat(mut vec1: Vec, mut vec2: Vec) -> Vec { + vec1.append(&mut vec2); + vec1 +} + +#[cfg(test)] +mod tests { + use super::*; + + fn hash_concat(h1: H256, h2: H256) -> H256 { + H256::from_slice(&hash(&concat( + h1.as_bytes().to_vec(), + h2.as_bytes().to_vec(), + ))) + } + + #[test] + fn verify_small_example() { + // Construct a small merkle tree manually + let leaf_b00 = H256::from([0xAA; 32]); + let leaf_b01 = H256::from([0xBB; 32]); + let leaf_b10 = H256::from([0xCC; 32]); + let leaf_b11 = H256::from([0xDD; 32]); + + let node_b0x = hash_concat(leaf_b00, leaf_b01); + let node_b1x = hash_concat(leaf_b10, leaf_b11); + + let root = hash_concat(node_b0x, node_b1x); + + // Run some proofs + assert!(verify_merkle_proof( + leaf_b00, + &[leaf_b01, node_b1x], + 2, + 0b00, + root + )); + assert!(verify_merkle_proof( + leaf_b01, + &[leaf_b00, node_b1x], + 2, + 0b01, + root + )); + assert!(verify_merkle_proof( + leaf_b10, + &[leaf_b11, node_b0x], + 2, + 0b10, + root + )); + assert!(verify_merkle_proof( + leaf_b11, + &[leaf_b10, node_b0x], + 2, + 0b11, + root + )); + assert!(verify_merkle_proof( + leaf_b11, + &[leaf_b10], + 1, + 0b11, + node_b1x + )); + + // Ensure that incorrect proofs fail + // Zero-length proof + assert!(!verify_merkle_proof(leaf_b01, &[], 2, 0b01, root)); + // Proof in reverse order + assert!(!verify_merkle_proof( + leaf_b01, + &[node_b1x, leaf_b00], + 2, + 0b01, + root + )); + // Proof too short + assert!(!verify_merkle_proof(leaf_b01, &[leaf_b00], 2, 0b01, root)); + // Wrong index + assert!(!verify_merkle_proof( + leaf_b01, + &[leaf_b00, node_b1x], + 2, + 0b10, + root + )); + // Wrong root + assert!(!verify_merkle_proof( + leaf_b01, + &[leaf_b00, node_b1x], + 2, + 0b01, + node_b1x + )); + } + + #[test] + fn verify_zero_depth() { + let leaf = H256::from([0xD6; 32]); + let junk = H256::from([0xD7; 32]); + assert!(verify_merkle_proof(leaf, &[], 0, 0, leaf)); + assert!(!verify_merkle_proof(leaf, &[], 0, 7, junk)); + } +} diff --git a/eth2/utils/ssz/Cargo.toml b/eth2/utils/ssz/Cargo.toml index 25326cb5b..f13db5def 100644 --- a/eth2/utils/ssz/Cargo.toml +++ b/eth2/utils/ssz/Cargo.toml @@ -6,5 +6,5 @@ edition = "2018" [dependencies] bytes = "0.4.9" -ethereum-types = "0.4.0" +ethereum-types = "0.5" hashing = { path = "../hashing" } diff --git a/eth2/utils/ssz/src/impl_decode.rs b/eth2/utils/ssz/src/impl_decode.rs index b9ca48f9b..b13cbeb5d 100644 --- a/eth2/utils/ssz/src/impl_decode.rs +++ b/eth2/utils/ssz/src/impl_decode.rs @@ -59,7 +59,7 @@ impl Decodable for H256 { if bytes.len() < 32 || bytes.len() - 32 < index { Err(DecodeError::TooShort) } else { - Ok((H256::from(&bytes[index..(index + 32)]), index + 32)) + Ok((H256::from_slice(&bytes[index..(index + 32)]), index + 32)) } } } @@ -69,7 +69,7 @@ impl Decodable for Address { if bytes.len() < 20 || bytes.len() - 20 < index { Err(DecodeError::TooShort) } else { - Ok((Address::from(&bytes[index..(index + 20)]), index + 20)) + Ok((Address::from_slice(&bytes[index..(index + 20)]), index + 20)) } } } @@ -95,7 +95,7 @@ mod tests { */ let input = vec![42_u8; 32]; let (decoded, i) = H256::ssz_decode(&input, 0).unwrap(); - assert_eq!(decoded.to_vec(), input); + assert_eq!(decoded.as_bytes(), &input[..]); assert_eq!(i, 32); /* @@ -104,7 +104,7 @@ mod tests { let mut input = vec![42_u8; 32]; input.push(12); let (decoded, i) = H256::ssz_decode(&input, 0).unwrap(); - assert_eq!(decoded.to_vec()[..], input[0..32]); + assert_eq!(decoded.as_bytes(), &input[0..32]); assert_eq!(i, 32); /* diff --git a/eth2/utils/ssz/src/impl_encode.rs b/eth2/utils/ssz/src/impl_encode.rs index 5f73b8483..bb1ec42d5 100644 --- a/eth2/utils/ssz/src/impl_encode.rs +++ b/eth2/utils/ssz/src/impl_encode.rs @@ -55,13 +55,13 @@ impl Encodable for bool { impl Encodable for H256 { fn ssz_append(&self, s: &mut SszStream) { - s.append_encoded_raw(&self.to_vec()); + s.append_encoded_raw(self.as_bytes()); } } impl Encodable for Address { fn ssz_append(&self, s: &mut SszStream) { - s.append_encoded_raw(&self.to_vec()); + s.append_encoded_raw(self.as_bytes()); } } diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 104a4bbe6..68405ed2f 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -1,11 +1,13 @@ use std::fs; use std::path::PathBuf; +use types::ChainSpec; /// Stores the core configuration for this validator instance. #[derive(Clone)] pub struct ClientConfig { pub data_dir: PathBuf, pub server: String, + pub spec: ChainSpec, } const DEFAULT_LIGHTHOUSE_DIR: &str = ".lighthouse-validators"; @@ -20,6 +22,11 @@ impl ClientConfig { fs::create_dir_all(&data_dir) .unwrap_or_else(|_| panic!("Unable to create {:?}", &data_dir)); let server = "localhost:50051".to_string(); - Self { data_dir, server } + let spec = ChainSpec::foundation(); + Self { + data_dir, + server, + spec, + } } } diff --git a/validator_client/src/main.rs b/validator_client/src/main.rs index b4d8ae5db..eea213a45 100644 --- a/validator_client/src/main.rs +++ b/validator_client/src/main.rs @@ -43,6 +43,16 @@ fn main() { .help("Address to connect to BeaconNode.") .takes_value(true), ) + .arg( + Arg::with_name("spec") + .long("spec") + .value_name("spec") + .short("s") + .help("Configuration of Beacon Chain") + .takes_value(true) + .possible_values(&["foundation", "few_validators"]) + .default_value("foundation"), + ) .get_matches(); let mut config = ClientConfig::default(); @@ -62,6 +72,17 @@ fn main() { } } + // TODO: Permit loading a custom spec from file. + // Custom spec + if let Some(spec_str) = matches.value_of("spec") { + match spec_str { + "foundation" => config.spec = ChainSpec::foundation(), + "few_validators" => config.spec = ChainSpec::few_validators(), + // Should be impossible due to clap's `possible_values(..)` function. + _ => unreachable!(), + }; + } + // Log configuration info!(log, ""; "data_dir" => &config.data_dir.to_str(), @@ -81,11 +102,8 @@ fn main() { Arc::new(ValidatorServiceClient::new(ch)) }; - // Ethereum - // - // TODO: Permit loading a custom spec from file. - // https://github.com/sigp/lighthouse/issues/160 - let spec = Arc::new(ChainSpec::foundation()); + // Spec + let spec = Arc::new(config.spec.clone()); // Clock for determining the present slot. // TODO: this shouldn't be a static time, instead it should be pulled from the beacon node.