From 403eefb7b494a8914a073978723e3ea7c24b18d5 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 23 Jan 2019 21:21:18 +1100 Subject: [PATCH] Move block_proposer into separate crate --- Cargo.toml | 1 + validator_client/block_proposer/Cargo.toml | 10 + validator_client/block_proposer/src/lib.rs | 263 ++++++++++++++++++ .../block_proposer/src/test_node.rs | 47 ++++ validator_client/block_proposer/src/traits.rs | 28 ++ 5 files changed, 349 insertions(+) create mode 100644 validator_client/block_proposer/Cargo.toml create mode 100644 validator_client/block_proposer/src/lib.rs create mode 100644 validator_client/block_proposer/src/test_node.rs create mode 100644 validator_client/block_proposer/src/traits.rs diff --git a/Cargo.toml b/Cargo.toml index bcaaef87e9..fdedbf6bb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,5 @@ members = [ "beacon_node/beacon_chain", "protos", "validator_client", + "validator_client/block_proposer", ] diff --git a/validator_client/block_proposer/Cargo.toml b/validator_client/block_proposer/Cargo.toml new file mode 100644 index 0000000000..460ca863ea --- /dev/null +++ b/validator_client/block_proposer/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "block_proposer" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2018" + +[dependencies] +slot_clock = { path = "../../eth2/utils/slot_clock" } +spec = { path = "../../eth2/spec" } +types = { path = "../../eth2/types" } diff --git a/validator_client/block_proposer/src/lib.rs b/validator_client/block_proposer/src/lib.rs new file mode 100644 index 0000000000..112684dbb6 --- /dev/null +++ b/validator_client/block_proposer/src/lib.rs @@ -0,0 +1,263 @@ +#[cfg(test)] +mod test_node; +mod traits; + +use slot_clock::SlotClock; +use spec::ChainSpec; +use std::sync::{Arc, RwLock}; +use types::BeaconBlock; + +pub use self::traits::{BeaconNode, BeaconNodeError, DutiesReader, DutiesReaderError}; + +#[derive(Debug, PartialEq)] +pub enum PollOutcome { + /// A new block was produced. + BlockProduced(u64), + /// A block was not produced as it would have been slashable. + SlashableBlockNotProduced(u64), + /// The validator duties did not require a block to be produced. + BlockProductionNotRequired(u64), + /// The duties for the present epoch were not found. + ProducerDutiesUnknown(u64), + /// The slot has already been processed, execution was skipped. + SlotAlreadyProcessed(u64), + /// The Beacon Node was unable to produce a block at that slot. + BeaconNodeUnableToProduceBlock(u64), +} + +#[derive(Debug, PartialEq)] +pub enum Error { + SlotClockError, + SlotUnknowable, + EpochMapPoisoned, + SlotClockPoisoned, + EpochLengthIsZero, + BeaconNodeError(BeaconNodeError), +} + +/// A polling state machine which performs block production duties, based upon some epoch duties +/// (`EpochDutiesMap`) and a concept of time (`SlotClock`). +/// +/// Ensures that messages are not slashable. +/// +/// Relies upon an external service to keep the `EpochDutiesMap` updated. +pub struct BlockProducer { + pub last_processed_slot: u64, + spec: Arc, + epoch_map: Arc, + slot_clock: Arc>, + beacon_node: Arc, +} + +impl BlockProducer { + /// Returns a new instance where `last_processed_slot == 0`. + pub fn new( + spec: Arc, + epoch_map: Arc, + slot_clock: Arc>, + beacon_node: Arc, + ) -> Self { + Self { + last_processed_slot: 0, + spec, + epoch_map, + slot_clock, + beacon_node, + } + } +} + +impl BlockProducer { + /// "Poll" to see if the validator is required to take any action. + /// + /// The slot clock will be read and any new actions undertaken. + pub fn poll(&mut self) -> Result { + let slot = self + .slot_clock + .read() + .map_err(|_| Error::SlotClockPoisoned)? + .present_slot() + .map_err(|_| Error::SlotClockError)? + .ok_or(Error::SlotUnknowable)?; + + let epoch = slot + .checked_div(self.spec.epoch_length) + .ok_or(Error::EpochLengthIsZero)?; + + // If this is a new slot. + if slot > self.last_processed_slot { + let is_block_production_slot = + match self.epoch_map.is_block_production_slot(epoch, slot) { + Ok(result) => result, + Err(DutiesReaderError::UnknownEpoch) => { + return Ok(PollOutcome::ProducerDutiesUnknown(slot)) + } + Err(DutiesReaderError::Poisoned) => return Err(Error::EpochMapPoisoned), + }; + + if is_block_production_slot { + self.last_processed_slot = slot; + + self.produce_block(slot) + } else { + Ok(PollOutcome::BlockProductionNotRequired(slot)) + } + } else { + Ok(PollOutcome::SlotAlreadyProcessed(slot)) + } + } + + /// Produce a block at some slot. + /// + /// Assumes that a block is required at this slot (does not check the duties). + /// + /// Ensures the message is not slashable. + /// + /// !!! UNSAFE !!! + /// + /// The slash-protection code is not yet implemented. There is zero protection against + /// slashing. + fn produce_block(&mut self, slot: u64) -> Result { + if let Some(block) = self.beacon_node.produce_beacon_block(slot)? { + if self.safe_to_produce(&block) { + let block = self.sign_block(block); + self.beacon_node.publish_beacon_block(block)?; + Ok(PollOutcome::BlockProduced(slot)) + } else { + Ok(PollOutcome::SlashableBlockNotProduced(slot)) + } + } else { + Ok(PollOutcome::BeaconNodeUnableToProduceBlock(slot)) + } + } + + /// Consumes a block, returning that block signed by the validators private key. + /// + /// Important: this function will not check to ensure the block is not slashable. This must be + /// done upstream. + fn sign_block(&mut self, block: BeaconBlock) -> BeaconBlock { + // TODO: sign the block + // https://github.com/sigp/lighthouse/issues/160 + self.store_produce(&block); + block + } + + /// Returns `true` if signing a block is safe (non-slashable). + /// + /// !!! UNSAFE !!! + /// + /// Important: this function is presently stubbed-out. It provides ZERO SAFETY. + fn safe_to_produce(&self, _block: &BeaconBlock) -> bool { + // TODO: ensure the producer doesn't produce slashable blocks. + // https://github.com/sigp/lighthouse/issues/160 + true + } + + /// Record that a block was produced so that slashable votes may not be made in the future. + /// + /// !!! UNSAFE !!! + /// + /// Important: this function is presently stubbed-out. It provides ZERO SAFETY. + fn store_produce(&mut self, _block: &BeaconBlock) { + // TODO: record this block production to prevent future slashings. + // https://github.com/sigp/lighthouse/issues/160 + } +} + +impl From for Error { + fn from(e: BeaconNodeError) -> Error { + Error::BeaconNodeError(e) + } +} + +#[cfg(test)] +mod tests { + use super::test_node::TestBeaconNode; + use super::*; + use slot_clock::TestingSlotClock; + use std::collections::HashMap; + use types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; + + // TODO: implement more thorough testing. + // https://github.com/sigp/lighthouse/issues/160 + // + // These tests should serve as a good example for future tests. + + type EpochMap = HashMap; + + impl DutiesReader for EpochMap { + fn is_block_production_slot( + &self, + epoch: u64, + slot: u64, + ) -> Result { + match self.get(&epoch) { + Some(s) if *s == slot => Ok(true), + Some(s) if *s != slot => Ok(false), + _ => Err(DutiesReaderError::UnknownEpoch), + } + } + } + + #[test] + pub fn polling() { + let mut rng = XorShiftRng::from_seed([42; 16]); + + let spec = Arc::new(ChainSpec::foundation()); + let slot_clock = Arc::new(RwLock::new(TestingSlotClock::new(0))); + let beacon_node = Arc::new(TestBeaconNode::default()); + + let mut epoch_map = EpochMap::new(); + let produce_slot = 100; + let produce_epoch = produce_slot / spec.epoch_length; + epoch_map.insert(produce_epoch, produce_slot); + let epoch_map = Arc::new(epoch_map); + + let mut block_producer = BlockProducer::new( + spec.clone(), + epoch_map.clone(), + slot_clock.clone(), + beacon_node.clone(), + ); + + // Configure responses from the BeaconNode. + beacon_node.set_next_produce_result(Ok(Some(BeaconBlock::random_for_test(&mut rng)))); + beacon_node.set_next_publish_result(Ok(true)); + + // One slot before production slot... + slot_clock.write().unwrap().set_slot(produce_slot - 1); + assert_eq!( + block_producer.poll(), + Ok(PollOutcome::BlockProductionNotRequired(produce_slot - 1)) + ); + + // On the produce slot... + slot_clock.write().unwrap().set_slot(produce_slot); + assert_eq!( + block_producer.poll(), + Ok(PollOutcome::BlockProduced(produce_slot)) + ); + + // Trying the same produce slot again... + slot_clock.write().unwrap().set_slot(produce_slot); + assert_eq!( + block_producer.poll(), + Ok(PollOutcome::SlotAlreadyProcessed(produce_slot)) + ); + + // One slot after the produce slot... + slot_clock.write().unwrap().set_slot(produce_slot + 1); + assert_eq!( + block_producer.poll(), + Ok(PollOutcome::BlockProductionNotRequired(produce_slot + 1)) + ); + + // In an epoch without known duties... + let slot = (produce_epoch + 1) * spec.epoch_length; + slot_clock.write().unwrap().set_slot(slot); + assert_eq!( + block_producer.poll(), + Ok(PollOutcome::ProducerDutiesUnknown(slot)) + ); + } +} diff --git a/validator_client/block_proposer/src/test_node.rs b/validator_client/block_proposer/src/test_node.rs new file mode 100644 index 0000000000..e99613e8f3 --- /dev/null +++ b/validator_client/block_proposer/src/test_node.rs @@ -0,0 +1,47 @@ +use super::traits::{BeaconNode, BeaconNodeError}; +use std::sync::RwLock; +use types::BeaconBlock; + +type ProduceResult = Result, BeaconNodeError>; +type PublishResult = Result; + +/// A test-only struct used to simulate a Beacon Node. +#[derive(Default)] +pub struct TestBeaconNode { + pub produce_input: RwLock>, + pub produce_result: RwLock>, + pub publish_input: RwLock>, + pub publish_result: RwLock>, +} + +impl TestBeaconNode { + /// Set the result to be returned when `produce_beacon_block` is called. + pub fn set_next_produce_result(&self, result: ProduceResult) { + *self.produce_result.write().unwrap() = Some(result); + } + + /// Set the result to be returned when `publish_beacon_block` is called. + pub fn set_next_publish_result(&self, result: PublishResult) { + *self.publish_result.write().unwrap() = Some(result); + } +} + +impl BeaconNode for TestBeaconNode { + /// Returns the value specified by the `set_next_produce_result`. + fn produce_beacon_block(&self, slot: u64) -> ProduceResult { + *self.produce_input.write().unwrap() = Some(slot); + match *self.produce_result.read().unwrap() { + Some(ref r) => r.clone(), + None => panic!("TestBeaconNode: produce_result == None"), + } + } + + /// Returns the value specified by the `set_next_publish_result`. + fn publish_beacon_block(&self, block: BeaconBlock) -> PublishResult { + *self.publish_input.write().unwrap() = Some(block); + match *self.publish_result.read().unwrap() { + Some(ref r) => r.clone(), + None => panic!("TestBeaconNode: publish_result == None"), + } + } +} diff --git a/validator_client/block_proposer/src/traits.rs b/validator_client/block_proposer/src/traits.rs new file mode 100644 index 0000000000..e16af24606 --- /dev/null +++ b/validator_client/block_proposer/src/traits.rs @@ -0,0 +1,28 @@ +use types::BeaconBlock; + +#[derive(Debug, PartialEq, Clone)] +pub enum BeaconNodeError { + RemoteFailure(String), + DecodeFailure, +} + +/// Defines the methods required to produce and publish blocks on a Beacon Node. +pub trait BeaconNode: Send + Sync { + /// Request that the node produces a block. + /// + /// Returns Ok(None) if the Beacon Node is unable to produce at the given slot. + fn produce_beacon_block(&self, slot: u64) -> Result, BeaconNodeError>; + /// Request that the node publishes a block. + /// + /// Returns `true` if the publish was sucessful. + fn publish_beacon_block(&self, block: BeaconBlock) -> Result; +} + +pub enum DutiesReaderError { + UnknownEpoch, + Poisoned, +} + +pub trait DutiesReader: Send + Sync { + fn is_block_production_slot(&self, epoch: u64, slot: u64) -> Result; +}