diff --git a/Cargo.toml b/Cargo.toml index da5d1b9050..55bd4de532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,5 +42,6 @@ members = [ "beacon_chain/utils/ssz", "beacon_chain/utils/ssz_helpers", "beacon_chain/validation", + "beacon_chain/validator_induction", "lighthouse/db", ] diff --git a/beacon_chain/types/src/lib.rs b/beacon_chain/types/src/lib.rs index a00e904e94..887a2b1cee 100644 --- a/beacon_chain/types/src/lib.rs +++ b/beacon_chain/types/src/lib.rs @@ -12,6 +12,7 @@ pub mod crosslink_record; pub mod shard_and_committee; pub mod special_record; pub mod validator_record; +pub mod validator_registration; use self::ethereum_types::{ H256, @@ -30,6 +31,7 @@ pub use crosslink_record::CrosslinkRecord; pub use shard_and_committee::ShardAndCommittee; pub use special_record::{ SpecialRecord, SpecialRecordKind }; pub use validator_record::{ ValidatorRecord, ValidatorStatus }; +pub use validator_registration::{ ValidatorRegistration }; pub type Hash256 = H256; pub type Address = H160; diff --git a/beacon_chain/types/src/validator_registration.rs b/beacon_chain/types/src/validator_registration.rs new file mode 100644 index 0000000000..384f2dc729 --- /dev/null +++ b/beacon_chain/types/src/validator_registration.rs @@ -0,0 +1,20 @@ +use bls::{ + Keypair, + PublicKey, + Signature, +}; +use super::{ + Address, + Hash256, +}; + + +/// The information gathered from the PoW chain validator registration function. +#[derive(Debug, Clone, PartialEq)] +pub struct ValidatorRegistration { + pub pubkey: PublicKey, + pub withdrawal_shard: u16, + pub withdrawal_address: Address, + pub randao_commitment: Hash256, + pub proof_of_possession: Signature, +} diff --git a/beacon_chain/utils/bls/Cargo.toml b/beacon_chain/utils/bls/Cargo.toml index eef0ec0aac..1199efc154 100644 --- a/beacon_chain/utils/bls/Cargo.toml +++ b/beacon_chain/utils/bls/Cargo.toml @@ -5,3 +5,4 @@ authors = ["Paul Hauner "] [dependencies] bls-aggregates = { git = "https://github.com/sigp/signature-schemes" } +hashing = { path = "../hashing" } diff --git a/beacon_chain/utils/bls/src/lib.rs b/beacon_chain/utils/bls/src/lib.rs index ff9a0919aa..0578eb0959 100644 --- a/beacon_chain/utils/bls/src/lib.rs +++ b/beacon_chain/utils/bls/src/lib.rs @@ -1,4 +1,5 @@ extern crate bls_aggregates; +extern crate hashing; pub use self::bls_aggregates::AggregateSignature; pub use self::bls_aggregates::AggregatePublicKey; @@ -8,3 +9,21 @@ pub use self::bls_aggregates::PublicKey; pub use self::bls_aggregates::SecretKey; pub const BLS_AGG_SIG_BYTE_SIZE: usize = 97; + +use hashing::proof_of_possession_hash; + +/// For some signature and public key, ensure that the signature message was the public key and it +/// was signed by the secret key that corresponds to that public key. +pub fn verify_proof_of_possession(sig: &Signature, pubkey: &PublicKey) + -> bool +{ + let hash = proof_of_possession_hash(&pubkey.as_bytes()); + sig.verify_hashed(&hash, &pubkey) +} + +pub fn create_proof_of_possession(keypair: &Keypair) + -> Signature +{ + let hash = proof_of_possession_hash(&keypair.pk.as_bytes()); + Signature::new_hashed(&hash, &keypair.sk) +} diff --git a/beacon_chain/utils/hashing/src/lib.rs b/beacon_chain/utils/hashing/src/lib.rs index 8e2bd37a9e..7c349e39d0 100644 --- a/beacon_chain/utils/hashing/src/lib.rs +++ b/beacon_chain/utils/hashing/src/lib.rs @@ -6,3 +6,12 @@ pub fn canonical_hash(input: &[u8]) -> Vec { let result = blake2b(64, &[], input); result.as_bytes()[0..32].to_vec() } + +pub fn proof_of_possession_hash(input: &[u8]) -> Vec { + let result = blake2b(64, &[], input); + let mut hash = result.as_bytes()[32..64].to_vec(); + // TODO: this padding is not part of the spec, it is required otherwise Milagro will panic. + // We should either drop the padding or ensure the padding is in the spec. + hash.append(&mut vec![0; 18]); + hash +} diff --git a/beacon_chain/validator_induction/Cargo.toml b/beacon_chain/validator_induction/Cargo.toml new file mode 100644 index 0000000000..9a443133e9 --- /dev/null +++ b/beacon_chain/validator_induction/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "validator_induction" +version = "0.1.0" +authors = ["Paul Hauner "] + +[dependencies] +bls = { path = "../utils/bls" } +hashing = { path = "../utils/hashing" } +types = { path = "../types" } diff --git a/beacon_chain/validator_induction/src/inductor.rs b/beacon_chain/validator_induction/src/inductor.rs new file mode 100644 index 0000000000..e025d0d437 --- /dev/null +++ b/beacon_chain/validator_induction/src/inductor.rs @@ -0,0 +1,281 @@ +use bls::{ + verify_proof_of_possession, +}; +use types::{ + ValidatorRecord, + ValidatorStatus, + ValidatorRegistration, +}; + +/// The size of a validators deposit in GWei. +pub const DEPOSIT_GWEI: u64 = 32_000_000_000; + +/// Inducts validators into a `CrystallizedState`. +pub struct ValidatorInductor { + pub current_slot: u64, + pub shard_count: u16, + validators: Vec, + empty_validator_start: usize, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ValidatorInductionError { + InvalidShard, + InvaidProofOfPossession, +} + +impl ValidatorInductor { + pub fn new(current_slot: u64, shard_count: u16, validators: Vec) + -> Self + { + Self { + current_slot, + shard_count, + validators, + empty_validator_start: 0, + } + } + + /// Attempt to induct a validator into the CrystallizedState. + /// + /// Returns an error if the registration is invalid, otherwise returns the index of the + /// validator in `CrystallizedState.validators`. + pub fn induct(&mut self, rego: &ValidatorRegistration) + -> Result + { + let v = self.process_registration(rego)?; + Ok(self.add_validator(v)) + } + + /// Verify a `ValidatorRegistration` and return a `ValidatorRecord` if valid. + fn process_registration(&self, r: &ValidatorRegistration) + -> Result + { + /* + * Ensure withdrawal shard is not too high. + */ + if r.withdrawal_shard > self.shard_count { + return Err(ValidatorInductionError::InvalidShard) + } + + /* + * Prove validator has knowledge of their secret key. + */ + if !verify_proof_of_possession(&r.proof_of_possession, &r.pubkey) { + return Err(ValidatorInductionError::InvaidProofOfPossession) + } + + Ok(ValidatorRecord { + pubkey: r.pubkey.clone(), + withdrawal_shard: r.withdrawal_shard, + withdrawal_address: r.withdrawal_address, + randao_commitment: r.randao_commitment, + randao_last_change: self.current_slot, + balance: DEPOSIT_GWEI, + status: ValidatorStatus::PendingActivation as u8, + exit_slot: 0, + }) + } + + /// Returns the index of the first `ValidatorRecord` in the `CrystallizedState` where + /// `validator.status == Withdrawn`. If no such record exists, `None` is returned. + fn first_withdrawn_validator(&mut self) + -> Option + { + for i in self.empty_validator_start..self.validators.len() { + if self.validators[i].status == ValidatorStatus::Withdrawn as u8 { + self.empty_validator_start = i + 1; + return Some(i) + } + } + None + } + + /// Adds a `ValidatorRecord` to the `CrystallizedState` by replacing first validator where + /// `validator.status == Withdraw`. If no such withdrawn validator exists, adds the new + /// validator to the end of the list. + fn add_validator(&mut self, v: ValidatorRecord) + -> usize + { + match self.first_withdrawn_validator() { + Some(i) => { + self.validators[i] = v; + i + } + None => { + self.validators.push(v); + self.validators.len() - 1 + } + } + } + + pub fn to_vec(self) + -> Vec + { + self.validators + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + use bls::{ + Keypair, + Signature, + }; + use types::{ + Address, + Hash256, + }; + use hashing::proof_of_possession_hash; + + fn registration_equals_record(reg: &ValidatorRegistration, rec: &ValidatorRecord) + -> bool + { + (reg.pubkey == rec.pubkey) & + (reg.withdrawal_shard == rec.withdrawal_shard) & + (reg.withdrawal_address == rec.withdrawal_address) & + (reg.randao_commitment == rec.randao_commitment) & + (verify_proof_of_possession(®.proof_of_possession, &rec.pubkey)) + } + + /// Generate a proof of possession for some keypair. + fn get_proof_of_possession(kp: &Keypair) -> Signature { + let pop_message = proof_of_possession_hash(&kp.pk.as_bytes()); + Signature::new_hashed(&pop_message, &kp.sk) + } + + /// Generate a basic working ValidatorRegistration for use in tests. + fn get_registration() -> ValidatorRegistration { + let kp = Keypair::random(); + ValidatorRegistration { + pubkey: kp.pk.clone(), + withdrawal_shard: 0, + withdrawal_address: Address::zero(), + randao_commitment: Hash256::zero(), + proof_of_possession: get_proof_of_possession(&kp), + } + } + + #[test] + fn test_validator_inductor_valid_empty_validators() { + let validators = vec![]; + + let r = get_registration(); + + let mut inductor = ValidatorInductor::new(0, 1024, validators); + let result = inductor.induct(&r); + let validators = inductor.to_vec(); + + assert_eq!(result.unwrap(), 0); + assert!(registration_equals_record(&r, &validators[0])); + assert_eq!(validators.len(), 1); + } + + #[test] + fn test_validator_inductor_valid_all_active_validators() { + let mut validators = vec![]; + for _ in 0..5 { + let (mut v, _) = ValidatorRecord::zero_with_thread_rand_keypair(); + v.status = ValidatorStatus::Active as u8; + validators.push(v); + } + + let r = get_registration(); + + let mut inductor = ValidatorInductor::new(0, 1024, validators); + let result = inductor.induct(&r); + let validators = inductor.to_vec(); + + assert_eq!(result.unwrap(), 5); + assert!(registration_equals_record(&r, &validators[5])); + assert_eq!(validators.len(), 6); + } + + #[test] + fn test_validator_inductor_valid_all_second_validator_withdrawn() { + let mut validators = vec![]; + let (mut v, _) = ValidatorRecord::zero_with_thread_rand_keypair(); + v.status = ValidatorStatus::Active as u8; + validators.push(v); + for _ in 0..4 { + let (mut v, _) = ValidatorRecord::zero_with_thread_rand_keypair(); + v.status = ValidatorStatus::Withdrawn as u8; + validators.push(v); + } + + let r = get_registration(); + + let mut inductor = ValidatorInductor::new(0, 1024, validators); + let result = inductor.induct(&r); + let validators = inductor.to_vec(); + + assert_eq!(result.unwrap(), 1); + assert!(registration_equals_record(&r, &validators[1])); + assert_eq!(validators.len(), 5); + } + + #[test] + fn test_validator_inductor_valid_all_withdrawn_validators() { + let mut validators = vec![]; + for _ in 0..5 { + let (mut v, _) = ValidatorRecord::zero_with_thread_rand_keypair(); + v.status = ValidatorStatus::Withdrawn as u8; + validators.push(v); + } + + /* + * Ensure the first validator gets the 0'th slot + */ + let r = get_registration(); + let mut inductor = ValidatorInductor::new(0, 1024, validators); + let result = inductor.induct(&r); + let validators = inductor.to_vec(); + assert_eq!(result.unwrap(), 0); + assert!(registration_equals_record(&r, &validators[0])); + + /* + * Ensure the second validator gets the 1'st slot + */ + let r_two = get_registration(); + let mut inductor = ValidatorInductor::new(0, 1024, validators); + let result = inductor.induct(&r_two); + let validators = inductor.to_vec(); + assert_eq!(result.unwrap(), 1); + assert!(registration_equals_record(&r_two, &validators[1])); + assert_eq!(validators.len(), 5); + } + + #[test] + fn test_validator_inductor_shard_too_high() { + let validators = vec![]; + + let mut r = get_registration(); + r.withdrawal_shard = 1025; + + let mut inductor = ValidatorInductor::new(0, 1024, validators); + let result = inductor.induct(&r); + let validators = inductor.to_vec(); + + assert_eq!(result, Err(ValidatorInductionError::InvalidShard)); + assert_eq!(validators.len(), 0); + } + + #[test] + fn test_validator_inductor_shard_proof_of_possession_failure() { + let validators = vec![]; + + let mut r = get_registration(); + let kp = Keypair::random(); + r.proof_of_possession = get_proof_of_possession(&kp); + + let mut inductor = ValidatorInductor::new(0, 1024, validators); + let result = inductor.induct(&r); + let validators = inductor.to_vec(); + + assert_eq!(result, Err(ValidatorInductionError::InvaidProofOfPossession)); + assert_eq!(validators.len(), 0); + } +} diff --git a/beacon_chain/validator_induction/src/lib.rs b/beacon_chain/validator_induction/src/lib.rs new file mode 100644 index 0000000000..e1f5ef09d1 --- /dev/null +++ b/beacon_chain/validator_induction/src/lib.rs @@ -0,0 +1,10 @@ +extern crate bls; +extern crate hashing; +extern crate types; + +mod inductor; + +pub use inductor::{ + ValidatorInductor, + ValidatorInductionError, +};