Merge pull request #55 from sigp/validator_induction
[BLOCKED] Validator induction
This commit is contained in:
commit
91876fb9ef
@ -42,5 +42,6 @@ members = [
|
|||||||
"beacon_chain/utils/ssz",
|
"beacon_chain/utils/ssz",
|
||||||
"beacon_chain/utils/ssz_helpers",
|
"beacon_chain/utils/ssz_helpers",
|
||||||
"beacon_chain/validation",
|
"beacon_chain/validation",
|
||||||
|
"beacon_chain/validator_induction",
|
||||||
"lighthouse/db",
|
"lighthouse/db",
|
||||||
]
|
]
|
||||||
|
@ -12,6 +12,7 @@ pub mod crosslink_record;
|
|||||||
pub mod shard_and_committee;
|
pub mod shard_and_committee;
|
||||||
pub mod special_record;
|
pub mod special_record;
|
||||||
pub mod validator_record;
|
pub mod validator_record;
|
||||||
|
pub mod validator_registration;
|
||||||
|
|
||||||
use self::ethereum_types::{
|
use self::ethereum_types::{
|
||||||
H256,
|
H256,
|
||||||
@ -30,6 +31,7 @@ pub use crosslink_record::CrosslinkRecord;
|
|||||||
pub use shard_and_committee::ShardAndCommittee;
|
pub use shard_and_committee::ShardAndCommittee;
|
||||||
pub use special_record::{ SpecialRecord, SpecialRecordKind };
|
pub use special_record::{ SpecialRecord, SpecialRecordKind };
|
||||||
pub use validator_record::{ ValidatorRecord, ValidatorStatus };
|
pub use validator_record::{ ValidatorRecord, ValidatorStatus };
|
||||||
|
pub use validator_registration::{ ValidatorRegistration };
|
||||||
|
|
||||||
pub type Hash256 = H256;
|
pub type Hash256 = H256;
|
||||||
pub type Address = H160;
|
pub type Address = H160;
|
||||||
|
20
beacon_chain/types/src/validator_registration.rs
Normal file
20
beacon_chain/types/src/validator_registration.rs
Normal file
@ -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,
|
||||||
|
}
|
@ -5,3 +5,4 @@ authors = ["Paul Hauner <paul@paulhauner.com>"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bls-aggregates = { git = "https://github.com/sigp/signature-schemes" }
|
bls-aggregates = { git = "https://github.com/sigp/signature-schemes" }
|
||||||
|
hashing = { path = "../hashing" }
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
extern crate bls_aggregates;
|
extern crate bls_aggregates;
|
||||||
|
extern crate hashing;
|
||||||
|
|
||||||
pub use self::bls_aggregates::AggregateSignature;
|
pub use self::bls_aggregates::AggregateSignature;
|
||||||
pub use self::bls_aggregates::AggregatePublicKey;
|
pub use self::bls_aggregates::AggregatePublicKey;
|
||||||
@ -8,3 +9,21 @@ pub use self::bls_aggregates::PublicKey;
|
|||||||
pub use self::bls_aggregates::SecretKey;
|
pub use self::bls_aggregates::SecretKey;
|
||||||
|
|
||||||
pub const BLS_AGG_SIG_BYTE_SIZE: usize = 97;
|
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)
|
||||||
|
}
|
||||||
|
@ -6,3 +6,12 @@ pub fn canonical_hash(input: &[u8]) -> Vec<u8> {
|
|||||||
let result = blake2b(64, &[], input);
|
let result = blake2b(64, &[], input);
|
||||||
result.as_bytes()[0..32].to_vec()
|
result.as_bytes()[0..32].to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn proof_of_possession_hash(input: &[u8]) -> Vec<u8> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
9
beacon_chain/validator_induction/Cargo.toml
Normal file
9
beacon_chain/validator_induction/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "validator_induction"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bls = { path = "../utils/bls" }
|
||||||
|
hashing = { path = "../utils/hashing" }
|
||||||
|
types = { path = "../types" }
|
281
beacon_chain/validator_induction/src/inductor.rs
Normal file
281
beacon_chain/validator_induction/src/inductor.rs
Normal file
@ -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<ValidatorRecord>,
|
||||||
|
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<ValidatorRecord>)
|
||||||
|
-> 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<usize, ValidatorInductionError>
|
||||||
|
{
|
||||||
|
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<ValidatorRecord, ValidatorInductionError>
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* 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<usize>
|
||||||
|
{
|
||||||
|
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<ValidatorRecord>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
10
beacon_chain/validator_induction/src/lib.rs
Normal file
10
beacon_chain/validator_induction/src/lib.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
extern crate bls;
|
||||||
|
extern crate hashing;
|
||||||
|
extern crate types;
|
||||||
|
|
||||||
|
mod inductor;
|
||||||
|
|
||||||
|
pub use inductor::{
|
||||||
|
ValidatorInductor,
|
||||||
|
ValidatorInductionError,
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user