From b4566a776aefa66889c971da809960994856143f Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 20 Oct 2018 03:11:45 +1100 Subject: [PATCH] Add validator induction functionality --- Cargo.toml | 1 + beacon_chain/validator_induction/Cargo.toml | 9 + .../validator_induction/src/inductor.rs | 256 ++++++++++++++++++ beacon_chain/validator_induction/src/lib.rs | 12 + .../src/proof_of_possession.rs | 14 + .../validator_induction/src/registration.rs | 105 +++++++ 6 files changed, 397 insertions(+) create mode 100644 beacon_chain/validator_induction/Cargo.toml create mode 100644 beacon_chain/validator_induction/src/inductor.rs create mode 100644 beacon_chain/validator_induction/src/lib.rs create mode 100644 beacon_chain/validator_induction/src/proof_of_possession.rs create mode 100644 beacon_chain/validator_induction/src/registration.rs 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/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..15d2c44494 --- /dev/null +++ b/beacon_chain/validator_induction/src/inductor.rs @@ -0,0 +1,256 @@ +use types::{ + ValidatorRecord, + ValidatorStatus, +}; + +use super::proof_of_possession::verify_proof_of_possession; +use super::registration::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<'a> { + pub current_slot: u64, + pub shard_count: u16, + validators: &'a mut Vec, + empty_validator_start: usize, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ValidatorInductionError { + InvalidShard, + InvaidProofOfPossession, +} + +impl<'a> ValidatorInductor<'a> { + /// 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 + } + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + use bls::{ + Keypair, + Signature, + }; + use types::{ + Address, + Hash256, + }; + use hashing::proof_of_possession_hash; + + /// 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), + } + } + + /// Induct a validator using the ValidatorInductor, return the result. + fn do_induction(validator_rego: &ValidatorRegistration, + validators: &mut Vec, + current_slot: u64, + shard_count: u16) + -> Result + { + let mut inductor = ValidatorInductor { + current_slot, + shard_count, + empty_validator_start: 0, + validators, + }; + inductor.induct(&validator_rego) + } + + #[test] + fn test_validator_inductor_valid_empty_validators() { + let mut validators = vec![]; + + let r = get_registration(); + + let result = do_induction(&r, &mut validators, 0, 1024); + + assert_eq!(result.unwrap(), 0); + assert_eq!(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 result = do_induction(&r, &mut validators, 0, 1024); + + assert_eq!(result.unwrap(), 5); + assert_eq!(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 result = do_induction(&r, &mut validators, 0, 1024); + + assert_eq!(result.unwrap(), 1); + assert_eq!(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 result = do_induction(&r, &mut validators, 0, 1024); + assert_eq!(result.unwrap(), 0); + assert_eq!(r, validators[0]); + assert_eq!(validators.len(), 5); + + /* + * Ensure the second validator gets the 1'st slot + */ + let r_two = get_registration(); + let result = do_induction(&r_two, &mut validators, 0, 1024); + assert_eq!(result.unwrap(), 1); + assert_eq!(r_two, validators[1]); + assert_eq!(validators.len(), 5); + } + + #[test] + fn test_validator_inductor_shard_too_high() { + let mut validators = vec![]; + + let mut r = get_registration(); + r.withdrawal_shard = 1025; + + let result = do_induction(&r, &mut validators, 0, 1024); + + assert_eq!(result, Err(ValidatorInductionError::InvalidShard)); + assert_eq!(validators.len(), 0); + } + + #[test] + fn test_validator_inductor_shard_proof_of_possession_failure() { + let mut validators = vec![]; + + let mut r = get_registration(); + let kp = Keypair::random(); + r.proof_of_possession = get_proof_of_possession(&kp); + + let result = do_induction(&r, &mut validators, 0, 1024); + + 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..6c5bf11a96 --- /dev/null +++ b/beacon_chain/validator_induction/src/lib.rs @@ -0,0 +1,12 @@ +extern crate bls; +extern crate hashing; +extern crate types; + +mod inductor; +mod proof_of_possession; +mod registration; + +pub use inductor::{ + ValidatorInductor, + ValidatorInductionError, +}; diff --git a/beacon_chain/validator_induction/src/proof_of_possession.rs b/beacon_chain/validator_induction/src/proof_of_possession.rs new file mode 100644 index 0000000000..85719c0147 --- /dev/null +++ b/beacon_chain/validator_induction/src/proof_of_possession.rs @@ -0,0 +1,14 @@ +use bls::{ + Signature, + PublicKey, +}; +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) +} diff --git a/beacon_chain/validator_induction/src/registration.rs b/beacon_chain/validator_induction/src/registration.rs new file mode 100644 index 0000000000..0cb62c6bd9 --- /dev/null +++ b/beacon_chain/validator_induction/src/registration.rs @@ -0,0 +1,105 @@ +use bls::{ + PublicKey, + Signature, +}; +use types::{ + Address, + Hash256, + ValidatorRecord, +}; + +use super::proof_of_possession::verify_proof_of_possession; + + +/// 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, +} + +impl PartialEq for ValidatorRegistration { + fn eq(&self, v: &ValidatorRecord) -> bool { + (self.pubkey == v.pubkey) & + (self.withdrawal_shard == v.withdrawal_shard) & + (self.withdrawal_address == v.withdrawal_address) & + (self.randao_commitment == v.randao_commitment) & + (verify_proof_of_possession(&self.proof_of_possession, &v.pubkey)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use bls::{ + Keypair, + Signature, + }; + use types::{ + Address, + Hash256, + ValidatorRecord, + }; + use hashing::proof_of_possession_hash; + + 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) + } + + fn get_equal_validator_registrations_and_records() + -> (ValidatorRegistration, ValidatorRecord) + { + let kp = Keypair::random(); + let rego = ValidatorRegistration { + pubkey: kp.pk.clone(), + withdrawal_shard: 0, + withdrawal_address: Address::zero(), + randao_commitment: Hash256::zero(), + proof_of_possession: get_proof_of_possession(&kp), + }; + let record = ValidatorRecord { + pubkey: rego.pubkey.clone(), + withdrawal_shard: rego.withdrawal_shard, + withdrawal_address: rego.withdrawal_address.clone(), + randao_commitment: rego.randao_commitment.clone(), + randao_last_change: 0, + balance: 0, + status: 0, + exit_slot: 0, + }; + (rego, record) + } + + #[test] + fn test_validator_registration_and_record_partial_eq() { + let (rego, record) = get_equal_validator_registrations_and_records(); + assert!(rego == record); + + let (mut rego, record) = get_equal_validator_registrations_and_records(); + let kp = Keypair::random(); + rego.pubkey = kp.pk.clone(); + assert!(rego != record); + + let (mut rego, record) = get_equal_validator_registrations_and_records(); + rego.withdrawal_shard = record.withdrawal_shard + 1; + assert!(rego != record); + + let (mut rego, record) = get_equal_validator_registrations_and_records(); + rego.withdrawal_address = Address::from(42); + assert!(rego != record); + + let (mut rego, record) = get_equal_validator_registrations_and_records(); + rego.randao_commitment = Hash256::from(42); + assert!(rego != record); + + let (mut rego, record) = get_equal_validator_registrations_and_records(); + let kp = Keypair::random(); + rego.proof_of_possession = get_proof_of_possession(&kp); + assert!(rego != record); + } +}