diff --git a/validator_client/src/beacon_block_node.rs b/validator_client/src/beacon_block_node.rs new file mode 100644 index 000000000..bc85ef194 --- /dev/null +++ b/validator_client/src/beacon_block_node.rs @@ -0,0 +1,23 @@ +#[Derive(Debug, PartialEq, Clone)] +pub enum BeaconNodeError { + RemoteFailure(String), + DecodeFailure, +} + +/// Defines the methods required to produce and publish blocks on a Beacon Node. Abstracts the +/// actual 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: Slot, + randao_reveal: &Signature, + ) -> Result, BeaconNodeError>; + + /// Request that the node publishes a block. + /// + /// Returns `true` if the publish was sucessful. + fn publish_beacon_block(&self, block: BeaconBlock) -> Result; +} diff --git a/validator_client/src/block_producer_service/beacon_block_grpc_client.rs b/validator_client/src/block_producer_service/beacon_block_grpc_client.rs deleted file mode 100644 index ba2acfffb..000000000 --- a/validator_client/src/block_producer_service/beacon_block_grpc_client.rs +++ /dev/null @@ -1,81 +0,0 @@ -use block_proposer::{BeaconNode, BeaconNodeError, PublishOutcome}; -use protos::services::{ - BeaconBlock as GrpcBeaconBlock, ProduceBeaconBlockRequest, PublishBeaconBlockRequest, -}; -use protos::services_grpc::BeaconBlockServiceClient; -use ssz::{ssz_encode, Decodable}; -use std::sync::Arc; -use types::{BeaconBlock, Signature, Slot}; - -/// A newtype designed to wrap the gRPC-generated service so the `BeaconNode` trait may be -/// implemented upon it. -pub struct BeaconBlockGrpcClient { - client: Arc, -} - -impl BeaconBlockGrpcClient { - pub fn new(client: Arc) -> Self { - Self { client } - } -} - -impl BeaconNode for BeaconBlockGrpcClient { - /// Request a Beacon Node (BN) to produce a new block at the supplied slot. - /// - /// Returns `None` if it is not possible to produce at the supplied slot. For example, if the - /// BN is unable to find a parent block. - fn produce_beacon_block( - &self, - slot: Slot, - // TODO: use randao_reveal, when proto APIs have been updated. - _randao_reveal: &Signature, - ) -> Result, BeaconNodeError> { - let mut req = ProduceBeaconBlockRequest::new(); - req.set_slot(slot.as_u64()); - - let reply = self - .client - .produce_beacon_block(&req) - .map_err(|err| BeaconNodeError::RemoteFailure(format!("{:?}", err)))?; - - if reply.has_block() { - let block = reply.get_block(); - let ssz = block.get_ssz(); - - let (block, _i) = - BeaconBlock::ssz_decode(&ssz, 0).map_err(|_| BeaconNodeError::DecodeFailure)?; - - Ok(Some(block)) - } else { - Ok(None) - } - } - - /// Request a Beacon Node (BN) to publish a block. - /// - /// Generally, this will be called after a `produce_beacon_block` call with a block that has - /// been completed (signed) by the validator client. - fn publish_beacon_block(&self, block: BeaconBlock) -> Result { - let mut req = PublishBeaconBlockRequest::new(); - - let ssz = ssz_encode(&block); - - // TODO: this conversion is incomplete; fix it. - let mut grpc_block = GrpcBeaconBlock::new(); - grpc_block.set_ssz(ssz); - - req.set_block(grpc_block); - - let reply = self - .client - .publish_beacon_block(&req) - .map_err(|err| BeaconNodeError::RemoteFailure(format!("{:?}", err)))?; - - if reply.get_success() { - Ok(PublishOutcome::ValidBlock) - } else { - // TODO: distinguish between different errors - Ok(PublishOutcome::InvalidBlock("Publish failed".to_string())) - } - } -} diff --git a/validator_client/src/block_producer_service/block_producer.rs b/validator_client/src/block_producer_service/block_producer.rs new file mode 100644 index 000000000..187f2b649 --- /dev/null +++ b/validator_client/src/block_producer_service/block_producer.rs @@ -0,0 +1,227 @@ +pub mod test_utils; +mod traits; + +use slot_clock::SlotClock; +use ssz::{SignedRoot, TreeHash}; +use std::sync::Arc; +use types::{BeaconBlock, ChainSpec, Domain, Slot}; + +#[derive(Debug, PartialEq)] +pub enum Error { + SlotClockError, + SlotUnknowable, + EpochMapPoisoned, + SlotClockPoisoned, + EpochLengthIsZero, + BeaconNodeError(BeaconNodeError), +} + +#[derive(Debug, PartialEq)] +pub enum BlockProducerEvent { + /// A new block was produced. + BlockProduced(Slot), + /// A block was not produced as it would have been slashable. + SlashableBlockNotProduced(Slot), + /// The Beacon Node was unable to produce a block at that slot. + BeaconNodeUnableToProduceBlock(Slot), + /// The signer failed to sign the message. + SignerRejection(Slot), + /// The public key for this validator is not an active validator. + ValidatorIsUnknown(Slot), +} + +/// This struct contains the logic for requesting and signing beacon blocks for a validator. The +/// validator can abstractly sign via the Signer trait object. +pub struct BlockProducer { + /// The current fork. + pub fork: Fork, + /// The current slot to produce a block for. + pub slot: Slot, + /// The current epoch. + pub epoch: Epoch, + /// The beacon node to connect to. + pub beacon_node: Arc, + /// The signer to sign the block. + pub signer: Arc, +} + +impl BlockProducer { + + /// 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) -> Result { + + let randao_reveal = { + // TODO: add domain, etc to this message. Also ensure result matches `into_to_bytes32`. + let message = slot.epoch(self.spec.slots_per_epoch).hash_tree_root(); + + match self.signer.sign_randao_reveal( + &message, + self.spec + .get_domain(slot.epoch(self.spec.slots_per_epoch), Domain::Randao, &fork), + ) { + None => return Ok(PollOutcome::SignerRejection(slot)), + Some(signature) => signature, + } + }; + + if let Some(block) = self + .beacon_node + .produce_beacon_block(slot, &randao_reveal)? + { + if self.safe_to_produce(&block) { + let domain = self.spec.get_domain( + slot.epoch(self.spec.slots_per_epoch), + Domain::BeaconBlock, + &fork, + ); + if let Some(block) = self.sign_block(block, domain) { + self.beacon_node.publish_beacon_block(block)?; + Ok(PollOutcome::BlockProduced(slot)) + } else { + Ok(PollOutcome::SignerRejection(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, mut block: BeaconBlock, domain: u64) -> Option { + self.store_produce(&block); + + match self + .signer + .sign_block_proposal(&block.signed_root()[..], domain) + { + None => None, + Some(signature) => { + block.signature = signature; + Some(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) + } +} + + +/* Old tests - Re-work for new logic +#[cfg(test)] +mod tests { + use super::test_utils::{EpochMap, LocalSigner, SimulatedBeaconNode}; + use super::*; + use slot_clock::TestingSlotClock; + use types::{ + test_utils::{SeedableRng, TestRandom, XorShiftRng}, + Keypair, + }; + + // TODO: implement more thorough testing. + // https://github.com/sigp/lighthouse/issues/160 + // + // These tests should serve as a good example for future tests. + + #[test] + pub fn polling() { + let mut rng = XorShiftRng::from_seed([42; 16]); + + let spec = Arc::new(ChainSpec::foundation()); + let slot_clock = Arc::new(TestingSlotClock::new(0)); + let beacon_node = Arc::new(SimulatedBeaconNode::default()); + let signer = Arc::new(LocalSigner::new(Keypair::random())); + + let mut epoch_map = EpochMap::new(spec.slots_per_epoch); + let produce_slot = Slot::new(100); + let produce_epoch = produce_slot.epoch(spec.slots_per_epoch); + epoch_map.map.insert(produce_epoch, produce_slot); + let epoch_map = Arc::new(epoch_map); + + let mut block_proposer = BlockProducer::new( + spec.clone(), + epoch_map.clone(), + slot_clock.clone(), + beacon_node.clone(), + signer.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(PublishOutcome::ValidBlock)); + + // One slot before production slot... + slot_clock.set_slot(produce_slot.as_u64() - 1); + assert_eq!( + block_proposer.poll(), + Ok(PollOutcome::BlockProductionNotRequired(produce_slot - 1)) + ); + + // On the produce slot... + slot_clock.set_slot(produce_slot.as_u64()); + assert_eq!( + block_proposer.poll(), + Ok(PollOutcome::BlockProduced(produce_slot.into())) + ); + + // Trying the same produce slot again... + slot_clock.set_slot(produce_slot.as_u64()); + assert_eq!( + block_proposer.poll(), + Ok(PollOutcome::SlotAlreadyProcessed(produce_slot)) + ); + + // One slot after the produce slot... + slot_clock.set_slot(produce_slot.as_u64() + 1); + assert_eq!( + block_proposer.poll(), + Ok(PollOutcome::BlockProductionNotRequired(produce_slot + 1)) + ); + + // In an epoch without known duties... + let slot = (produce_epoch.as_u64() + 1) * spec.slots_per_epoch; + slot_clock.set_slot(slot); + assert_eq!( + block_proposer.poll(), + Ok(PollOutcome::ProducerDutiesUnknown(Slot::new(slot))) + ); + } +} diff --git a/validator_client/src/block_producer_service/mod.rs b/validator_client/src/block_producer_service/mod.rs index 91e7606a7..8a8cce613 100644 --- a/validator_client/src/block_producer_service/mod.rs +++ b/validator_client/src/block_producer_service/mod.rs @@ -1,61 +1,80 @@ -mod beacon_block_grpc_client; -// mod block_producer_service; - -use block_proposer::{ - BeaconNode, BlockProducer, DutiesReader, PollOutcome as BlockProducerPollOutcome, Signer, +use protos::services::{ + BeaconBlock as GrpcBeaconBlock, ProduceBeaconBlockRequest, PublishBeaconBlockRequest, }; -use slog::{error, info, warn, Logger}; -use slot_clock::SlotClock; -use std::time::Duration; +use protos::services_grpc::BeaconBlockServiceClient; +use ssz::{ssz_encode, Decodable}; +use std::sync::Arc; +use types::{BeaconBlock, Signature, Slot}; -pub use self::beacon_block_grpc_client::BeaconBlockGrpcClient; - -pub struct BlockProducerService { - pub block_producer: BlockProducer, - pub poll_interval_millis: u64, - pub log: Logger, +/// A newtype designed to wrap the gRPC-generated service so the `BeaconNode` trait may be +/// implemented upon it. +pub struct BeaconBlockGrpcClient { + inner: Arc, } -impl BlockProducerService { - /// Run a loop which polls the block producer each `poll_interval_millis` millseconds. - /// - /// Logs the results of the polls. - pub fn run(&mut self) { - loop { - match self.block_producer.poll() { - Err(error) => { - error!(self.log, "Block producer poll error"; "error" => format!("{:?}", error)) - } - Ok(BlockProducerPollOutcome::BlockProduced(slot)) => { - info!(self.log, "Produced block"; "slot" => slot) - } - Ok(BlockProducerPollOutcome::SlashableBlockNotProduced(slot)) => { - warn!(self.log, "Slashable block was not signed"; "slot" => slot) - } - Ok(BlockProducerPollOutcome::BlockProductionNotRequired(slot)) => { - info!(self.log, "Block production not required"; "slot" => slot) - } - Ok(BlockProducerPollOutcome::ProducerDutiesUnknown(slot)) => { - error!(self.log, "Block production duties unknown"; "slot" => slot) - } - Ok(BlockProducerPollOutcome::SlotAlreadyProcessed(slot)) => { - warn!(self.log, "Attempted to re-process slot"; "slot" => slot) - } - Ok(BlockProducerPollOutcome::BeaconNodeUnableToProduceBlock(slot)) => { - error!(self.log, "Beacon node unable to produce block"; "slot" => slot) - } - Ok(BlockProducerPollOutcome::SignerRejection(slot)) => { - error!(self.log, "The cryptographic signer refused to sign the block"; "slot" => slot) - } - Ok(BlockProducerPollOutcome::ValidatorIsUnknown(slot)) => { - error!(self.log, "The Beacon Node does not recognise the validator"; "slot" => slot) - } - Ok(BlockProducerPollOutcome::UnableToGetFork(slot)) => { - error!(self.log, "Unable to get a `Fork` struct to generate signature domains"; "slot" => slot) - } - }; +impl BeaconBlockGrpcClient { + pub fn new(client: Arc) -> Self { + Self { inner: client } + } +} - std::thread::sleep(Duration::from_millis(self.poll_interval_millis)); +impl BeaconNode for BeaconBlockGrpcClient { + /// Request a Beacon Node (BN) to produce a new block at the supplied slot. + /// + /// Returns `None` if it is not possible to produce at the supplied slot. For example, if the + /// BN is unable to find a parent block. + fn produce_beacon_block( + &self, + slot: Slot, + // TODO: use randao_reveal, when proto APIs have been updated. + _randao_reveal: &Signature, + ) -> Result, BeaconNodeError> { + let mut req = ProduceBeaconBlockRequest::new(); + req.set_slot(slot.as_u64()); + + let reply = self + .client + .produce_beacon_block(&req) + .map_err(|err| BeaconNodeError::RemoteFailure(format!("{:?}", err)))?; + + if reply.has_block() { + let block = reply.get_block(); + let ssz = block.get_ssz(); + + let (block, _i) = + BeaconBlock::ssz_decode(&ssz, 0).map_err(|_| BeaconNodeError::DecodeFailure)?; + + Ok(Some(block)) + } else { + Ok(None) + } + } + + /// Request a Beacon Node (BN) to publish a block. + /// + /// Generally, this will be called after a `produce_beacon_block` call with a block that has + /// been completed (signed) by the validator client. + fn publish_beacon_block(&self, block: BeaconBlock) -> Result { + let mut req = PublishBeaconBlockRequest::new(); + + let ssz = ssz_encode(&block); + + // TODO: this conversion is incomplete; fix it. + let mut grpc_block = GrpcBeaconBlock::new(); + grpc_block.set_ssz(ssz); + + req.set_block(grpc_block); + + let reply = self + .client + .publish_beacon_block(&req) + .map_err(|err| BeaconNodeError::RemoteFailure(format!("{:?}", err)))?; + + if reply.get_success() { + Ok(PublishOutcome::ValidBlock) + } else { + // TODO: distinguish between different errors + Ok(PublishOutcome::InvalidBlock("Publish failed".to_string())) } } } diff --git a/validator_client/src/service.rs b/validator_client/src/service.rs index fb0304f87..64ed7cb03 100644 --- a/validator_client/src/service.rs +++ b/validator_client/src/service.rs @@ -78,7 +78,7 @@ impl Service { // Beacon node gRPC beacon node endpoints. let beacon_node_client = { let ch = ChannelBuilder::new(env.clone()).connect(&config.server); - Arc::new(BeaconNodeServiceClient::new(ch)) + BeaconNodeServiceClient::new(ch) }; // retrieve node information and validate the beacon node @@ -287,7 +287,6 @@ impl Service { } /* - // Spawn a new thread to perform block production for the validator. let producer_thread = { let spec = spec.clone();