diff --git a/Cargo.toml b/Cargo.toml index 57b5de8ce..59dfce953 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "eth2/attester", "eth2/block_producer", "eth2/genesis", "eth2/naive_fork_choice", diff --git a/eth2/attester/Cargo.toml b/eth2/attester/Cargo.toml new file mode 100644 index 000000000..956ecf565 --- /dev/null +++ b/eth2/attester/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "attester" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2018" + +[dependencies] +slot_clock = { path = "../../eth2/utils/slot_clock" } +ssz = { path = "../../eth2/utils/ssz" } +types = { path = "../../eth2/types" } diff --git a/eth2/attester/src/lib.rs b/eth2/attester/src/lib.rs new file mode 100644 index 000000000..7e3f0eb6b --- /dev/null +++ b/eth2/attester/src/lib.rs @@ -0,0 +1,244 @@ +pub mod test_utils; +mod traits; + +use slot_clock::SlotClock; +use std::sync::Arc; +use types::{AttestationData, Signature}; + +pub use self::traits::{ + BeaconNode, BeaconNodeError, DutiesReader, DutiesReaderError, PublishOutcome, Signer, +}; + +const PHASE_0_CUSTODY_BIT: bool = false; + +#[derive(Debug, PartialEq)] +pub enum PollOutcome { + AttestationProduced(u64), + AttestationNotRequired(u64), + SlashableAttestationNotProduced(u64), + BeaconNodeUnableToProduceAttestation(u64), + ProducerDutiesUnknown(u64), + SlotAlreadyProcessed(u64), + SignerRejection(u64), + ValidatorIsUnknown(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 Attester { + pub last_processed_slot: Option, + duties: Arc, + slot_clock: Arc, + beacon_node: Arc, + signer: Arc, +} + +impl Attester { + /// Returns a new instance where `last_processed_slot == 0`. + pub fn new(duties: Arc, slot_clock: Arc, beacon_node: Arc, signer: Arc) -> Self { + Self { + last_processed_slot: None, + duties, + slot_clock, + beacon_node, + signer, + } + } +} + +impl Attester { + /// Poll the `BeaconNode` and produce an attestation if required. + pub fn poll(&mut self) -> Result { + let slot = self + .slot_clock + .present_slot() + .map_err(|_| Error::SlotClockError)? + .ok_or(Error::SlotUnknowable)?; + + if !self.is_processed_slot(slot) { + self.last_processed_slot = Some(slot); + + let shard = match self.duties.attestation_shard(slot) { + Ok(Some(result)) => result, + Ok(None) => return Ok(PollOutcome::AttestationNotRequired(slot)), + Err(DutiesReaderError::UnknownEpoch) => { + return Ok(PollOutcome::ProducerDutiesUnknown(slot)); + } + Err(DutiesReaderError::UnknownValidator) => { + return Ok(PollOutcome::ValidatorIsUnknown(slot)); + } + Err(DutiesReaderError::EpochLengthIsZero) => return Err(Error::EpochLengthIsZero), + Err(DutiesReaderError::Poisoned) => return Err(Error::EpochMapPoisoned), + }; + + self.produce_attestation(slot, shard) + } else { + Ok(PollOutcome::SlotAlreadyProcessed(slot)) + } + } + + fn produce_attestation(&mut self, slot: u64, shard: u64) -> Result { + let attestation_data = match self.beacon_node.produce_attestation_data(slot, shard)? { + Some(attestation_data) => attestation_data, + None => return Ok(PollOutcome::BeaconNodeUnableToProduceAttestation(slot)), + }; + + if !self.safe_to_produce(&attestation_data) { + return Ok(PollOutcome::SlashableAttestationNotProduced(slot)); + } + + let signature = match self.sign_attestation_data(&attestation_data) { + Some(signature) => signature, + None => return Ok(PollOutcome::SignerRejection(slot)), + }; + + let validator_index = match self.duties.validator_index() { + Some(validator_index) => validator_index, + None => return Ok(PollOutcome::ValidatorIsUnknown(slot)), + }; + + self.beacon_node + .publish_attestation_data(attestation_data, signature, validator_index)?; + Ok(PollOutcome::AttestationProduced(slot)) + } + + fn is_processed_slot(&self, slot: u64) -> bool { + match self.last_processed_slot { + Some(processed_slot) if slot <= processed_slot => true, + _ => false, + } + } + + /// 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_attestation_data(&mut self, attestation_data: &AttestationData) -> Option { + self.store_produce(attestation_data); + + self.signer + .bls_sign(&attestation_data.signable_message(PHASE_0_CUSTODY_BIT)[..]) + } + + /// Returns `true` if signing some attestation_data is safe (non-slashable). + /// + /// !!! UNSAFE !!! + /// + /// Important: this function is presently stubbed-out. It provides ZERO SAFETY. + fn safe_to_produce(&self, _attestation_data: &AttestationData) -> 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: &AttestationData) { + // 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_utils::{TestBeaconNode, TestEpochMap, TestSigner}; + use super::*; + use slot_clock::TestingSlotClock; + use types::{ + test_utils::{SeedableRng, TestRandom, XorShiftRng}, + ChainSpec, 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(TestBeaconNode::default()); + let signer = Arc::new(TestSigner::new(Keypair::random())); + + let mut duties = TestEpochMap::new(spec.epoch_length); + let attest_slot = 100; + let attest_epoch = attest_slot / spec.epoch_length; + let attest_shard = 12; + duties.insert_attestation_shard(attest_slot, attest_shard); + duties.set_validator_index(Some(2)); + let duties = Arc::new(duties); + + let mut attester = Attester::new( + duties.clone(), + slot_clock.clone(), + beacon_node.clone(), + signer.clone(), + ); + + // Configure responses from the BeaconNode. + beacon_node.set_next_produce_result(Ok(Some(AttestationData::random_for_test(&mut rng)))); + beacon_node.set_next_publish_result(Ok(PublishOutcome::ValidAttestation)); + + // One slot before attestation slot... + slot_clock.set_slot(attest_slot - 1); + assert_eq!( + attester.poll(), + Ok(PollOutcome::AttestationNotRequired(attest_slot - 1)) + ); + + // On the attest slot... + slot_clock.set_slot(attest_slot); + assert_eq!( + attester.poll(), + Ok(PollOutcome::AttestationProduced(attest_slot)) + ); + + // Trying the same attest slot again... + slot_clock.set_slot(attest_slot); + assert_eq!( + attester.poll(), + Ok(PollOutcome::SlotAlreadyProcessed(attest_slot)) + ); + + // One slot after the attest slot... + slot_clock.set_slot(attest_slot + 1); + assert_eq!( + attester.poll(), + Ok(PollOutcome::AttestationNotRequired(attest_slot + 1)) + ); + + // In an epoch without known duties... + let slot = (attest_epoch + 1) * spec.epoch_length; + slot_clock.set_slot(slot); + assert_eq!( + attester.poll(), + Ok(PollOutcome::ProducerDutiesUnknown(slot)) + ); + } +} diff --git a/eth2/attester/src/test_utils/beacon_node.rs b/eth2/attester/src/test_utils/beacon_node.rs new file mode 100644 index 000000000..4ebcbdf5a --- /dev/null +++ b/eth2/attester/src/test_utils/beacon_node.rs @@ -0,0 +1,49 @@ +use crate::traits::{BeaconNode, BeaconNodeError, PublishOutcome}; +use std::sync::RwLock; +use types::{AttestationData, Signature}; + +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 { + pub fn set_next_produce_result(&self, result: ProduceResult) { + *self.produce_result.write().unwrap() = Some(result); + } + + pub fn set_next_publish_result(&self, result: PublishResult) { + *self.publish_result.write().unwrap() = Some(result); + } +} + +impl BeaconNode for TestBeaconNode { + fn produce_attestation_data(&self, slot: u64, shard: u64) -> ProduceResult { + *self.produce_input.write().unwrap() = Some((slot, shard)); + match *self.produce_result.read().unwrap() { + Some(ref r) => r.clone(), + None => panic!("TestBeaconNode: produce_result == None"), + } + } + + fn publish_attestation_data( + &self, + attestation_data: AttestationData, + signature: Signature, + validator_index: u64, + ) -> PublishResult { + *self.publish_input.write().unwrap() = Some((attestation_data, signature, validator_index)); + match *self.publish_result.read().unwrap() { + Some(ref r) => r.clone(), + None => panic!("TestBeaconNode: publish_result == None"), + } + } +} diff --git a/eth2/attester/src/test_utils/epoch_map.rs b/eth2/attester/src/test_utils/epoch_map.rs new file mode 100644 index 000000000..9f44ecf1a --- /dev/null +++ b/eth2/attester/src/test_utils/epoch_map.rs @@ -0,0 +1,44 @@ +use crate::{DutiesReader, DutiesReaderError}; +use std::collections::HashMap; + +pub struct TestEpochMap { + epoch_length: u64, + validator_index: Option, + map: HashMap, +} + +impl TestEpochMap { + pub fn new(epoch_length: u64) -> Self { + Self { + epoch_length, + validator_index: None, + map: HashMap::new(), + } + } + + pub fn insert_attestation_shard(&mut self, slot: u64, shard: u64) { + let epoch = slot / self.epoch_length; + + self.map.insert(epoch, (slot, shard)); + } + + pub fn set_validator_index(&mut self, index: Option) { + self.validator_index = index; + } +} + +impl DutiesReader for TestEpochMap { + fn attestation_shard(&self, slot: u64) -> Result, DutiesReaderError> { + let epoch = slot / self.epoch_length; + + match self.map.get(&epoch) { + Some((attest_slot, attest_shard)) if *attest_slot == slot => Ok(Some(*attest_shard)), + Some((attest_slot, _attest_shard)) if *attest_slot != slot => Ok(None), + _ => Err(DutiesReaderError::UnknownEpoch), + } + } + + fn validator_index(&self) -> Option { + self.validator_index + } +} diff --git a/eth2/attester/src/test_utils/mod.rs b/eth2/attester/src/test_utils/mod.rs new file mode 100644 index 000000000..0dd384b12 --- /dev/null +++ b/eth2/attester/src/test_utils/mod.rs @@ -0,0 +1,7 @@ +mod beacon_node; +mod epoch_map; +mod signer; + +pub use self::beacon_node::TestBeaconNode; +pub use self::epoch_map::TestEpochMap; +pub use self::signer::TestSigner; diff --git a/eth2/attester/src/test_utils/signer.rs b/eth2/attester/src/test_utils/signer.rs new file mode 100644 index 000000000..d43c0feb0 --- /dev/null +++ b/eth2/attester/src/test_utils/signer.rs @@ -0,0 +1,31 @@ +use crate::traits::Signer; +use std::sync::RwLock; +use types::{Keypair, Signature}; + +/// A test-only struct used to simulate a Beacon Node. +pub struct TestSigner { + keypair: Keypair, + should_sign: RwLock, +} + +impl TestSigner { + /// Produce a new TestSigner with signing enabled by default. + pub fn new(keypair: Keypair) -> Self { + Self { + keypair, + should_sign: RwLock::new(true), + } + } + + /// If set to `false`, the service will refuse to sign all messages. Otherwise, all messages + /// will be signed. + pub fn enable_signing(&self, enabled: bool) { + *self.should_sign.write().unwrap() = enabled; + } +} + +impl Signer for TestSigner { + fn bls_sign(&self, message: &[u8]) -> Option { + Some(Signature::new(message, &self.keypair.sk)) + } +} diff --git a/eth2/attester/src/traits.rs b/eth2/attester/src/traits.rs new file mode 100644 index 000000000..bc3ccf63f --- /dev/null +++ b/eth2/attester/src/traits.rs @@ -0,0 +1,51 @@ +use types::{AttestationData, Signature}; + +#[derive(Debug, PartialEq, Clone)] +pub enum BeaconNodeError { + RemoteFailure(String), + DecodeFailure, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum PublishOutcome { + ValidAttestation, + InvalidAttestation(String), +} + +/// Defines the methods required to produce and publish blocks on a Beacon Node. +pub trait BeaconNode: Send + Sync { + fn produce_attestation_data( + &self, + slot: u64, + shard: u64, + ) -> Result, BeaconNodeError>; + + fn publish_attestation_data( + &self, + attestation_data: AttestationData, + signature: Signature, + validator_index: u64, + ) -> Result; +} + +#[derive(Debug, PartialEq, Clone)] +pub enum DutiesReaderError { + UnknownValidator, + UnknownEpoch, + EpochLengthIsZero, + Poisoned, +} + +/// Informs a validator of their duties (e.g., block production). +pub trait DutiesReader: Send + Sync { + /// Returns `Some(shard)` if this slot is an attestation slot. Otherwise, returns `None.` + fn attestation_shard(&self, slot: u64) -> Result, DutiesReaderError>; + + /// Returns `Some(shard)` if this slot is an attestation slot. Otherwise, returns `None.` + fn validator_index(&self) -> Option; +} + +/// Signs message using an internally-maintained private key. +pub trait Signer { + fn bls_sign(&self, message: &[u8]) -> Option; +}