Implement Slashing Protection (#1116)

* Implement slashing protection

Roll-up of #588 with some conflicts resolved

* WIP improvements

* Require slot uniqueness for blocks (rather than epochs)
* Native DB support for Slot and Epoch
* Simplify surrounding/surrounded-by queries

* Implement unified slashing protection database

A single SQL database saves on open file descriptors.

* Make slashing protection concurrency safe.

Revive tests, add parallel tests.

* Some simplifications

* Auto-registration, test clean-ups

* More tests, clean-ups, hardening

* Fix comments in BLS

* Optimise bulk validator registration

* Delete outdated tests

* Use bundled SQLite in slashing protection

* Auto-register validators in simulation

* Use real signing_root in slashing protection

* Update book for --auto-register

* Refine log messages and help flags

* Correct typo in Cargo.toml authors

* Fix merge conflicts

* Safer error handling in sqlite slot/epoch

* Address review comments

* Add attestation test mutating block root

Co-authored-by: pscott <scottpiriou@gmail.com>
This commit is contained in:
Michael Sproul
2020-05-18 16:25:16 +10:00
committed by GitHub
parent 90b3953dda
commit 2d8e2dd7f5
30 changed files with 1720 additions and 52 deletions

View File

@@ -17,6 +17,7 @@ eth2_config = { path = "../eth2/utils/eth2_config" }
tree_hash = "0.1.0"
clap = "2.33.0"
eth2_interop_keypairs = { path = "../eth2/utils/eth2_interop_keypairs" }
slashing_protection = { path = "./slashing_protection" }
slot_clock = { path = "../eth2/utils/slot_clock" }
rest_types = { path = "../eth2/utils/rest_types" }
types = { path = "../eth2/types" }

View File

@@ -0,0 +1,17 @@
[package]
name = "slashing_protection"
version = "0.1.0"
authors = ["Michael Sproul <michael@sigmaprime.io>", "pscott <scottpiriou@gmail.com>"]
edition = "2018"
[dependencies]
tempfile = "3.1.0"
types = { path = "../../eth2/types" }
tree_hash = { path = "../../eth2/utils/tree_hash" }
rusqlite = { version = "0.22.0", features = ["bundled"] }
r2d2 = "0.8.8"
r2d2_sqlite = "0.15"
parking_lot = "0.9.0"
[dev-dependencies]
rayon = "1.3.0"

View File

@@ -0,0 +1,389 @@
#![cfg(test)]
use crate::test_utils::*;
use crate::*;
use types::{AttestationData, Checkpoint, Epoch, Hash256, Slot};
pub fn build_checkpoint(epoch_num: u64) -> Checkpoint {
Checkpoint {
epoch: Epoch::from(epoch_num),
root: Hash256::zero(),
}
}
pub fn attestation_data_builder(source: u64, target: u64) -> AttestationData {
let source = build_checkpoint(source);
let target = build_checkpoint(target);
let index = 0u64;
let slot = Slot::from(0u64);
AttestationData {
slot,
index,
beacon_block_root: Hash256::zero(),
source,
target,
}
}
/// Create a signed attestation from `attestation`, assuming the default domain.
fn signed_att(attestation: &AttestationData) -> SignedAttestation {
SignedAttestation::from_attestation(attestation, DEFAULT_DOMAIN)
}
#[test]
fn valid_empty_history() {
StreamTest {
cases: vec![Test::single(attestation_data_builder(2, 3))],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_genesis() {
StreamTest {
cases: vec![Test::single(attestation_data_builder(0, 0))],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_out_of_order_attestation() {
StreamTest {
cases: vec![
Test::single(attestation_data_builder(0, 3)),
Test::single(attestation_data_builder(2, 5)),
Test::single(attestation_data_builder(1, 4)),
],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_repeat_attestation() {
StreamTest {
cases: vec![
Test::single(attestation_data_builder(0, 1)),
Test::single(attestation_data_builder(0, 1)).expect_same_data(),
],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_source_from_first_entry() {
StreamTest {
cases: vec![
Test::single(attestation_data_builder(6, 7)),
Test::single(attestation_data_builder(6, 8)),
],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_multiple_validators_double_vote() {
StreamTest {
registered_validators: vec![pubkey(0), pubkey(1)],
cases: vec![
Test::with_pubkey(pubkey(0), attestation_data_builder(0, 1)),
Test::with_pubkey(pubkey(1), attestation_data_builder(0, 1)),
],
}
.run()
}
#[test]
fn valid_vote_chain_repeat_first() {
StreamTest {
cases: vec![
Test::single(attestation_data_builder(0, 1)),
Test::single(attestation_data_builder(1, 2)),
Test::single(attestation_data_builder(2, 3)),
Test::single(attestation_data_builder(0, 1)).expect_same_data(),
],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_vote_chain_repeat_middle() {
StreamTest {
cases: vec![
Test::single(attestation_data_builder(0, 1)),
Test::single(attestation_data_builder(1, 2)),
Test::single(attestation_data_builder(2, 3)),
Test::single(attestation_data_builder(1, 2)).expect_same_data(),
],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_vote_chain_repeat_last() {
StreamTest {
cases: vec![
Test::single(attestation_data_builder(0, 1)),
Test::single(attestation_data_builder(1, 2)),
Test::single(attestation_data_builder(2, 3)),
Test::single(attestation_data_builder(2, 3)).expect_same_data(),
],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_multiple_validators_not_surrounding() {
// Attestations that would be problematic if they came from the same validator, but are OK
// coming from different validators.
StreamTest {
registered_validators: vec![pubkey(0), pubkey(1)],
cases: vec![
Test::with_pubkey(pubkey(0), attestation_data_builder(0, 10)),
Test::with_pubkey(pubkey(0), attestation_data_builder(10, 20)),
Test::with_pubkey(pubkey(1), attestation_data_builder(1, 9)),
Test::with_pubkey(pubkey(1), attestation_data_builder(9, 21)),
],
}
.run()
}
#[test]
fn invalid_source_exceeds_target() {
StreamTest {
cases: vec![Test::single(attestation_data_builder(1, 0))
.expect_invalid_att(InvalidAttestation::SourceExceedsTarget)],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_unregistered_validator() {
StreamTest {
registered_validators: vec![],
cases: vec![
Test::single(attestation_data_builder(2, 3)).expect_result(Err(
NotSafe::UnregisteredValidator(pubkey(DEFAULT_VALIDATOR_INDEX)),
)),
],
}
.run()
}
#[test]
fn invalid_double_vote_diff_source() {
let first = attestation_data_builder(0, 2);
StreamTest {
cases: vec![
Test::single(first.clone()),
Test::single(attestation_data_builder(1, 2))
.expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_double_vote_diff_target() {
let first = attestation_data_builder(0, 2);
let mut second = attestation_data_builder(0, 2);
second.target.root = Hash256::random();
assert_ne!(first, second);
StreamTest {
cases: vec![
Test::single(first.clone()),
Test::single(second)
.expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_double_vote_diff_data() {
let first = attestation_data_builder(0, 2);
let mut second = attestation_data_builder(0, 2);
second.beacon_block_root = Hash256::random();
assert_ne!(first, second);
StreamTest {
cases: vec![
Test::single(first.clone()),
Test::single(second)
.expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_double_vote_diff_domain() {
let first = attestation_data_builder(0, 2);
let domain1 = Hash256::from_low_u64_le(1);
let domain2 = Hash256::from_low_u64_le(2);
StreamTest {
cases: vec![
Test::single(first.clone()).with_domain(domain1),
Test::single(first.clone())
.with_domain(domain2)
.expect_invalid_att(InvalidAttestation::DoubleVote(
SignedAttestation::from_attestation(&first, domain1),
)),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_double_vote_diff_source_multi() {
let first = attestation_data_builder(0, 2);
let second = attestation_data_builder(1, 3);
let third = attestation_data_builder(2, 4);
StreamTest {
cases: vec![
Test::single(first.clone()),
Test::single(second.clone()),
Test::single(third.clone()),
Test::single(attestation_data_builder(1, 2))
.expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))),
Test::single(attestation_data_builder(2, 3))
.expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&second))),
Test::single(attestation_data_builder(3, 4))
.expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&third))),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_surrounding_single() {
let first = attestation_data_builder(2, 3);
let second = attestation_data_builder(4, 5);
let third = attestation_data_builder(6, 7);
StreamTest {
cases: vec![
Test::single(first.clone()),
Test::single(second.clone()),
Test::single(third.clone()),
Test::single(attestation_data_builder(1, 4)).expect_invalid_att(
InvalidAttestation::NewSurroundsPrev {
prev: signed_att(&first),
},
),
Test::single(attestation_data_builder(3, 6)).expect_invalid_att(
InvalidAttestation::NewSurroundsPrev {
prev: signed_att(&second),
},
),
Test::single(attestation_data_builder(5, 8)).expect_invalid_att(
InvalidAttestation::NewSurroundsPrev {
prev: signed_att(&third),
},
),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_surrounding_from_first_source() {
let first = attestation_data_builder(2, 3);
let second = attestation_data_builder(3, 4);
StreamTest {
cases: vec![
Test::single(first.clone()),
Test::single(second.clone()),
Test::single(attestation_data_builder(2, 5)).expect_invalid_att(
InvalidAttestation::NewSurroundsPrev {
prev: signed_att(&second),
},
),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_surrounding_multiple_votes() {
let first = attestation_data_builder(0, 1);
let second = attestation_data_builder(1, 2);
let third = attestation_data_builder(2, 3);
StreamTest {
cases: vec![
Test::single(first.clone()),
Test::single(second.clone()),
Test::single(third.clone()),
Test::single(attestation_data_builder(0, 4)).expect_invalid_att(
InvalidAttestation::NewSurroundsPrev {
prev: signed_att(&third),
},
),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_prev_surrounds_new() {
let first = attestation_data_builder(0, 7);
StreamTest {
cases: vec![
Test::single(first.clone()),
Test::single(attestation_data_builder(1, 6)).expect_invalid_att(
InvalidAttestation::PrevSurroundsNew {
prev: signed_att(&first),
},
),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_prev_surrounds_new_multiple() {
let first = attestation_data_builder(0, 4);
let second = attestation_data_builder(1, 7);
let third = attestation_data_builder(8, 10);
StreamTest {
cases: vec![
Test::single(first.clone()),
Test::single(second.clone()),
Test::single(third.clone()),
Test::single(attestation_data_builder(9, 9)).expect_invalid_att(
InvalidAttestation::PrevSurroundsNew {
prev: signed_att(&third),
},
),
Test::single(attestation_data_builder(2, 6)).expect_invalid_att(
InvalidAttestation::PrevSurroundsNew {
prev: signed_att(&second),
},
),
Test::single(attestation_data_builder(1, 2)).expect_invalid_att(
InvalidAttestation::PrevSurroundsNew {
prev: signed_att(&first),
},
),
],
..StreamTest::default()
}
.run()
}

View File

@@ -0,0 +1,124 @@
#![cfg(test)]
use super::*;
use crate::test_utils::*;
use types::{BeaconBlockHeader, Hash256, Slot};
pub fn block(slot: u64) -> BeaconBlockHeader {
BeaconBlockHeader {
slot: Slot::new(slot),
proposer_index: 0,
parent_root: Hash256::random(),
state_root: Hash256::random(),
body_root: Hash256::random(),
}
}
#[test]
fn valid_empty_history() {
StreamTest {
cases: vec![Test::single(block(1))],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_blocks() {
StreamTest {
cases: vec![
Test::single(block(1)),
Test::single(block(2)),
Test::single(block(3)),
Test::single(block(4)),
],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_same_block() {
let block = block(100);
StreamTest {
cases: vec![
Test::single(block.clone()),
Test::single(block).expect_same_data(),
],
..StreamTest::default()
}
.run()
}
#[test]
fn valid_same_slot_different_validator() {
StreamTest {
registered_validators: vec![pubkey(0), pubkey(1)],
cases: vec![
Test::with_pubkey(pubkey(0), block(100)),
Test::with_pubkey(pubkey(1), block(100)),
],
}
.run()
}
#[test]
fn valid_same_block_different_validator() {
let block = block(100);
StreamTest {
registered_validators: vec![pubkey(0), pubkey(1)],
cases: vec![
Test::with_pubkey(pubkey(0), block.clone()),
Test::with_pubkey(pubkey(1), block.clone()),
],
}
.run()
}
#[test]
fn invalid_double_block_proposal() {
let first_block = block(1);
StreamTest {
cases: vec![
Test::single(first_block.clone()),
Test::single(block(1)).expect_invalid_block(InvalidBlock::DoubleBlockProposal(
SignedBlock::from_header(&first_block, DEFAULT_DOMAIN),
)),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_double_block_proposal_diff_domain() {
let first_block = block(1);
let domain1 = Hash256::from_low_u64_be(1);
let domain2 = Hash256::from_low_u64_be(2);
StreamTest {
cases: vec![
Test::single(first_block.clone()).with_domain(domain1),
Test::single(first_block.clone())
.with_domain(domain2)
.expect_invalid_block(InvalidBlock::DoubleBlockProposal(SignedBlock::from_header(
&first_block,
domain1,
))),
],
..StreamTest::default()
}
.run()
}
#[test]
fn invalid_unregistered_validator() {
StreamTest {
registered_validators: vec![],
cases: vec![
Test::single(block(0)).expect_result(Err(NotSafe::UnregisteredValidator(pubkey(
DEFAULT_VALIDATOR_INDEX,
)))),
],
}
.run()
}

View File

@@ -0,0 +1,77 @@
mod attestation_tests;
mod block_tests;
mod parallel_tests;
mod signed_attestation;
mod signed_block;
mod slashing_database;
mod test_utils;
pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation};
pub use crate::signed_block::{InvalidBlock, SignedBlock};
pub use crate::slashing_database::SlashingDatabase;
use rusqlite::Error as SQLError;
use std::io::{Error as IOError, ErrorKind};
use std::string::ToString;
use types::{Hash256, PublicKey};
/// The attestation or block is not safe to sign.
///
/// This could be because it's slashable, or because an error occurred.
#[derive(PartialEq, Debug)]
pub enum NotSafe {
UnregisteredValidator(PublicKey),
InvalidBlock(InvalidBlock),
InvalidAttestation(InvalidAttestation),
IOError(ErrorKind),
SQLError(String),
SQLPoolError(String),
}
/// The attestation or block is safe to sign, and will not cause the signer to be slashed.
#[derive(PartialEq, Debug)]
pub enum Safe {
/// Casting the exact same data (block or attestation) twice is never slashable.
SameData,
/// Incoming data is safe from slashing, and is not a duplicate.
Valid,
}
/// Safely parse a `Hash256` from the given `column` of an SQLite `row`.
fn hash256_from_row(column: usize, row: &rusqlite::Row) -> rusqlite::Result<Hash256> {
use rusqlite::{types::Type, Error};
let bytes: Vec<u8> = row.get(column)?;
if bytes.len() == 32 {
Ok(Hash256::from_slice(&bytes))
} else {
Err(Error::FromSqlConversionFailure(
column,
Type::Blob,
Box::from(format!("Invalid length for Hash256: {}", bytes.len())),
))
}
}
impl From<IOError> for NotSafe {
fn from(error: IOError) -> NotSafe {
NotSafe::IOError(error.kind())
}
}
impl From<SQLError> for NotSafe {
fn from(error: SQLError) -> NotSafe {
NotSafe::SQLError(error.to_string())
}
}
impl From<r2d2::Error> for NotSafe {
fn from(error: r2d2::Error) -> Self {
NotSafe::SQLPoolError(format!("{:?}", error))
}
}
impl ToString for NotSafe {
fn to_string(&self) -> String {
format!("{:?}", self)
}
}

View File

@@ -0,0 +1,82 @@
//! Tests that stress the concurrency safety of the slashing protection DB.
#![cfg(test)]
use crate::attestation_tests::attestation_data_builder;
use crate::block_tests::block;
use crate::test_utils::*;
use crate::*;
use rayon::prelude::*;
use tempfile::tempdir;
#[test]
fn block_same_slot() {
let dir = tempdir().unwrap();
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
let pk = pubkey(0);
slashing_db.register_validator(&pk).unwrap();
// A stream of blocks all with the same slot.
let num_blocks = 10;
let results = (0..num_blocks)
.into_par_iter()
.map(|_| slashing_db.check_and_insert_block_proposal(&pk, &block(1), DEFAULT_DOMAIN))
.collect::<Vec<_>>();
let num_successes = results.iter().filter(|res| res.is_ok()).count();
assert_eq!(num_successes, 1);
}
#[test]
fn attestation_same_target() {
let dir = tempdir().unwrap();
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
let pk = pubkey(0);
slashing_db.register_validator(&pk).unwrap();
// A stream of attestations all with the same target.
let num_attestations = 10;
let results = (0..num_attestations)
.into_par_iter()
.map(|i| {
slashing_db.check_and_insert_attestation(
&pk,
&attestation_data_builder(i, num_attestations),
DEFAULT_DOMAIN,
)
})
.collect::<Vec<_>>();
let num_successes = results.iter().filter(|res| res.is_ok()).count();
assert_eq!(num_successes, 1);
}
#[test]
fn attestation_surround_fest() {
let dir = tempdir().unwrap();
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
let pk = pubkey(0);
slashing_db.register_validator(&pk).unwrap();
// A stream of attestations that all surround each other.
let num_attestations = 10;
let results = (0..num_attestations)
.into_par_iter()
.map(|i| {
let att = attestation_data_builder(i, 2 * num_attestations - i);
slashing_db.check_and_insert_attestation(&pk, &att, DEFAULT_DOMAIN)
})
.collect::<Vec<_>>();
let num_successes = results.iter().filter(|res| res.is_ok()).count();
assert_eq!(num_successes, 1);
}

View File

@@ -0,0 +1,50 @@
use crate::hash256_from_row;
use types::{AttestationData, Epoch, Hash256, SignedRoot};
/// An attestation that has previously been signed.
#[derive(Clone, Debug, PartialEq)]
pub struct SignedAttestation {
pub source_epoch: Epoch,
pub target_epoch: Epoch,
pub signing_root: Hash256,
}
/// Reasons why an attestation may be slashable (or invalid).
#[derive(PartialEq, Debug)]
pub enum InvalidAttestation {
/// The attestation has the same target epoch as an attestation from the DB (enclosed).
DoubleVote(SignedAttestation),
/// The attestation surrounds an existing attestation from the database (`prev`).
NewSurroundsPrev { prev: SignedAttestation },
/// The attestation is surrounded by an existing attestation from the database (`prev`).
PrevSurroundsNew { prev: SignedAttestation },
/// The attestation is invalid because its source epoch is greater than its target epoch.
SourceExceedsTarget,
}
impl SignedAttestation {
pub fn new(source_epoch: Epoch, target_epoch: Epoch, signing_root: Hash256) -> Self {
Self {
source_epoch,
target_epoch,
signing_root,
}
}
/// Create a `SignedAttestation` from attestation data and a domain.
pub fn from_attestation(attestation: &AttestationData, domain: Hash256) -> Self {
Self {
source_epoch: attestation.source.epoch,
target_epoch: attestation.target.epoch,
signing_root: attestation.signing_root(domain),
}
}
/// Create a `SignedAttestation` from an SQLite row of `(source, target, signing_root)`.
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
let source = row.get(0)?;
let target = row.get(1)?;
let signing_root = hash256_from_row(2, row)?;
Ok(SignedAttestation::new(source, target, signing_root))
}
}

View File

@@ -0,0 +1,35 @@
use crate::hash256_from_row;
use types::{BeaconBlockHeader, Hash256, SignedRoot, Slot};
/// A block that has previously been signed.
#[derive(Clone, Debug, PartialEq)]
pub struct SignedBlock {
pub slot: Slot,
pub signing_root: Hash256,
}
/// Reasons why a block may be slashable.
#[derive(PartialEq, Debug)]
pub enum InvalidBlock {
DoubleBlockProposal(SignedBlock),
}
impl SignedBlock {
pub fn new(slot: Slot, signing_root: Hash256) -> Self {
Self { slot, signing_root }
}
pub fn from_header(header: &BeaconBlockHeader, domain: Hash256) -> Self {
Self {
slot: header.slot,
signing_root: header.signing_root(domain),
}
}
/// Parse an SQLite row of `(slot, signing_root)`.
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
let slot = row.get(0)?;
let signing_root = hash256_from_row(1, row)?;
Ok(SignedBlock { slot, signing_root })
}
}

View File

@@ -0,0 +1,471 @@
use crate::signed_attestation::InvalidAttestation;
use crate::signed_block::InvalidBlock;
use crate::{NotSafe, Safe, SignedAttestation, SignedBlock};
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{params, OptionalExtension, Transaction, TransactionBehavior};
use std::fs::{File, OpenOptions};
use std::path::Path;
use std::time::Duration;
use types::{AttestationData, BeaconBlockHeader, Hash256, PublicKey, SignedRoot};
type Pool = r2d2::Pool<SqliteConnectionManager>;
/// We set the pool size to 1 for compatibility with locking_mode=EXCLUSIVE.
///
/// This is perhaps overkill in the presence of exclusive transactions, but has
/// the added bonus of preventing other processes from trying to use our slashing database.
pub const POOL_SIZE: u32 = 1;
#[cfg(not(test))]
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(test)]
pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(100);
#[derive(Debug, Clone)]
pub struct SlashingDatabase {
conn_pool: Pool,
}
impl SlashingDatabase {
/// Open an existing database at the given `path`, or create one if none exists.
pub fn open_or_create(path: &Path) -> Result<Self, NotSafe> {
if path.exists() {
Self::open(path)
} else {
Self::create(path)
}
}
/// Create a slashing database at the given path.
///
/// Error if a database (or any file) already exists at `path`.
pub fn create(path: &Path) -> Result<Self, NotSafe> {
let file = OpenOptions::new()
.write(true)
.read(true)
.create_new(true)
.open(path)?;
Self::set_db_file_permissions(&file)?;
let conn_pool = Self::open_conn_pool(path)?;
let conn = conn_pool.get()?;
conn.execute(
"CREATE TABLE validators (
id INTEGER PRIMARY KEY,
public_key BLOB NOT NULL
)",
params![],
)?;
conn.execute(
"CREATE TABLE signed_blocks (
validator_id INTEGER NOT NULL,
slot INTEGER NOT NULL,
signing_root BLOB NOT NULL,
FOREIGN KEY(validator_id) REFERENCES validators(id)
UNIQUE (validator_id, slot)
)",
params![],
)?;
conn.execute(
"CREATE TABLE signed_attestations (
validator_id INTEGER,
source_epoch INTEGER NOT NULL,
target_epoch INTEGER NOT NULL,
signing_root BLOB NOT NULL,
FOREIGN KEY(validator_id) REFERENCES validators(id)
UNIQUE (validator_id, target_epoch)
)",
params![],
)?;
Ok(Self { conn_pool })
}
/// Open an existing `SlashingDatabase` from disk.
pub fn open(path: &Path) -> Result<Self, NotSafe> {
let conn_pool = Self::open_conn_pool(&path)?;
Ok(Self { conn_pool })
}
/// Open a new connection pool with all of the necessary settings and tweaks.
fn open_conn_pool(path: &Path) -> Result<Pool, NotSafe> {
let manager = SqliteConnectionManager::file(path)
.with_flags(rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)
.with_init(Self::apply_pragmas);
let conn_pool = Pool::builder()
.max_size(POOL_SIZE)
.connection_timeout(CONNECTION_TIMEOUT)
.build(manager)
.map_err(|e| NotSafe::SQLError(format!("Unable to open database: {:?}", e)))?;
Ok(conn_pool)
}
/// Apply the necessary settings to an SQLite connection.
///
/// Most importantly, put the database into exclusive locking mode, so that threads are forced
/// to serialise all DB access (to prevent slashable data being checked and signed in parallel).
/// The exclusive locking mode also has the benefit of applying to other processes, so multiple
/// Lighthouse processes trying to access the same database will also be blocked.
fn apply_pragmas(conn: &mut rusqlite::Connection) -> Result<(), rusqlite::Error> {
conn.pragma_update(None, "foreign_keys", &true)?;
conn.pragma_update(None, "locking_mode", &"EXCLUSIVE")?;
Ok(())
}
/// Set the database file to readable and writable only by its owner (0600).
#[cfg(unix)]
fn set_db_file_permissions(file: &File) -> Result<(), NotSafe> {
use std::os::unix::fs::PermissionsExt;
let mut perm = file.metadata()?.permissions();
perm.set_mode(0o600);
file.set_permissions(perm)?;
Ok(())
}
// TODO: add support for Windows ACLs
#[cfg(windows)]
fn set_db_file_permissions(file: &File) -> Result<(), NotSafe> {}
/// Register a validator with the slashing protection database.
///
/// This allows the validator to record their signatures in the database, and check
/// for slashings.
pub fn register_validator(&self, validator_pk: &PublicKey) -> Result<(), NotSafe> {
self.register_validators(std::iter::once(validator_pk))
}
/// Register multiple validators with the slashing protection database.
pub fn register_validators<'a>(
&self,
public_keys: impl Iterator<Item = &'a PublicKey>,
) -> Result<(), NotSafe> {
let mut conn = self.conn_pool.get()?;
let txn = conn.transaction()?;
{
let mut stmt = txn.prepare("INSERT INTO validators (public_key) VALUES (?1)")?;
for pubkey in public_keys {
stmt.execute(&[pubkey.as_hex_string()])?;
}
}
txn.commit()?;
Ok(())
}
/// Get the database-internal ID for a validator.
///
/// This is NOT the same as a validator index, and depends on the ordering that validators
/// are registered with the slashing protection database (and may vary between machines).
fn get_validator_id(txn: &Transaction, public_key: &PublicKey) -> Result<i64, NotSafe> {
txn.query_row(
"SELECT id FROM validators WHERE public_key = ?1",
params![&public_key.as_hex_string()],
|row| row.get(0),
)
.optional()?
.ok_or_else(|| NotSafe::UnregisteredValidator(public_key.clone()))
}
/// Check a block proposal from `validator_pubkey` for slash safety.
fn check_block_proposal(
&self,
txn: &Transaction,
validator_pubkey: &PublicKey,
block_header: &BeaconBlockHeader,
domain: Hash256,
) -> Result<Safe, NotSafe> {
let validator_id = Self::get_validator_id(txn, validator_pubkey)?;
let existing_block = txn
.prepare(
"SELECT slot, signing_root
FROM signed_blocks
WHERE validator_id = ?1 AND slot = ?2",
)?
.query_row(
params![validator_id, block_header.slot],
SignedBlock::from_row,
)
.optional()?;
if let Some(existing_block) = existing_block {
if existing_block.signing_root == block_header.signing_root(domain) {
// Same slot and same hash -> we're re-broadcasting a previously signed block
Ok(Safe::SameData)
} else {
// Same epoch but not the same hash -> it's a DoubleBlockProposal
Err(NotSafe::InvalidBlock(InvalidBlock::DoubleBlockProposal(
existing_block,
)))
}
} else {
Ok(Safe::Valid)
}
}
/// Check an attestation from `validator_pubkey` for slash safety.
fn check_attestation(
&self,
txn: &Transaction,
validator_pubkey: &PublicKey,
attestation: &AttestationData,
domain: Hash256,
) -> Result<Safe, NotSafe> {
let att_source_epoch = attestation.source.epoch;
let att_target_epoch = attestation.target.epoch;
// Although it's not required to avoid slashing, we disallow attestations
// which are obviously invalid by virtue of their source epoch exceeding their target.
if att_source_epoch > att_target_epoch {
return Err(NotSafe::InvalidAttestation(
InvalidAttestation::SourceExceedsTarget,
));
}
let validator_id = Self::get_validator_id(txn, validator_pubkey)?;
// 1. Check for a double vote. Namely, an existing attestation with the same target epoch,
// and a different signing root.
let same_target_att = txn
.prepare(
"SELECT source_epoch, target_epoch, signing_root
FROM signed_attestations
WHERE validator_id = ?1 AND target_epoch = ?2",
)?
.query_row(
params![validator_id, att_target_epoch],
SignedAttestation::from_row,
)
.optional()?;
if let Some(existing_attestation) = same_target_att {
// If the new attestation is identical to the existing attestation, then we already
// know that it is safe, and can return immediately.
if existing_attestation.signing_root == attestation.signing_root(domain) {
return Ok(Safe::SameData);
// Otherwise if the hashes are different, this is a double vote.
} else {
return Err(NotSafe::InvalidAttestation(InvalidAttestation::DoubleVote(
existing_attestation,
)));
}
}
// 2. Check that no previous vote is surrounding `attestation`.
// If there is a surrounding attestation, we only return the most recent one.
let surrounding_attestation = txn
.prepare(
"SELECT source_epoch, target_epoch, signing_root
FROM signed_attestations
WHERE validator_id = ?1 AND source_epoch < ?2 AND target_epoch > ?3
ORDER BY target_epoch DESC
LIMIT 1",
)?
.query_row(
params![validator_id, att_source_epoch, att_target_epoch],
SignedAttestation::from_row,
)
.optional()?;
if let Some(prev) = surrounding_attestation {
return Err(NotSafe::InvalidAttestation(
InvalidAttestation::PrevSurroundsNew { prev },
));
}
// 3. Check that no previous vote is surrounded by `attestation`.
// If there is a surrounded attestation, we only return the most recent one.
let surrounded_attestation = txn
.prepare(
"SELECT source_epoch, target_epoch, signing_root
FROM signed_attestations
WHERE validator_id = ?1 AND source_epoch > ?2 AND target_epoch < ?3
ORDER BY target_epoch DESC
LIMIT 1",
)?
.query_row(
params![validator_id, att_source_epoch, att_target_epoch],
SignedAttestation::from_row,
)
.optional()?;
if let Some(prev) = surrounded_attestation {
return Err(NotSafe::InvalidAttestation(
InvalidAttestation::NewSurroundsPrev { prev },
));
}
// Everything has been checked, return Valid
Ok(Safe::Valid)
}
/// Insert a block proposal into the slashing database.
///
/// This should *only* be called in the same (exclusive) transaction as `check_block_proposal`
/// so that the check isn't invalidated by a concurrent mutation.
fn insert_block_proposal(
&self,
txn: &Transaction,
validator_pubkey: &PublicKey,
block_header: &BeaconBlockHeader,
domain: Hash256,
) -> Result<(), NotSafe> {
let validator_id = Self::get_validator_id(txn, validator_pubkey)?;
txn.execute(
"INSERT INTO signed_blocks (validator_id, slot, signing_root)
VALUES (?1, ?2, ?3)",
params![
validator_id,
block_header.slot,
block_header.signing_root(domain).as_bytes()
],
)?;
Ok(())
}
/// Insert an attestation into the slashing database.
///
/// This should *only* be called in the same (exclusive) transaction as `check_attestation`
/// so that the check isn't invalidated by a concurrent mutation.
fn insert_attestation(
&self,
txn: &Transaction,
validator_pubkey: &PublicKey,
attestation: &AttestationData,
domain: Hash256,
) -> Result<(), NotSafe> {
let validator_id = Self::get_validator_id(txn, validator_pubkey)?;
txn.execute(
"INSERT INTO signed_attestations (validator_id, source_epoch, target_epoch, signing_root)
VALUES (?1, ?2, ?3, ?4)",
params![
validator_id,
attestation.source.epoch,
attestation.target.epoch,
attestation.signing_root(domain).as_bytes()
],
)?;
Ok(())
}
/// Check a block proposal for slash safety, and if it is safe, record it in the database.
///
/// The checking and inserting happen atomically and exclusively. We enforce exclusivity
/// to prevent concurrent checks and inserts from resulting in slashable data being inserted.
///
/// This is the safe, externally-callable interface for checking block proposals.
pub fn check_and_insert_block_proposal(
&self,
validator_pubkey: &PublicKey,
block_header: &BeaconBlockHeader,
domain: Hash256,
) -> Result<Safe, NotSafe> {
let mut conn = self.conn_pool.get()?;
let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
let safe = self.check_block_proposal(&txn, validator_pubkey, block_header, domain)?;
if safe != Safe::SameData {
self.insert_block_proposal(&txn, validator_pubkey, block_header, domain)?;
}
txn.commit()?;
Ok(safe)
}
/// Check an attestation for slash safety, and if it is safe, record it in the database.
///
/// The checking and inserting happen atomically and exclusively. We enforce exclusivity
/// to prevent concurrent checks and inserts from resulting in slashable data being inserted.
///
/// This is the safe, externally-callable interface for checking attestations.
pub fn check_and_insert_attestation(
&self,
validator_pubkey: &PublicKey,
attestation: &AttestationData,
domain: Hash256,
) -> Result<Safe, NotSafe> {
let mut conn = self.conn_pool.get()?;
let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
let safe = self.check_attestation(&txn, validator_pubkey, attestation, domain)?;
if safe != Safe::SameData {
self.insert_attestation(&txn, validator_pubkey, attestation, domain)?;
}
txn.commit()?;
Ok(safe)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::pubkey;
use tempfile::tempdir;
#[test]
fn open_non_existent_error() {
let dir = tempdir().unwrap();
let file = dir.path().join("db.sqlite");
assert!(SlashingDatabase::open(&file).is_err());
}
// Due to the exclusive locking, trying to use an already open database should error.
#[test]
fn double_open_error() {
let dir = tempdir().unwrap();
let file = dir.path().join("db.sqlite");
let _db1 = SlashingDatabase::create(&file).unwrap();
let db2 = SlashingDatabase::open(&file).unwrap();
db2.register_validator(&pubkey(0)).unwrap_err();
}
// Attempting to create the same database twice should error.
#[test]
fn double_create_error() {
let dir = tempdir().unwrap();
let file = dir.path().join("db.sqlite");
let _db1 = SlashingDatabase::create(&file).unwrap();
drop(_db1);
SlashingDatabase::create(&file).unwrap_err();
}
// Check that both `open` and `create` apply the same connection settings.
#[test]
fn connection_settings_applied() {
let dir = tempdir().unwrap();
let file = dir.path().join("db.sqlite");
let check = |db: &SlashingDatabase| {
assert_eq!(db.conn_pool.max_size(), POOL_SIZE);
assert_eq!(db.conn_pool.connection_timeout(), CONNECTION_TIMEOUT);
let conn = db.conn_pool.get().unwrap();
assert_eq!(
conn.pragma_query_value(None, "foreign_keys", |row| { row.get::<_, bool>(0) })
.unwrap(),
true
);
assert_eq!(
conn.pragma_query_value(None, "locking_mode", |row| { row.get::<_, String>(0) })
.unwrap()
.to_uppercase(),
"EXCLUSIVE"
);
};
let db1 = SlashingDatabase::create(&file).unwrap();
check(&db1);
drop(db1);
let db2 = SlashingDatabase::open(&file).unwrap();
check(&db2);
}
}

View File

@@ -0,0 +1,116 @@
#![cfg(test)]
use crate::*;
use tempfile::tempdir;
use types::{
test_utils::generate_deterministic_keypair, AttestationData, BeaconBlockHeader, Hash256,
};
pub const DEFAULT_VALIDATOR_INDEX: usize = 0;
pub const DEFAULT_DOMAIN: Hash256 = Hash256::zero();
pub fn pubkey(index: usize) -> PublicKey {
generate_deterministic_keypair(index).pk
}
pub struct Test<T> {
pubkey: PublicKey,
data: T,
domain: Hash256,
expected: Result<Safe, NotSafe>,
}
impl<T> Test<T> {
pub fn single(data: T) -> Self {
Self::with_pubkey(pubkey(DEFAULT_VALIDATOR_INDEX), data)
}
pub fn with_pubkey(pubkey: PublicKey, data: T) -> Self {
Self {
pubkey,
data,
domain: DEFAULT_DOMAIN,
expected: Ok(Safe::Valid),
}
}
pub fn with_domain(mut self, domain: Hash256) -> Self {
self.domain = domain;
self
}
pub fn expect_result(mut self, result: Result<Safe, NotSafe>) -> Self {
self.expected = result;
self
}
pub fn expect_invalid_att(self, error: InvalidAttestation) -> Self {
self.expect_result(Err(NotSafe::InvalidAttestation(error)))
}
pub fn expect_invalid_block(self, error: InvalidBlock) -> Self {
self.expect_result(Err(NotSafe::InvalidBlock(error)))
}
pub fn expect_same_data(self) -> Self {
self.expect_result(Ok(Safe::SameData))
}
}
pub struct StreamTest<T> {
/// Validators to register.
pub registered_validators: Vec<PublicKey>,
/// Vector of cases and the value expected when calling `check_and_insert_X`.
pub cases: Vec<Test<T>>,
}
impl<T> Default for StreamTest<T> {
fn default() -> Self {
Self {
registered_validators: vec![pubkey(DEFAULT_VALIDATOR_INDEX)],
cases: vec![],
}
}
}
impl StreamTest<AttestationData> {
pub fn run(&self) {
let dir = tempdir().unwrap();
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
for pubkey in &self.registered_validators {
slashing_db.register_validator(pubkey).unwrap();
}
for (i, test) in self.cases.iter().enumerate() {
assert_eq!(
slashing_db.check_and_insert_attestation(&test.pubkey, &test.data, test.domain),
test.expected,
"attestation {} not processed as expected",
i
);
}
}
}
impl StreamTest<BeaconBlockHeader> {
pub fn run(&self) {
let dir = tempdir().unwrap();
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
for pubkey in &self.registered_validators {
slashing_db.register_validator(pubkey).unwrap();
}
for (i, test) in self.cases.iter().enumerate() {
assert_eq!(
slashing_db.check_and_insert_block_proposal(&test.pubkey, &test.data, test.domain),
test.expected,
"attestation {} not processed as expected",
i
);
}
}
}

View File

@@ -320,6 +320,12 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> {
return Ok(None);
}
let current_epoch = self
.slot_clock
.now()
.ok_or_else(|| "Unable to determine current slot from clock".to_string())?
.epoch(E::slots_per_epoch());
let attestation = self
.beacon_node
.http
@@ -366,26 +372,14 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> {
let mut attestation = attestation.clone();
if self
.validator_store
self.validator_store
.sign_attestation(
duty.validator_pubkey(),
validator_committee_position,
&mut attestation,
current_epoch,
)
.is_none()
{
crit!(
log,
"Attestation signing refused";
"validator" => format!("{:?}", duty.validator_pubkey()),
"slot" => attestation.data.slot,
"index" => attestation.data.index,
);
None
} else {
Some(attestation)
}
.map(|_| attestation)
})
.collect::<Vec<_>>();

View File

@@ -210,6 +210,11 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
async fn publish_block(self, slot: Slot, validator_pubkey: PublicKey) -> Result<(), String> {
let log = &self.context.log;
let current_slot = self
.slot_clock
.now()
.ok_or_else(|| "Unable to determine current slot from clock".to_string())?;
let randao_reveal = self
.validator_store
.randao_reveal(&validator_pubkey, slot.epoch(E::slots_per_epoch()))
@@ -225,7 +230,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
let signed_block = self
.validator_store
.sign_block(&validator_pubkey, block)
.sign_block(&validator_pubkey, block, current_slot)
.ok_or_else(|| "Unable to sign block".to_string())?;
let publish_status = self

View File

@@ -17,9 +17,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.arg(
Arg::with_name("allow-unsynced")
.long("allow-unsynced")
.help("If present, the validator client will still poll for duties if the beacon
.help("If present, the validator client will still poll for duties if the beacon \
node is not synced.")
)
.arg(
Arg::with_name("auto-register")
.long("auto-register")
.help("If present, the validator client will register any new signing keys with \
the slashing protection database so that they may be used. WARNING: \
enabling the same signing key on multiple validator clients WILL lead to \
that validator getting slashed. Only use this flag the first time you run \
the validator client, or if you're certain there are no other \
nodes using the same key.")
)
/*
* The "testnet" sub-command.
*

View File

@@ -4,6 +4,8 @@ use std::path::PathBuf;
pub const DEFAULT_HTTP_SERVER: &str = "http://localhost:5052/";
pub const DEFAULT_DATA_DIR: &str = ".lighthouse/validators";
/// Path to the slashing protection database within the datadir.
pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite";
/// Specifies a method for obtaining validator keypairs.
#[derive(Clone)]
@@ -35,19 +37,22 @@ pub struct Config {
/// If true, the validator client will still poll for duties and produce blocks even if the
/// beacon node is not synced at startup.
pub allow_unsynced_beacon_node: bool,
/// If true, register new validator keys with the slashing protection database.
pub auto_register: bool,
}
impl Default for Config {
/// Build a new configuration from defaults.
fn default() -> Self {
let mut data_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
data_dir.push(".lighthouse");
data_dir.push("validators");
let data_dir = dirs::home_dir()
.map(|home| home.join(DEFAULT_DATA_DIR))
.unwrap_or_else(|| PathBuf::from("."));
Self {
data_dir,
key_source: <_>::default(),
http_server: DEFAULT_HTTP_SERVER.to_string(),
allow_unsynced_beacon_node: false,
auto_register: false,
}
}
}
@@ -93,6 +98,7 @@ impl Config {
};
config.allow_unsynced_beacon_node = cli_args.is_present("allow-unsynced");
config.auto_register = cli_args.is_present("auto-register");
Ok(config)
}

View File

@@ -128,6 +128,7 @@ impl TryInto<DutyAndProof> for ValidatorDutyBytes {
}
/// The outcome of inserting some `ValidatorDuty` into the `DutiesStore`.
#[derive(PartialEq, Debug, Clone)]
enum InsertOutcome {
/// These are the first duties received for this validator.
NewValidator,
@@ -426,8 +427,6 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> {
/// Returns the pubkeys of the validators which are assigned to propose in the given slot.
///
/// In normal cases, there should be 0 or 1 validators returned. In extreme cases (i.e., deep forking)
///
/// It is possible that multiple validators have an identical proposal slot, however that is
/// likely the result of heavy forking (lol) or inconsistent beacon node connections.
pub fn block_producers(&self, slot: Slot) -> Vec<PublicKey> {

View File

@@ -188,6 +188,12 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
"voting_validators" => validator_store.num_voting_validators()
);
if config.auto_register {
info!(log, "Registering all validators for slashing protection");
validator_store.register_all_validators_for_slashing_protection()?;
info!(log, "Validator auto-registration complete");
}
let duties_service = DutiesServiceBuilder::new()
.slot_clock(slot_clock.clone())
.validator_store(validator_store.clone())

View File

@@ -35,7 +35,7 @@ fn dir_name(voting_pubkey: &PublicKey) -> String {
/// Represents the files/objects for each dedicated lighthouse validator directory.
///
/// Generally lives in `~/.lighthouse/validators/`.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub struct ValidatorDirectory {
pub directory: PathBuf,
pub voting_keypair: Option<Keypair>,
@@ -147,11 +147,13 @@ pub struct ValidatorDirectoryBuilder {
}
impl ValidatorDirectoryBuilder {
/// Set the specification for this validator.
pub fn spec(mut self, spec: ChainSpec) -> Self {
self.spec = Some(spec);
self
}
/// Use the `MAX_EFFECTIVE_BALANCE` as this validator's deposit.
pub fn full_deposit_amount(mut self) -> Result<Self, String> {
let spec = self
.spec
@@ -161,17 +163,24 @@ impl ValidatorDirectoryBuilder {
Ok(self)
}
/// Use a validator deposit of `gwei`.
pub fn custom_deposit_amount(mut self, gwei: u64) -> Self {
self.amount = Some(gwei);
self
}
/// Generate keypairs using `Keypair::random()`.
pub fn thread_random_keypairs(mut self) -> Self {
self.voting_keypair = Some(Keypair::random());
self.withdrawal_keypair = Some(Keypair::random());
self
}
/// Generate insecure, deterministic keypairs.
///
///
/// ## Warning
/// Only for use in testing. Do not store value in these keys.
pub fn insecure_keypairs(mut self, index: usize) -> Self {
let keypair = generate_deterministic_keypair(index);
self.voting_keypair = Some(keypair.clone());
@@ -203,6 +212,7 @@ impl ValidatorDirectoryBuilder {
Ok(self)
}
/// Write the validators keypairs to disk.
pub fn write_keypair_files(self) -> Result<Self, String> {
let voting_keypair = self
.voting_keypair
@@ -285,7 +295,7 @@ impl ValidatorDirectoryBuilder {
.directory
.as_ref()
.map(|directory| directory.join(ETH1_DEPOSIT_DATA_FILE))
.ok_or_else(|| "write_eth1_data_filer requires a directory")?;
.ok_or_else(|| "write_eth1_data_file requires a directory")?;
let (deposit_data, _) = self.get_deposit_data()?;
@@ -328,8 +338,10 @@ impl ValidatorDirectoryBuilder {
}
pub fn build(self) -> Result<ValidatorDirectory, String> {
let directory = self.directory.ok_or_else(|| "build requires a directory")?;
Ok(ValidatorDirectory {
directory: self.directory.ok_or_else(|| "build requires a directory")?,
directory,
voting_keypair: self.voting_keypair,
withdrawal_keypair: self.withdrawal_keypair,
deposit_data: self.deposit_data,

View File

@@ -1,8 +1,10 @@
use crate::config::SLASHING_PROTECTION_FILENAME;
use crate::fork_service::ForkService;
use crate::validator_directory::{ValidatorDirectory, ValidatorDirectoryBuilder};
use parking_lot::RwLock;
use rayon::prelude::*;
use slog::{error, Logger};
use slashing_protection::{NotSafe, Safe, SlashingDatabase};
use slog::{crit, error, warn, Logger};
use slot_clock::SlotClock;
use std::collections::HashMap;
use std::fs::read_dir;
@@ -19,6 +21,7 @@ use types::{
#[derive(Clone)]
pub struct ValidatorStore<T, E: EthSpec> {
validators: Arc<RwLock<HashMap<PublicKey, ValidatorDirectory>>>,
slashing_protection: SlashingDatabase,
genesis_validators_root: Hash256,
spec: Arc<ChainSpec>,
log: Logger,
@@ -35,6 +38,15 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
fork_service: ForkService<T, E>,
log: Logger,
) -> Result<Self, String> {
let slashing_db_path = base_dir.join(SLASHING_PROTECTION_FILENAME);
let slashing_protection =
SlashingDatabase::open_or_create(&slashing_db_path).map_err(|e| {
format!(
"Failed to open or create slashing protection database: {:?}",
e
)
})?;
let validator_key_values = read_dir(&base_dir)
.map_err(|e| format!("Failed to read base directory {:?}: {:?}", base_dir, e))?
.collect::<Vec<_>>()
@@ -68,6 +80,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
Ok(Self {
validators: Arc::new(RwLock::new(HashMap::from_par_iter(validator_key_values))),
slashing_protection,
genesis_validators_root,
spec: Arc::new(spec),
log,
@@ -88,6 +101,10 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
.map_err(|e| format!("Unable to create temp dir: {:?}", e))?;
let data_dir = PathBuf::from(temp_dir.path());
let slashing_db_path = data_dir.join(SLASHING_PROTECTION_FILENAME);
let slashing_protection = SlashingDatabase::create(&slashing_db_path)
.map_err(|e| format!("Failed to create slashing protection database: {:?}", e))?;
let validators = validator_indices
.par_iter()
.map(|index| {
@@ -111,6 +128,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
Ok(Self {
validators: Arc::new(RwLock::new(HashMap::from_iter(validators))),
slashing_protection,
genesis_validators_root,
spec: Arc::new(spec),
log,
@@ -120,6 +138,16 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
})
}
/// Register all known validators with the slashing protection database.
///
/// Registration is required to protect against a lost or missing slashing database,
/// such as when relocating validator keys to a new machine.
pub fn register_all_validators_for_slashing_protection(&self) -> Result<(), String> {
self.slashing_protection
.register_validators(self.validators.read().keys())
.map_err(|e| format!("Error while registering validators: {:?}", e))
}
pub fn voting_pubkeys(&self) -> Vec<PublicKey> {
self.validators
.read()
@@ -165,20 +193,73 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
&self,
validator_pubkey: &PublicKey,
block: BeaconBlock<E>,
current_slot: Slot,
) -> Option<SignedBeaconBlock<E>> {
// TODO: check for slashing.
self.validators
.read()
.get(validator_pubkey)
.and_then(|validator_dir| {
let voting_keypair = validator_dir.voting_keypair.as_ref()?;
// Make sure the block slot is not higher than the current slot to avoid potential attacks.
if block.slot > current_slot {
warn!(
self.log,
"Not signing block with slot greater than current slot";
"block_slot" => block.slot.as_u64(),
"current_slot" => current_slot.as_u64()
);
return None;
}
// Check for slashing conditions.
let fork = self.fork()?;
let domain = self.spec.get_domain(
block.epoch(),
Domain::BeaconProposer,
&fork,
self.genesis_validators_root,
);
let slashing_status = self.slashing_protection.check_and_insert_block_proposal(
validator_pubkey,
&block.block_header(),
domain,
);
match slashing_status {
// We can safely sign this block.
Ok(Safe::Valid) => {
let validators = self.validators.read();
let validator = validators.get(validator_pubkey)?;
let voting_keypair = validator.voting_keypair.as_ref()?;
Some(block.sign(
&voting_keypair.sk,
&self.fork()?,
&fork,
self.genesis_validators_root,
&self.spec,
))
})
}
Ok(Safe::SameData) => {
warn!(
self.log,
"Skipping signing of previously signed block";
);
None
}
Err(NotSafe::UnregisteredValidator(pk)) => {
warn!(
self.log,
"Not signing block for unregistered validator";
"msg" => "Carefully consider running with --auto-register (see --help)",
"public_key" => format!("{:?}", pk)
);
None
}
Err(e) => {
crit!(
self.log,
"Not signing slashable block";
"error" => format!("{:?}", e)
);
None
}
}
}
pub fn sign_attestation(
@@ -186,19 +267,40 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
validator_pubkey: &PublicKey,
validator_committee_position: usize,
attestation: &mut Attestation<E>,
current_epoch: Epoch,
) -> Option<()> {
// TODO: check for slashing.
self.validators
.read()
.get(validator_pubkey)
.and_then(|validator_dir| {
let voting_keypair = validator_dir.voting_keypair.as_ref()?;
// Make sure the target epoch is not higher than the current epoch to avoid potential attacks.
if attestation.data.target.epoch > current_epoch {
return None;
}
// Checking for slashing conditions.
let fork = self.fork()?;
let domain = self.spec.get_domain(
attestation.data.target.epoch,
Domain::BeaconAttester,
&fork,
self.genesis_validators_root,
);
let slashing_status = self.slashing_protection.check_and_insert_attestation(
validator_pubkey,
&attestation.data,
domain,
);
match slashing_status {
// We can safely sign this attestation.
Ok(Safe::Valid) => {
let validators = self.validators.read();
let validator = validators.get(validator_pubkey)?;
let voting_keypair = validator.voting_keypair.as_ref()?;
attestation
.sign(
&voting_keypair.sk,
validator_committee_position,
&self.fork()?,
&fork,
self.genesis_validators_root,
&self.spec,
)
@@ -212,7 +314,33 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
.ok()?;
Some(())
})
}
Ok(Safe::SameData) => {
warn!(
self.log,
"Skipping signing of previously signed attestation"
);
None
}
Err(NotSafe::UnregisteredValidator(pk)) => {
warn!(
self.log,
"Not signing attestation for unregistered validator";
"msg" => "Carefully consider running with --auto-register (see --help)",
"public_key" => format!("{:?}", pk)
);
None
}
Err(e) => {
crit!(
self.log,
"Not signing slashable attestation";
"attestation" => format!("{:?}", attestation.data),
"error" => format!("{:?}", e)
);
None
}
}
}
/// Signs an `AggregateAndProof` for a given validator.