Implement slasher (#1567)

This is an implementation of a slasher that lives inside the BN and can be enabled via `lighthouse bn --slasher`.

Features included in this PR:

- [x] Detection of attester slashing conditions (double votes, surrounds existing, surrounded by existing)
- [x] Integration into Lighthouse's attestation verification flow
- [x] Detection of proposer slashing conditions
- [x] Extraction of attestations from blocks as they are verified
- [x] Compression of chunks
- [x] Configurable history length
- [x] Pruning of old attestations and blocks
- [x] More tests

Future work:

* Focus on a slice of history separate from the most recent N epochs (e.g. epochs `current - K` to `current - M`)
* Run out-of-process
* Ingest attestations from the chain without a resync

Design notes are here https://hackmd.io/@sproul/HJSEklmPL
This commit is contained in:
Michael Sproul
2020-11-23 03:43:22 +00:00
parent 59b2247ab8
commit 5828ff1204
44 changed files with 3662 additions and 87 deletions
+1
View File
@@ -43,3 +43,4 @@ clap_utils = { path = "../common/clap_utils" }
hyper = "0.13.8"
lighthouse_version = { path = "../common/lighthouse_version" }
hex = "0.4.2"
slasher = { path = "../slasher" }
+1
View File
@@ -61,4 +61,5 @@ derivative = "2.1.1"
itertools = "0.9.0"
regex = "1.3.9"
exit-future = "0.2.0"
slasher = { path = "../../slasher" }
eth2 = { path = "../../common/eth2" }
@@ -37,6 +37,7 @@ use crate::{
};
use bls::verify_signature_sets;
use proto_array::Block as ProtoBlock;
use slog::debug;
use slot_clock::SlotClock;
use state_processing::{
common::get_indexed_attestation,
@@ -297,6 +298,76 @@ impl<T: BeaconChainTypes> SignatureVerifiedAttestation<T> for VerifiedUnaggregat
}
}
/// Information about invalid attestations which might still be slashable despite being invalid.
pub enum AttestationSlashInfo<T: BeaconChainTypes, TErr> {
/// The attestation is invalid, but its signature wasn't checked.
SignatureNotChecked(Attestation<T::EthSpec>, TErr),
/// As for `SignatureNotChecked`, but we know the `IndexedAttestation`.
SignatureNotCheckedIndexed(IndexedAttestation<T::EthSpec>, TErr),
/// The attestation's signature is invalid, so it will never be slashable.
SignatureInvalid(TErr),
/// The signature is valid but the attestation is invalid in some other way.
SignatureValid(IndexedAttestation<T::EthSpec>, TErr),
}
/// After processing an attestation normally, optionally process it further for the slasher.
///
/// This maps an `AttestationSlashInfo` error back into a regular `Error`, performing signature
/// checks on attestations that failed verification for other reasons.
///
/// No substantial extra work will be done if there is no slasher configured.
fn process_slash_info<T: BeaconChainTypes>(
slash_info: AttestationSlashInfo<T, Error>,
chain: &BeaconChain<T>,
) -> Error {
use AttestationSlashInfo::*;
if let Some(slasher) = chain.slasher.as_ref() {
let (indexed_attestation, check_signature, err) = match slash_info {
SignatureNotChecked(attestation, err) => {
match obtain_indexed_attestation_and_committees_per_slot(chain, &attestation) {
Ok((indexed, _)) => (indexed, true, err),
Err(e) => {
debug!(
chain.log,
"Unable to obtain indexed form of attestation for slasher";
"attestation_root" => format!("{:?}", attestation.tree_hash_root()),
"error" => format!("{:?}", e)
);
return err;
}
}
}
SignatureNotCheckedIndexed(indexed, err) => (indexed, true, err),
SignatureInvalid(e) => return e,
SignatureValid(indexed, err) => (indexed, false, err),
};
if check_signature {
if let Err(e) = verify_attestation_signature(chain, &indexed_attestation) {
debug!(
chain.log,
"Signature verification for slasher failed";
"error" => format!("{:?}", e),
);
return err;
}
}
// Supply to slasher.
slasher.accept_attestation(indexed_attestation);
err
} else {
match slash_info {
SignatureNotChecked(_, e)
| SignatureNotCheckedIndexed(_, e)
| SignatureInvalid(e)
| SignatureValid(_, e) => e,
}
}
}
impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> {
/// Returns `Ok(Self)` if the `signed_aggregate` is valid to be (re)published on the gossip
/// network.
@@ -304,6 +375,21 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> {
signed_aggregate: SignedAggregateAndProof<T::EthSpec>,
chain: &BeaconChain<T>,
) -> Result<Self, Error> {
Self::verify_slashable(signed_aggregate, chain)
.map(|verified_aggregate| {
if let Some(slasher) = chain.slasher.as_ref() {
slasher.accept_attestation(verified_aggregate.indexed_attestation.clone());
}
verified_aggregate
})
.map_err(|slash_info| process_slash_info(slash_info, chain))
}
/// Run the checks that happen before an indexed attestation is constructed.
fn verify_early_checks(
signed_aggregate: &SignedAggregateAndProof<T::EthSpec>,
chain: &BeaconChain<T>,
) -> Result<Hash256, Error> {
let attestation = &signed_aggregate.message.aggregate;
// Ensure attestation is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a
@@ -364,37 +450,20 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> {
// Ensure that the attestation has participants.
if attestation.aggregation_bits.is_zero() {
return Err(Error::EmptyAggregationBitfield);
Err(Error::EmptyAggregationBitfield)
} else {
Ok(attestation_root)
}
}
let indexed_attestation =
map_attestation_committee(chain, attestation, |(committee, _)| {
// Note: this clones the signature which is known to be a relatively slow operation.
//
// Future optimizations should remove this clone.
let selection_proof =
SelectionProof::from(signed_aggregate.message.selection_proof.clone());
if !selection_proof
.is_aggregator(committee.committee.len(), &chain.spec)
.map_err(|e| Error::BeaconChainError(e.into()))?
{
return Err(Error::InvalidSelectionProof { aggregator_index });
}
// Ensure the aggregator is a member of the committee for which it is aggregating.
if !committee.committee.contains(&(aggregator_index as usize)) {
return Err(Error::AggregatorNotInCommittee { aggregator_index });
}
get_indexed_attestation(committee.committee, &attestation)
.map_err(|e| BeaconChainError::from(e).into())
})?;
// Ensure that all signatures are valid.
if !verify_signed_aggregate_signatures(chain, &signed_aggregate, &indexed_attestation)? {
return Err(Error::InvalidSignature);
}
/// Run the checks that happen after the indexed attestation and signature have been checked.
fn verify_late_checks(
signed_aggregate: &SignedAggregateAndProof<T::EthSpec>,
attestation_root: Hash256,
chain: &BeaconChain<T>,
) -> Result<(), Error> {
let attestation = &signed_aggregate.message.aggregate;
let aggregator_index = signed_aggregate.message.aggregator_index;
// Observe the valid attestation so we do not re-process it.
//
@@ -425,6 +494,68 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> {
});
}
Ok(())
}
/// Verify the attestation, producing extra information about whether it might be slashable.
pub fn verify_slashable(
signed_aggregate: SignedAggregateAndProof<T::EthSpec>,
chain: &BeaconChain<T>,
) -> Result<Self, AttestationSlashInfo<T, Error>> {
use AttestationSlashInfo::*;
let attestation = &signed_aggregate.message.aggregate;
let aggregator_index = signed_aggregate.message.aggregator_index;
let attestation_root = match Self::verify_early_checks(&signed_aggregate, chain) {
Ok(root) => root,
Err(e) => return Err(SignatureNotChecked(signed_aggregate.message.aggregate, e)),
};
let indexed_attestation =
match map_attestation_committee(chain, attestation, |(committee, _)| {
// Note: this clones the signature which is known to be a relatively slow operation.
//
// Future optimizations should remove this clone.
let selection_proof =
SelectionProof::from(signed_aggregate.message.selection_proof.clone());
if !selection_proof
.is_aggregator(committee.committee.len(), &chain.spec)
.map_err(|e| Error::BeaconChainError(e.into()))?
{
return Err(Error::InvalidSelectionProof { aggregator_index });
}
// Ensure the aggregator is a member of the committee for which it is aggregating.
if !committee.committee.contains(&(aggregator_index as usize)) {
return Err(Error::AggregatorNotInCommittee { aggregator_index });
}
get_indexed_attestation(committee.committee, attestation)
.map_err(|e| BeaconChainError::from(e).into())
}) {
Ok(indexed_attestation) => indexed_attestation,
Err(e) => return Err(SignatureNotChecked(signed_aggregate.message.aggregate, e)),
};
// Ensure that all signatures are valid.
if let Err(e) =
verify_signed_aggregate_signatures(chain, &signed_aggregate, &indexed_attestation)
.and_then(|is_valid| {
if !is_valid {
Err(Error::InvalidSignature)
} else {
Ok(())
}
})
{
return Err(SignatureInvalid(e));
}
if let Err(e) = Self::verify_late_checks(&signed_aggregate, attestation_root, chain) {
return Err(SignatureValid(indexed_attestation, e));
}
Ok(VerifiedAggregatedAttestation {
signed_aggregate,
indexed_attestation,
@@ -448,16 +579,11 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> {
}
impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> {
/// Returns `Ok(Self)` if the `attestation` is valid to be (re)published on the gossip
/// network.
///
/// `subnet_id` is the subnet from which we received this attestation. This function will
/// verify that it was received on the correct subnet.
pub fn verify(
attestation: Attestation<T::EthSpec>,
subnet_id: Option<SubnetId>,
/// Run the checks that happen before an indexed attestation is constructed.
pub fn verify_early_checks(
attestation: &Attestation<T::EthSpec>,
chain: &BeaconChain<T>,
) -> Result<Self, Error> {
) -> Result<(), Error> {
let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch());
// Check the attestation's epoch matches its target.
@@ -491,9 +617,17 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> {
// Check the attestation target root is consistent with the head root.
verify_attestation_target_root::<T::EthSpec>(&head_block, &attestation)?;
let (indexed_attestation, committees_per_slot) =
obtain_indexed_attestation_and_committees_per_slot(chain, &attestation)?;
Ok(())
}
/// Run the checks that apply to the indexed attestation before the signature is checked.
pub fn verify_middle_checks(
attestation: &Attestation<T::EthSpec>,
indexed_attestation: &IndexedAttestation<T::EthSpec>,
committees_per_slot: u64,
subnet_id: Option<SubnetId>,
chain: &BeaconChain<T>,
) -> Result<(u64, SubnetId), Error> {
let expected_subnet_id = SubnetId::compute_subnet_for_attestation_data::<T::EthSpec>(
&indexed_attestation.data,
committees_per_slot,
@@ -532,9 +666,15 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> {
});
}
// The aggregate signature of the attestation is valid.
verify_attestation_signature(chain, &indexed_attestation)?;
Ok((validator_index, expected_subnet_id))
}
/// Run the checks that apply after the signature has been checked.
fn verify_late_checks(
attestation: &Attestation<T::EthSpec>,
validator_index: u64,
chain: &BeaconChain<T>,
) -> Result<(), Error> {
// Now that the attestation has been fully verified, store that we have received a valid
// attestation from this validator.
//
@@ -552,6 +692,68 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> {
epoch: attestation.data.target.epoch,
});
}
Ok(())
}
/// Returns `Ok(Self)` if the `attestation` is valid to be (re)published on the gossip
/// network.
///
/// `subnet_id` is the subnet from which we received this attestation. This function will
/// verify that it was received on the correct subnet.
pub fn verify(
attestation: Attestation<T::EthSpec>,
subnet_id: Option<SubnetId>,
chain: &BeaconChain<T>,
) -> Result<Self, Error> {
Self::verify_slashable(attestation, subnet_id, chain)
.map(|verified_unaggregated| {
if let Some(slasher) = chain.slasher.as_ref() {
slasher.accept_attestation(verified_unaggregated.indexed_attestation.clone());
}
verified_unaggregated
})
.map_err(|slash_info| process_slash_info(slash_info, chain))
}
/// Verify the attestation, producing extra information about whether it might be slashable.
pub fn verify_slashable(
attestation: Attestation<T::EthSpec>,
subnet_id: Option<SubnetId>,
chain: &BeaconChain<T>,
) -> Result<Self, AttestationSlashInfo<T, Error>> {
use AttestationSlashInfo::*;
if let Err(e) = Self::verify_early_checks(&attestation, chain) {
return Err(SignatureNotChecked(attestation, e));
}
let (indexed_attestation, committees_per_slot) =
match obtain_indexed_attestation_and_committees_per_slot(chain, &attestation) {
Ok(x) => x,
Err(e) => {
return Err(SignatureNotChecked(attestation, e));
}
};
let (validator_index, expected_subnet_id) = match Self::verify_middle_checks(
&attestation,
&indexed_attestation,
committees_per_slot,
subnet_id,
chain,
) {
Ok(t) => t,
Err(e) => return Err(SignatureNotCheckedIndexed(indexed_attestation, e)),
};
// The aggregate signature of the attestation is valid.
if let Err(e) = verify_attestation_signature(chain, &indexed_attestation) {
return Err(SignatureInvalid(e));
}
if let Err(e) = Self::verify_late_checks(&attestation, validator_index, chain) {
return Err(SignatureValid(indexed_attestation, e));
}
Ok(Self {
attestation,
+82 -4
View File
@@ -32,12 +32,13 @@ use futures::channel::mpsc::Sender;
use itertools::process_results;
use operation_pool::{OperationPool, PersistedOperationPool};
use parking_lot::{Mutex, RwLock};
use slasher::Slasher;
use slog::{crit, debug, error, info, trace, warn, Logger};
use slot_clock::SlotClock;
use state_processing::{
common::get_indexed_attestation, per_block_processing,
per_block_processing::errors::AttestationValidationError, per_slot_processing,
BlockSignatureStrategy, SigVerifiedOp,
BlockSignatureStrategy, SigVerifiedOp, VerifyOperation,
};
use std::borrow::Cow;
use std::cmp::Ordering;
@@ -232,6 +233,8 @@ pub struct BeaconChain<T: BeaconChainTypes> {
pub(crate) log: Logger,
/// Arbitrary bytes included in the blocks.
pub(crate) graffiti: Graffiti,
/// Optional slasher.
pub(crate) slasher: Option<Arc<Slasher<T::EthSpec>>>,
}
type BeaconBlockAndState<T> = (BeaconBlock<T>, BeaconState<T>);
@@ -518,10 +521,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
/// Apply a function to the canonical head without cloning it.
pub fn with_head<U>(
pub fn with_head<U, E>(
&self,
f: impl FnOnce(&BeaconSnapshot<T::EthSpec>) -> Result<U, Error>,
) -> Result<U, Error> {
f: impl FnOnce(&BeaconSnapshot<T::EthSpec>) -> Result<U, E>,
) -> Result<U, E>
where
E: From<Error>,
{
let head_lock = self
.canonical_head
.try_read_for(HEAD_LOCK_TIMEOUT)
@@ -1080,6 +1086,63 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
Ok(signed_aggregate)
}
/// Move slashings collected by the slasher into the op pool for block inclusion.
fn ingest_slashings_to_op_pool(&self, state: &BeaconState<T::EthSpec>) {
if let Some(slasher) = self.slasher.as_ref() {
let attester_slashings = slasher.get_attester_slashings();
let proposer_slashings = slasher.get_proposer_slashings();
if !attester_slashings.is_empty() || !proposer_slashings.is_empty() {
debug!(
self.log,
"Ingesting slashings";
"num_attester_slashings" => attester_slashings.len(),
"num_proposer_slashings" => proposer_slashings.len(),
);
}
for slashing in attester_slashings {
let verified_slashing = match slashing.clone().validate(state, &self.spec) {
Ok(verified) => verified,
Err(e) => {
error!(
self.log,
"Attester slashing from slasher failed verification";
"error" => format!("{:?}", e),
"slashing" => format!("{:?}", slashing),
);
continue;
}
};
if let Err(e) = self.import_attester_slashing(verified_slashing) {
error!(
self.log,
"Attester slashing from slasher is invalid";
"error" => format!("{:?}", e),
"slashing" => format!("{:?}", slashing),
);
}
}
for slashing in proposer_slashings {
let verified_slashing = match slashing.clone().validate(state, &self.spec) {
Ok(verified) => verified,
Err(e) => {
error!(
self.log,
"Proposer slashing from slasher failed verification";
"error" => format!("{:?}", e),
"slashing" => format!("{:?}", slashing),
);
continue;
}
};
self.import_proposer_slashing(verified_slashing);
}
}
}
/// Check that the shuffling at `block_root` is equal to one of the shufflings of `state`.
///
/// The `target_epoch` argument determines which shuffling to check compatibility with, it
@@ -1525,6 +1588,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
metrics::stop_timer(attestation_observation_timer);
// If a slasher is configured, provide the attestations from the block.
if let Some(slasher) = self.slasher.as_ref() {
for attestation in &signed_block.message.body.attestations {
let committee =
state.get_beacon_committee(attestation.data.slot, attestation.data.index)?;
let indexed_attestation =
get_indexed_attestation(&committee.committee, attestation)
.map_err(|e| BlockError::BeaconChainError(e.into()))?;
slasher.accept_attestation(indexed_attestation);
}
}
// If there are new validators in this block, update our pubkey cache.
//
// We perform this _before_ adding the block to fork choice because the pubkey cache is
@@ -1735,6 +1810,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
state.latest_block_header.canonical_root()
};
self.ingest_slashings_to_op_pool(&state);
let (proposer_slashings, attester_slashings) =
self.op_pool.get_slashings(&state, &self.spec);
@@ -1951,6 +2027,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
{
self.persist_head_and_fork_choice()?;
self.op_pool.prune_attestations(self.epoch()?);
self.ingest_slashings_to_op_pool(&new_head.beacon_state);
self.persist_op_pool()?;
}
let update_head_timer = metrics::start_timer(&metrics::UPDATE_HEAD_TIMES);
@@ -67,7 +67,7 @@ use store::{Error as DBError, HotColdDB, HotStateSummary, KeyValueStore, StoreOp
use tree_hash::TreeHash;
use types::{
BeaconBlock, BeaconState, BeaconStateError, ChainSpec, CloneConfig, EthSpec, Hash256,
PublicKey, RelativeEpoch, SignedBeaconBlock, Slot,
PublicKey, RelativeEpoch, SignedBeaconBlock, SignedBeaconBlockHeader, Slot,
};
/// Maximum block slot number. Block with slots bigger than this constant will NOT be processed.
@@ -268,6 +268,27 @@ impl<T: EthSpec> From<DBError> for BlockError<T> {
}
}
/// Information about invalid blocks which might still be slashable despite being invalid.
pub enum BlockSlashInfo<TErr> {
/// The block is invalid, but its proposer signature wasn't checked.
SignatureNotChecked(SignedBeaconBlockHeader, TErr),
/// The block's proposer signature is invalid, so it will never be slashable.
SignatureInvalid(TErr),
/// The signature is valid but the attestation is invalid in some other way.
SignatureValid(SignedBeaconBlockHeader, TErr),
}
impl<E: EthSpec> BlockSlashInfo<BlockError<E>> {
pub fn from_early_error(header: SignedBeaconBlockHeader, e: BlockError<E>) -> Self {
match e {
BlockError::ProposalSignatureInvalid => BlockSlashInfo::SignatureInvalid(e),
// `InvalidSignature` could indicate any signature in the block, so we want
// to recheck the proposer signature alone.
_ => BlockSlashInfo::SignatureNotChecked(header, e),
}
}
}
/// Verify all signatures (except deposit signatures) on all blocks in the `chain_segment`. If all
/// signatures are valid, the `chain_segment` is mapped to a `Vec<SignatureVerifiedBlock>` that can
/// later be transformed into a `FullyVerifiedBlock` without re-checking the signatures. If any
@@ -369,11 +390,51 @@ pub struct FullyVerifiedBlock<'a, T: BeaconChainTypes> {
/// Implemented on types that can be converted into a `FullyVerifiedBlock`.
///
/// Used to allow functions to accept blocks at various stages of verification.
pub trait IntoFullyVerifiedBlock<T: BeaconChainTypes> {
pub trait IntoFullyVerifiedBlock<T: BeaconChainTypes>: Sized {
fn into_fully_verified_block(
self,
chain: &BeaconChain<T>,
) -> Result<FullyVerifiedBlock<T>, BlockError<T::EthSpec>>;
) -> Result<FullyVerifiedBlock<T>, BlockError<T::EthSpec>> {
self.into_fully_verified_block_slashable(chain)
.map(|fully_verified| {
// Supply valid block to slasher.
if let Some(slasher) = chain.slasher.as_ref() {
slasher.accept_block_header(fully_verified.block.signed_block_header());
}
fully_verified
})
.map_err(|slash_info| {
// Process invalid blocks to see if they are suitable for the slasher.
if let Some(slasher) = chain.slasher.as_ref() {
let (verified_header, error) = match slash_info {
BlockSlashInfo::SignatureNotChecked(header, e) => {
if verify_header_signature(chain, &header).is_ok() {
(header, e)
} else {
return e;
}
}
BlockSlashInfo::SignatureInvalid(e) => return e,
BlockSlashInfo::SignatureValid(header, e) => (header, e),
};
slasher.accept_block_header(verified_header);
error
} else {
match slash_info {
BlockSlashInfo::SignatureNotChecked(_, e)
| BlockSlashInfo::SignatureInvalid(e)
| BlockSlashInfo::SignatureValid(_, e) => e,
}
}
})
}
/// Convert the block to fully-verified form while producing data to aid checking slashability.
fn into_fully_verified_block_slashable(
self,
chain: &BeaconChain<T>,
) -> Result<FullyVerifiedBlock<T>, BlockSlashInfo<BlockError<T::EthSpec>>>;
fn block(&self) -> &SignedBeaconBlock<T::EthSpec>;
}
@@ -506,12 +567,13 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> {
impl<T: BeaconChainTypes> IntoFullyVerifiedBlock<T> for GossipVerifiedBlock<T> {
/// Completes verification of the wrapped `block`.
fn into_fully_verified_block(
fn into_fully_verified_block_slashable(
self,
chain: &BeaconChain<T>,
) -> Result<FullyVerifiedBlock<T>, BlockError<T::EthSpec>> {
let fully_verified = SignatureVerifiedBlock::from_gossip_verified_block(self, chain)?;
fully_verified.into_fully_verified_block(chain)
) -> Result<FullyVerifiedBlock<T>, BlockSlashInfo<BlockError<T::EthSpec>>> {
let fully_verified =
SignatureVerifiedBlock::from_gossip_verified_block_check_slashable(self, chain)?;
fully_verified.into_fully_verified_block_slashable(chain)
}
fn block(&self) -> &SignedBeaconBlock<T::EthSpec> {
@@ -558,6 +620,15 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> {
}
}
/// As for `new` above but producing `BlockSlashInfo`.
pub fn check_slashable(
block: SignedBeaconBlock<T::EthSpec>,
chain: &BeaconChain<T>,
) -> Result<Self, BlockSlashInfo<BlockError<T::EthSpec>>> {
let header = block.signed_block_header();
Self::new(block, chain).map_err(|e| BlockSlashInfo::from_early_error(header, e))
}
/// Finishes signature verification on the provided `GossipVerifedBlock`. Does not re-verify
/// the proposer signature.
pub fn from_gossip_verified_block(
@@ -589,18 +660,30 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> {
Err(BlockError::InvalidSignature)
}
}
/// Same as `from_gossip_verified_block` but producing slashing-relevant data as well.
pub fn from_gossip_verified_block_check_slashable(
from: GossipVerifiedBlock<T>,
chain: &BeaconChain<T>,
) -> Result<Self, BlockSlashInfo<BlockError<T::EthSpec>>> {
let header = from.block.signed_block_header();
Self::from_gossip_verified_block(from, chain)
.map_err(|e| BlockSlashInfo::from_early_error(header, e))
}
}
impl<T: BeaconChainTypes> IntoFullyVerifiedBlock<T> for SignatureVerifiedBlock<T> {
/// Completes verification of the wrapped `block`.
fn into_fully_verified_block(
fn into_fully_verified_block_slashable(
self,
chain: &BeaconChain<T>,
) -> Result<FullyVerifiedBlock<T>, BlockError<T::EthSpec>> {
) -> Result<FullyVerifiedBlock<T>, BlockSlashInfo<BlockError<T::EthSpec>>> {
let header = self.block.signed_block_header();
let (parent, block) = if let Some(parent) = self.parent {
(parent, self.block)
} else {
load_parent(self.block, chain)?
load_parent(self.block, chain)
.map_err(|e| BlockSlashInfo::SignatureValid(header.clone(), e))?
};
FullyVerifiedBlock::from_signature_verified_components(
@@ -609,6 +692,7 @@ impl<T: BeaconChainTypes> IntoFullyVerifiedBlock<T> for SignatureVerifiedBlock<T
parent,
chain,
)
.map_err(|e| BlockSlashInfo::SignatureValid(header, e))
}
fn block(&self) -> &SignedBeaconBlock<T::EthSpec> {
@@ -619,11 +703,12 @@ impl<T: BeaconChainTypes> IntoFullyVerifiedBlock<T> for SignatureVerifiedBlock<T
impl<T: BeaconChainTypes> IntoFullyVerifiedBlock<T> for SignedBeaconBlock<T::EthSpec> {
/// Verifies the `SignedBeaconBlock` by first transforming it into a `SignatureVerifiedBlock`
/// and then using that implementation of `IntoFullyVerifiedBlock` to complete verification.
fn into_fully_verified_block(
fn into_fully_verified_block_slashable(
self,
chain: &BeaconChain<T>,
) -> Result<FullyVerifiedBlock<T>, BlockError<T::EthSpec>> {
SignatureVerifiedBlock::new(self, chain)?.into_fully_verified_block(chain)
) -> Result<FullyVerifiedBlock<T>, BlockSlashInfo<BlockError<T::EthSpec>>> {
SignatureVerifiedBlock::check_slashable(self, chain)?
.into_fully_verified_block_slashable(chain)
}
fn block(&self) -> &SignedBeaconBlock<T::EthSpec> {
@@ -1125,6 +1210,38 @@ fn get_signature_verifier<'a, E: EthSpec>(
)
}
/// Verify that `header` was signed with a valid signature from its proposer.
///
/// Return `Ok(())` if the signature is valid, and an `Err` otherwise.
fn verify_header_signature<T: BeaconChainTypes>(
chain: &BeaconChain<T>,
header: &SignedBeaconBlockHeader,
) -> Result<(), BlockError<T::EthSpec>> {
let proposer_pubkey = get_validator_pubkey_cache(chain)?
.get(header.message.proposer_index as usize)
.cloned()
.ok_or_else(|| BlockError::UnknownValidator(header.message.proposer_index))?;
let (fork, genesis_validators_root) = chain
.with_head(|head| {
Ok((
head.beacon_state.fork,
head.beacon_state.genesis_validators_root,
))
})
.map_err(|e: BlockError<T::EthSpec>| e)?;
if header.verify_signature::<T::EthSpec>(
&proposer_pubkey,
&fork,
genesis_validators_root,
&chain.spec,
) {
Ok(())
} else {
Err(BlockError::ProposalSignatureInvalid)
}
}
fn expose_participation_metrics(summaries: &[EpochProcessingSummary]) {
if !cfg!(feature = "participation_metrics") {
return;
+10
View File
@@ -21,6 +21,7 @@ use fork_choice::ForkChoice;
use futures::channel::mpsc::Sender;
use operation_pool::{OperationPool, PersistedOperationPool};
use parking_lot::RwLock;
use slasher::Slasher;
use slog::{crit, info, Logger};
use slot_clock::{SlotClock, TestingSlotClock};
use std::marker::PhantomData;
@@ -99,6 +100,7 @@ pub struct BeaconChainBuilder<T: BeaconChainTypes> {
disabled_forks: Vec<String>,
log: Option<Logger>,
graffiti: Graffiti,
slasher: Option<Arc<Slasher<T::EthSpec>>>,
}
impl<TSlotClock, TEth1Backend, TEthSpec, TEventHandler, THotStore, TColdStore>
@@ -139,6 +141,7 @@ where
chain_config: ChainConfig::default(),
log: None,
graffiti: Graffiti::default(),
slasher: None,
}
}
@@ -174,6 +177,12 @@ where
self
}
/// Sets the slasher.
pub fn slasher(mut self, slasher: Arc<Slasher<TEthSpec>>) -> Self {
self.slasher = Some(slasher);
self
}
/// Sets the logger.
///
/// Should generally be called early in the build chain.
@@ -571,6 +580,7 @@ where
.ok_or_else(|| "Cannot build without a shutdown sender.".to_string())?,
log: log.clone(),
graffiti: self.graffiti,
slasher: self.slasher.clone(),
};
let head = beacon_chain
+1
View File
@@ -44,3 +44,4 @@ bus = "2.2.3"
directory = {path = "../../common/directory"}
http_api = { path = "../http_api" }
http_metrics = { path = "../http_metrics" }
slasher = { path = "../../slasher" }
+39
View File
@@ -16,6 +16,7 @@ use eth2_libp2p::NetworkGlobals;
use genesis::{interop_genesis_state, Eth1GenesisService};
use network::{NetworkConfig, NetworkMessage, NetworkService};
use parking_lot::Mutex;
use slasher::{Slasher, SlasherServer};
use slog::{debug, info, warn};
use ssz::Decode;
use std::net::SocketAddr;
@@ -64,6 +65,7 @@ pub struct ClientBuilder<T: BeaconChainTypes> {
http_api_config: http_api::Config,
http_metrics_config: http_metrics::Config,
websocket_listen_addr: Option<SocketAddr>,
slasher: Option<Arc<Slasher<T::EthSpec>>>,
eth_spec_instance: T::EthSpec,
}
@@ -97,6 +99,7 @@ where
http_api_config: <_>::default(),
http_metrics_config: <_>::default(),
websocket_listen_addr: None,
slasher: None,
eth_spec_instance,
}
}
@@ -113,6 +116,11 @@ where
self
}
pub fn slasher(mut self, slasher: Arc<Slasher<TEthSpec>>) -> Self {
self.slasher = Some(slasher);
self
}
/// Initializes the `BeaconChainBuilder`. The `build_beacon_chain` method will need to be
/// called later in order to actually instantiate the `BeaconChain`.
pub async fn beacon_chain_builder(
@@ -146,6 +154,12 @@ where
.disabled_forks(disabled_forks)
.graffiti(graffiti);
let builder = if let Some(slasher) = self.slasher.clone() {
builder.slasher(slasher)
} else {
builder
};
let chain_exists = builder
.store_contains_beacon_chain()
.unwrap_or_else(|_| false);
@@ -343,6 +357,27 @@ where
self
}
/// Immediately start the slasher service.
///
/// Error if no slasher is configured.
pub fn start_slasher_server(&self) -> Result<(), String> {
let context = self
.runtime_context
.as_ref()
.ok_or_else(|| "slasher requires a runtime_context")?
.service_context("slasher_server_ctxt".into());
let slasher = self
.slasher
.clone()
.ok_or_else(|| "slasher server requires a slasher")?;
let slot_clock = self
.slot_clock
.clone()
.ok_or_else(|| "slasher server requires a slot clock")?;
SlasherServer::run(slasher, slot_clock, &context.executor);
Ok(())
}
/// Immediately starts the service that periodically logs information each slot.
pub fn notifier(self) -> Result<Self, String> {
let context = self
@@ -442,6 +477,10 @@ where
None
};
if self.slasher.is_some() {
self.start_slasher_server()?;
}
Ok(Client {
beacon_chain: self.beacon_chain,
network_globals: self.network_globals,
+2
View File
@@ -63,6 +63,7 @@ pub struct Config {
pub eth1: eth1::Config,
pub http_api: http_api::Config,
pub http_metrics: http_metrics::Config,
pub slasher: Option<slasher::Config>,
}
impl Default for Config {
@@ -84,6 +85,7 @@ impl Default for Config {
graffiti: Graffiti::default(),
http_api: <_>::default(),
http_metrics: <_>::default(),
slasher: None,
}
}
}
+2 -2
View File
@@ -5,7 +5,7 @@ use crate::{
NetworkConfig,
};
use crate::{error, metrics};
use beacon_chain::{BeaconChain, BeaconChainTypes};
use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes};
use eth2_libp2p::{
rpc::{GoodbyeReason, RPCResponseErrorCode, RequestId},
Gossipsub, Libp2pEvent, PeerAction, PeerRequestId, PubsubMessage, Request, Response,
@@ -281,7 +281,7 @@ fn spawn_service<T: BeaconChainTypes>(
_ = service.gossipsub_parameter_update.next() => {
if let Ok(slot) = service.beacon_chain.slot() {
if let Some(active_validators) = service.beacon_chain.with_head(|head| {
Ok(
Ok::<_, BeaconChainError>(
head
.beacon_state
.get_cached_active_validator_indices(RelativeEpoch::Current)
+74
View File
@@ -359,6 +359,80 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.value_name("NUM_SLOTS")
.takes_value(true)
)
/*
* Slasher.
*/
.arg(
Arg::with_name("slasher")
.long("slasher")
.help(
"Run a slasher alongside the beacon node. It is currently only recommended for \
expert users because of the immaturity of the slasher UX and the extra \
resources required."
)
.takes_value(false)
)
.arg(
Arg::with_name("slasher-dir")
.long("slasher-dir")
.help(
"Set the slasher's database directory."
)
.value_name("PATH")
.takes_value(true)
.requires("slasher")
)
.arg(
Arg::with_name("slasher-update-period")
.long("slasher-update-period")
.help(
"Configure how often the slasher runs batch processing."
)
.value_name("SECONDS")
.requires("slasher")
.takes_value(true)
)
.arg(
Arg::with_name("slasher-history-length")
.long("slasher-history-length")
.help(
"Configure how many epochs of history the slasher keeps. Immutable after \
initialization."
)
.value_name("EPOCHS")
.requires("slasher")
.takes_value(true)
)
.arg(
Arg::with_name("slasher-max-db-size")
.long("slasher-max-db-size")
.help(
"Maximum size of the LMDB database used by the slasher."
)
.value_name("GIGABYTES")
.requires("slasher")
.takes_value(true)
)
.arg(
Arg::with_name("slasher-chunk-size")
.long("slasher-chunk-size")
.help(
"Number of epochs per validator per chunk stored on disk."
)
.value_name("EPOCHS")
.requires("slasher")
.takes_value(true)
)
.arg(
Arg::with_name("slasher-validator-chunk-size")
.long("slasher-validator-chunk-size")
.help(
"Number of validators per chunk stored on disk."
)
.value_name("NUM_VALIDATORS")
.requires("slasher")
.takes_value(true)
)
.arg(
Arg::with_name("wss-checkpoint")
.long("wss-checkpoint")
+37
View File
@@ -351,6 +351,43 @@ pub fn get_config<E: EthSpec>(
};
}
if cli_args.is_present("slasher") {
let slasher_dir = if let Some(slasher_dir) = cli_args.value_of("slasher-dir") {
PathBuf::from(slasher_dir)
} else {
client_config.data_dir.join("slasher_db")
};
let mut slasher_config = slasher::Config::new(slasher_dir);
if let Some(update_period) = clap_utils::parse_optional(cli_args, "slasher-update-period")?
{
slasher_config.update_period = update_period;
}
if let Some(history_length) =
clap_utils::parse_optional(cli_args, "slasher-history-length")?
{
slasher_config.history_length = history_length;
}
if let Some(max_db_size) = clap_utils::parse_optional(cli_args, "slasher-max-db-size")? {
slasher_config.max_db_size_gbs = max_db_size;
}
if let Some(chunk_size) = clap_utils::parse_optional(cli_args, "slasher-chunk-size")? {
slasher_config.chunk_size = chunk_size;
}
if let Some(validator_chunk_size) =
clap_utils::parse_optional(cli_args, "slasher-validator-chunk-size")?
{
slasher_config.validator_chunk_size = validator_chunk_size;
}
client_config.slasher = Some(slasher_config);
}
Ok(client_config)
}
+12
View File
@@ -17,8 +17,10 @@ use beacon_chain::{
};
use clap::ArgMatches;
use environment::RuntimeContext;
use slasher::Slasher;
use slog::{info, warn};
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use types::EthSpec;
/// A type-alias to the tighten the definition of a production-intended `Client`.
@@ -82,6 +84,16 @@ impl<E: EthSpec> ProductionBeaconNode<E> {
.http_api_config(client_config.http_api.clone())
.disk_store(&db_path, &freezer_db_path_res?, store_config)?;
let builder = if let Some(slasher_config) = client_config.slasher.clone() {
let slasher = Arc::new(
Slasher::open(slasher_config, log.new(slog::o!("service" => "slasher")))
.map_err(|e| format!("Slasher open error: {:?}", e))?,
);
builder.slasher(slasher)
} else {
builder
};
let builder = builder
.beacon_chain_builder(client_genesis, client_config_1)
.await?;
+1
View File
@@ -30,3 +30,4 @@ lazy_static = "1.4.0"
lighthouse_metrics = { path = "../../common/lighthouse_metrics" }
lru = "0.6.0"
sloggers = "1.0.1"
directory = { path = "../../common/directory" }
+1 -15
View File
@@ -1,6 +1,6 @@
pub use lighthouse_metrics::{set_gauge, try_create_int_gauge, *};
use std::fs;
use directory::size_of_dir;
use std::path::Path;
lazy_static! {
@@ -134,17 +134,3 @@ pub fn scrape_for_metrics(db_path: &Path, freezer_db_path: &Path) {
let freezer_db_size = size_of_dir(freezer_db_path);
set_gauge(&FREEZER_DB_SIZE, freezer_db_size as i64);
}
fn size_of_dir(path: &Path) -> u64 {
if let Ok(iter) = fs::read_dir(path) {
iter.filter_map(std::result::Result::ok)
.map(size_of_dir_entry)
.sum()
} else {
0
}
}
fn size_of_dir_entry(dir: fs::DirEntry) -> u64 {
dir.metadata().map(|m| m.len()).unwrap_or(0)
}