Add unsafe attester
crate
This commit is contained in:
parent
d5da84d967
commit
acf8b79fe9
@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"eth2/attester",
|
||||
"eth2/block_producer",
|
||||
"eth2/genesis",
|
||||
"eth2/naive_fork_choice",
|
||||
|
10
eth2/attester/Cargo.toml
Normal file
10
eth2/attester/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "attester"
|
||||
version = "0.1.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
slot_clock = { path = "../../eth2/utils/slot_clock" }
|
||||
ssz = { path = "../../eth2/utils/ssz" }
|
||||
types = { path = "../../eth2/types" }
|
244
eth2/attester/src/lib.rs
Normal file
244
eth2/attester/src/lib.rs
Normal file
@ -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<T: SlotClock, U: BeaconNode, V: DutiesReader, W: Signer> {
|
||||
pub last_processed_slot: Option<u64>,
|
||||
duties: Arc<V>,
|
||||
slot_clock: Arc<T>,
|
||||
beacon_node: Arc<U>,
|
||||
signer: Arc<W>,
|
||||
}
|
||||
|
||||
impl<T: SlotClock, U: BeaconNode, V: DutiesReader, W: Signer> Attester<T, U, V, W> {
|
||||
/// Returns a new instance where `last_processed_slot == 0`.
|
||||
pub fn new(duties: Arc<V>, slot_clock: Arc<T>, beacon_node: Arc<U>, signer: Arc<W>) -> Self {
|
||||
Self {
|
||||
last_processed_slot: None,
|
||||
duties,
|
||||
slot_clock,
|
||||
beacon_node,
|
||||
signer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SlotClock, U: BeaconNode, V: DutiesReader, W: Signer> Attester<T, U, V, W> {
|
||||
/// Poll the `BeaconNode` and produce an attestation if required.
|
||||
pub fn poll(&mut self) -> Result<PollOutcome, Error> {
|
||||
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<PollOutcome, Error> {
|
||||
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<Signature> {
|
||||
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<BeaconNodeError> 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))
|
||||
);
|
||||
}
|
||||
}
|
49
eth2/attester/src/test_utils/beacon_node.rs
Normal file
49
eth2/attester/src/test_utils/beacon_node.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use crate::traits::{BeaconNode, BeaconNodeError, PublishOutcome};
|
||||
use std::sync::RwLock;
|
||||
use types::{AttestationData, Signature};
|
||||
|
||||
type ProduceResult = Result<Option<AttestationData>, BeaconNodeError>;
|
||||
type PublishResult = Result<PublishOutcome, BeaconNodeError>;
|
||||
|
||||
/// A test-only struct used to simulate a Beacon Node.
|
||||
#[derive(Default)]
|
||||
pub struct TestBeaconNode {
|
||||
pub produce_input: RwLock<Option<(u64, u64)>>,
|
||||
pub produce_result: RwLock<Option<ProduceResult>>,
|
||||
|
||||
pub publish_input: RwLock<Option<(AttestationData, Signature, u64)>>,
|
||||
pub publish_result: RwLock<Option<PublishResult>>,
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
44
eth2/attester/src/test_utils/epoch_map.rs
Normal file
44
eth2/attester/src/test_utils/epoch_map.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use crate::{DutiesReader, DutiesReaderError};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct TestEpochMap {
|
||||
epoch_length: u64,
|
||||
validator_index: Option<u64>,
|
||||
map: HashMap<u64, (u64, u64)>,
|
||||
}
|
||||
|
||||
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<u64>) {
|
||||
self.validator_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
impl DutiesReader for TestEpochMap {
|
||||
fn attestation_shard(&self, slot: u64) -> Result<Option<u64>, 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<u64> {
|
||||
self.validator_index
|
||||
}
|
||||
}
|
7
eth2/attester/src/test_utils/mod.rs
Normal file
7
eth2/attester/src/test_utils/mod.rs
Normal file
@ -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;
|
31
eth2/attester/src/test_utils/signer.rs
Normal file
31
eth2/attester/src/test_utils/signer.rs
Normal file
@ -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<bool>,
|
||||
}
|
||||
|
||||
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<Signature> {
|
||||
Some(Signature::new(message, &self.keypair.sk))
|
||||
}
|
||||
}
|
51
eth2/attester/src/traits.rs
Normal file
51
eth2/attester/src/traits.rs
Normal file
@ -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<Option<AttestationData>, BeaconNodeError>;
|
||||
|
||||
fn publish_attestation_data(
|
||||
&self,
|
||||
attestation_data: AttestationData,
|
||||
signature: Signature,
|
||||
validator_index: u64,
|
||||
) -> Result<PublishOutcome, BeaconNodeError>;
|
||||
}
|
||||
|
||||
#[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<Option<u64>, DutiesReaderError>;
|
||||
|
||||
/// Returns `Some(shard)` if this slot is an attestation slot. Otherwise, returns `None.`
|
||||
fn validator_index(&self) -> Option<u64>;
|
||||
}
|
||||
|
||||
/// Signs message using an internally-maintained private key.
|
||||
pub trait Signer {
|
||||
fn bls_sign(&self, message: &[u8]) -> Option<Signature>;
|
||||
}
|
Loading…
Reference in New Issue
Block a user