Attestation processing (#497)

* Renamed fork_choice::process_attestation_from_block

* Processing attestation in fork choice

* Retrieving state from store and checking signature

* Looser check on beacon state validity.

* Cleaned up get_attestation_state

* Expanded fork choice api to provide latest validator message.

* Checking if the an attestation contains a latest message

* Correct process_attestation error handling.

* Copy paste error in comment fixed.

* Tidy ancestor iterators

* Getting attestation slot via helper method

* Refactored attestation creation in test utils

* Revert "Refactored attestation creation in test utils"

This reverts commit 4d277fe4239a7194758b18fb5c00dfe0b8231306.

* Integration tests for free attestation processing

* Implicit conflicts resolved.

* formatting

* Do first pass on Grants code

* Add another attestation processing test

* Tidy attestation processing

* Remove old code fragment

* Add non-compiling half finished changes

* Simplify, fix bugs, add tests for chain iters

* Remove attestation processing from op pool

* Fix bug with fork choice, tidy

* Fix overly restrictive check in fork choice.

* Ensure committee cache is build during attn proc

* Ignore unknown blocks at fork choice

* Various minor fixes

* Make fork choice write lock in to read lock

* Remove unused method

* Tidy comments

* Fix attestation prod. target roots change

* Fix compile error in store iters

* Reject any attestation prior to finalization

* Fix minor PR comments

* Remove duplicated attestation finalization check

* Remove awkward `let` statement
This commit is contained in:
Paul Hauner
2019-08-14 10:55:24 +10:00
committed by GitHub
parent 989e2727d7
commit cd26a19a70
16 changed files with 695 additions and 183 deletions

View File

@@ -11,9 +11,12 @@ use operation_pool::{OperationPool, PersistedOperationPool};
use parking_lot::{RwLock, RwLockReadGuard};
use slog::{error, info, warn, Logger};
use slot_clock::SlotClock;
use state_processing::per_block_processing::errors::{
AttestationValidationError, AttesterSlashingValidationError, DepositValidationError,
ExitValidationError, ProposerSlashingValidationError, TransferValidationError,
use state_processing::per_block_processing::{
errors::{
AttestationValidationError, AttesterSlashingValidationError, DepositValidationError,
ExitValidationError, ProposerSlashingValidationError, TransferValidationError,
},
verify_attestation_for_state, VerifySignatures,
};
use state_processing::{
per_block_processing, per_block_processing_without_verifying_block_signature,
@@ -54,6 +57,26 @@ pub enum BlockProcessingOutcome {
PerBlockProcessingError(BlockProcessingError),
}
#[derive(Debug, PartialEq)]
pub enum AttestationProcessingOutcome {
Processed,
UnknownHeadBlock {
beacon_block_root: Hash256,
},
/// The attestation is attesting to a state that is later than itself. (Viz., attesting to the
/// future).
AttestsToFutureState {
state: Slot,
attestation: Slot,
},
/// The slot is finalized, no need to import.
FinalizedSlot {
attestation: Epoch,
finalized: Epoch,
},
Invalid(AttestationValidationError),
}
pub trait BeaconChainTypes {
type Store: store::Store;
type SlotClock: slot_clock::SlotClock;
@@ -237,15 +260,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
/// - Iterator returns `(Hash256, Slot)`.
/// - As this iterator starts at the `head` of the chain (viz., the best block), the first slot
/// returned may be earlier than the wall-clock slot.
pub fn rev_iter_block_roots(
&self,
slot: Slot,
) -> ReverseBlockRootIterator<T::EthSpec, T::Store> {
pub fn rev_iter_block_roots(&self) -> ReverseBlockRootIterator<T::EthSpec, T::Store> {
let state = &self.head().beacon_state;
let block_root = self.head().beacon_block_root;
let block_slot = state.slot;
let iter = BlockRootsIterator::owned(self.store.clone(), state.clone(), slot);
let iter = BlockRootsIterator::owned(self.store.clone(), state.clone());
ReverseBlockRootIterator::new((block_root, block_slot), iter)
}
@@ -259,15 +279,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
/// - Iterator returns `(Hash256, Slot)`.
/// - As this iterator starts at the `head` of the chain (viz., the best block), the first slot
/// returned may be earlier than the wall-clock slot.
pub fn rev_iter_state_roots(
&self,
slot: Slot,
) -> ReverseStateRootIterator<T::EthSpec, T::Store> {
pub fn rev_iter_state_roots(&self) -> ReverseStateRootIterator<T::EthSpec, T::Store> {
let state = &self.head().beacon_state;
let state_root = self.head().beacon_state_root;
let state_slot = state.slot;
let iter = StateRootsIterator::owned(self.store.clone(), state.clone(), slot);
let iter = StateRootsIterator::owned(self.store.clone(), state.clone());
ReverseStateRootIterator::new((state_root, state_slot), iter)
}
@@ -484,6 +501,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
} else {
*state.get_block_root(current_epoch_start_slot)?
};
let target = Checkpoint {
epoch: state.current_epoch(),
root: target_root,
@@ -513,38 +531,212 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
})
}
/// Accept a new attestation from the network.
/// Accept a new, potentially invalid attestation from the network.
///
/// If valid, the attestation is added to the `op_pool` and aggregated with another attestation
/// if possible.
/// If valid, the attestation is added to `self.op_pool` and `self.fork_choice`.
///
/// Returns an `Ok(AttestationProcessingOutcome)` if the chain was able to make a determination
/// about the `attestation` (whether it was invalid or not). Returns an `Err` if there was an
/// error during this process and no determination was able to be made.
///
/// ## Notes
///
/// - Whilst the `attestation` is added to fork choice, the head is not updated. That must be
/// done separately.
pub fn process_attestation(
&self,
attestation: Attestation<T::EthSpec>,
) -> Result<(), AttestationValidationError> {
) -> Result<AttestationProcessingOutcome, Error> {
// From the store, load the attestation's "head block".
//
// An honest validator would have set this block to be the head of the chain (i.e., the
// result of running fork choice).
if let Some(attestation_head_block) = self
.store
.get::<BeaconBlock<T::EthSpec>>(&attestation.data.beacon_block_root)?
{
// Attempt to process the attestation using the `self.head()` state.
//
// This is purely an effort to avoid loading a `BeaconState` unnecessarily from the DB.
// Take a read lock on the head beacon state.
let state = &self.head().beacon_state;
// If it turns out that the attestation was made using the head state, then there
// is no need to load a state from the database to process the attestation.
//
// Note: use the epoch of the target because it indicates which epoch the
// attestation was created in. You cannot use the epoch of the head block, because
// the block doesn't necessarily need to be in the same epoch as the attestation
// (e.g., if there are skip slots between the epoch the block was created in and
// the epoch for the attestation).
//
// This check also ensures that the slot for `data.beacon_block_root` is not higher
// than `state.root` by ensuring that the block is in the history of `state`.
if state.current_epoch() == attestation.data.target.epoch
&& (attestation.data.beacon_block_root == self.head().beacon_block_root
|| state
.get_block_root(attestation_head_block.slot)
.map(|root| *root == attestation.data.beacon_block_root)
.unwrap_or_else(|_| false))
{
// The head state is able to be used to validate this attestation. No need to load
// anything from the database.
return self.process_attestation_for_state_and_block(
attestation.clone(),
state,
&attestation_head_block,
);
}
// Ensure the read-lock from `self.head()` is dropped.
//
// This is likely unnecessary, however it remains as a reminder to ensure this lock
// isn't hogged.
std::mem::drop(state);
// Use the `data.beacon_block_root` to load the state from the latest non-skipped
// slot preceding the attestation's creation.
//
// This state is guaranteed to be in the same chain as the attestation, but it's
// not guaranteed to be from the same slot or epoch as the attestation.
let mut state: BeaconState<T::EthSpec> = self
.store
.get(&attestation_head_block.state_root)?
.ok_or_else(|| Error::MissingBeaconState(attestation_head_block.state_root))?;
// Ensure the state loaded from the database matches the state of the attestation
// head block.
//
// The state needs to be advanced from the current slot through to the epoch in
// which the attestation was created in. It would be an error to try and use
// `state.get_attestation_data_slot(..)` because the state matching the
// `data.beacon_block_root` isn't necessarily in a nearby epoch to the attestation
// (e.g., if there were lots of skip slots since the head of the chain and the
// epoch creation epoch).
for _ in state.slot.as_u64()
..attestation
.data
.target
.epoch
.start_slot(T::EthSpec::slots_per_epoch())
.as_u64()
{
per_slot_processing(&mut state, &self.spec)?;
}
state.build_committee_cache(RelativeEpoch::Current, &self.spec)?;
let attestation_slot = state.get_attestation_data_slot(&attestation.data)?;
// Reject any attestation where the `state` loaded from `data.beacon_block_root`
// has a higher slot than the attestation.
//
// Permitting this would allow for attesters to vote on _future_ slots.
if attestation_slot > state.slot {
Ok(AttestationProcessingOutcome::AttestsToFutureState {
state: state.slot,
attestation: attestation_slot,
})
} else {
self.process_attestation_for_state_and_block(
attestation,
&state,
&attestation_head_block,
)
}
} else {
// Drop any attestation where we have not processed `attestation.data.beacon_block_root`.
//
// This is likely overly restrictive, we could store the attestation for later
// processing.
warn!(
self.log,
"Dropped attestation for unknown block";
"block" => format!("{}", attestation.data.beacon_block_root)
);
Ok(AttestationProcessingOutcome::UnknownHeadBlock {
beacon_block_root: attestation.data.beacon_block_root,
})
}
}
/// Verifies the `attestation` against the `state` to which it is attesting.
///
/// Updates fork choice with any new latest messages, but _does not_ find or update the head.
///
/// ## Notes
///
/// The given `state` must fulfil one of the following conditions:
///
/// - `state` corresponds to the `block.state_root` identified by
/// `attestation.data.beacon_block_root`. (Viz., `attestation` was created using `state`).
/// - `state.slot` is in the same epoch as `data.target.epoch` and
/// `attestation.data.beacon_block_root` is in the history of `state`.
///
/// Additionally, `attestation.data.beacon_block_root` **must** be available to read in
/// `self.store` _and_ be the root of the given `block`.
///
/// If the given conditions are not fulfilled, the function may error or provide a false
/// negative (indicating that a given `attestation` is invalid when it is was validly formed).
fn process_attestation_for_state_and_block(
&self,
attestation: Attestation<T::EthSpec>,
state: &BeaconState<T::EthSpec>,
block: &BeaconBlock<T::EthSpec>,
) -> Result<AttestationProcessingOutcome, Error> {
self.metrics.attestation_processing_requests.inc();
let timer = self.metrics.attestation_processing_times.start_timer();
let result = self
.op_pool
.insert_attestation(attestation, &*self.state.read(), &self.spec);
// Find the highest between:
//
// - The highest valid finalized epoch we've ever seen (i.e., the head).
// - The finalized epoch that this attestation was created against.
let finalized_epoch = std::cmp::max(
self.head().beacon_state.finalized_checkpoint.epoch,
state.finalized_checkpoint.epoch,
);
let result = if block.slot <= finalized_epoch.start_slot(T::EthSpec::slots_per_epoch()) {
// Ignore any attestation where the slot of `data.beacon_block_root` is equal to or
// prior to the finalized epoch.
//
// For any valid attestation if the `beacon_block_root` is prior to finalization, then
// all other parameters (source, target, etc) must all be prior to finalization and
// therefore no longer interesting.
Ok(AttestationProcessingOutcome::FinalizedSlot {
attestation: block.slot.epoch(T::EthSpec::slots_per_epoch()),
finalized: finalized_epoch,
})
} else if let Err(e) =
verify_attestation_for_state(state, &attestation, &self.spec, VerifySignatures::True)
{
warn!(
self.log,
"Invalid attestation";
"state_epoch" => state.current_epoch(),
"error" => format!("{:?}", e),
);
Ok(AttestationProcessingOutcome::Invalid(e))
} else {
// Provide the attestation to fork choice, updating the validator latest messages but
// _without_ finding and updating the head.
self.fork_choice
.process_attestation(&state, &attestation, block)?;
// Provide the valid attestation to op pool, which may choose to retain the
// attestation for inclusion in a future block.
self.op_pool
.insert_attestation(attestation, state, &self.spec)?;
// Update the metrics.
self.metrics.attestation_processing_successes.inc();
Ok(AttestationProcessingOutcome::Processed)
};
timer.observe_duration();
if result.is_ok() {
self.metrics.attestation_processing_successes.inc();
}
// TODO: process attestation. Please consider:
//
// - Because a block was not added to the op pool does not mean it's invalid (it might
// just be old).
// - The attestation should be rejected if we don't know the block (ideally it should be
// queued, but this may be overkill).
// - The attestation _must_ be validated against it's state before being added to fork
// choice.
// - You can avoid verifying some attestations by first checking if they're a latest
// message. This would involve expanding the `LmdGhost` API.
result
}
@@ -612,7 +804,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
return Ok(BlockProcessingOutcome::GenesisBlock);
}
let block_root = block.block_header().canonical_root();
let block_root = block.canonical_root();
if block_root == self.genesis_block_root {
return Ok(BlockProcessingOutcome::GenesisBlock);
@@ -658,6 +850,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
per_slot_processing(&mut state, &self.spec)?;
}
state.build_committee_cache(RelativeEpoch::Previous, &self.spec)?;
state.build_committee_cache(RelativeEpoch::Current, &self.spec)?;
// Apply the received block to its parent state (which has been transitioned into this

View File

@@ -1,5 +1,8 @@
use crate::fork_choice::Error as ForkChoiceError;
use crate::metrics::Error as MetricsError;
use state_processing::per_block_processing::errors::{
AttestationValidationError, IndexedAttestationValidationError,
};
use state_processing::BlockProcessingError;
use state_processing::SlotProcessingError;
use types::*;
@@ -23,6 +26,7 @@ pub enum BeaconChainError {
previous_epoch: Epoch,
new_epoch: Epoch,
},
UnableToFindTargetRoot(Slot),
BeaconStateError(BeaconStateError),
DBInconsistent(String),
DBError(store::Error),
@@ -31,6 +35,11 @@ pub enum BeaconChainError {
MissingBeaconState(Hash256),
SlotProcessingError(SlotProcessingError),
MetricsError(String),
NoStateForAttestation {
beacon_block_root: Hash256,
},
AttestationValidationError(AttestationValidationError),
IndexedAttestationValidationError(IndexedAttestationValidationError),
}
easy_from_to!(SlotProcessingError, BeaconChainError);
@@ -53,3 +62,5 @@ pub enum BlockProductionError {
easy_from_to!(BlockProcessingError, BlockProductionError);
easy_from_to!(BeaconStateError, BlockProductionError);
easy_from_to!(SlotProcessingError, BlockProductionError);
easy_from_to!(AttestationValidationError, BeaconChainError);
easy_from_to!(IndexedAttestationValidationError, BeaconChainError);

View File

@@ -3,7 +3,9 @@ use lmd_ghost::LmdGhost;
use state_processing::common::get_attesting_indices;
use std::sync::Arc;
use store::{Error as StoreError, Store};
use types::{Attestation, BeaconBlock, BeaconState, BeaconStateError, Epoch, EthSpec, Hash256};
use types::{
Attestation, BeaconBlock, BeaconState, BeaconStateError, Epoch, EthSpec, Hash256, Slot,
};
type Result<T> = std::result::Result<T, Error>;
@@ -17,8 +19,8 @@ pub enum Error {
}
pub struct ForkChoice<T: BeaconChainTypes> {
backend: T::LmdGhost,
store: Arc<T::Store>,
backend: T::LmdGhost,
/// Used for resolving the `0x00..00` alias back to genesis.
///
/// Does not necessarily need to be the _actual_ genesis, it suffices to be the finalized root
@@ -117,18 +119,34 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
//
// https://github.com/ethereum/eth2.0-specs/blob/v0.7.0/specs/core/0_fork-choice.md
for attestation in &block.body.attestations {
self.process_attestation_from_block(state, attestation)?;
// If the `data.beacon_block_root` block is not known to us, simply ignore the latest
// vote.
if let Some(block) = self
.store
.get::<BeaconBlock<T::EthSpec>>(&attestation.data.beacon_block_root)?
{
self.process_attestation(state, attestation, &block)?;
}
}
// This does not apply a vote to the block, it just makes fork choice aware of the block so
// it can still be identified as the head even if it doesn't have any votes.
//
// A case where a block without any votes can be the head is where it is the only child of
// a block that has the majority of votes applied to it.
self.backend.process_block(block, block_root)?;
Ok(())
}
fn process_attestation_from_block(
/// Process an attestation which references `block` in `attestation.data.beacon_block_root`.
///
/// Assumes the attestation is valid.
pub fn process_attestation(
&self,
state: &BeaconState<T::EthSpec>,
attestation: &Attestation<T::EthSpec>,
block: &BeaconBlock<T::EthSpec>,
) -> Result<()> {
let block_hash = attestation.data.beacon_block_root;
@@ -147,26 +165,26 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
// to genesis just by being present in the chain.
//
// Additionally, don't add any block hash to fork choice unless we have imported the block.
if block_hash != Hash256::zero()
&& self
.store
.exists::<BeaconBlock<T::EthSpec>>(&block_hash)
.unwrap_or(false)
{
if block_hash != Hash256::zero() {
let validator_indices =
get_attesting_indices(state, &attestation.data, &attestation.aggregation_bits)?;
let block_slot = state.get_attestation_data_slot(&attestation.data)?;
for validator_index in validator_indices {
self.backend
.process_attestation(validator_index, block_hash, block_slot)?;
.process_attestation(validator_index, block_hash, block.slot)?;
}
}
Ok(())
}
/// Returns the latest message for a given validator, if any.
///
/// Returns `(block_root, block_slot)`.
pub fn latest_message(&self, validator_index: usize) -> Option<(Hash256, Slot)> {
self.backend.latest_message(validator_index)
}
/// Inform the fork choice that the given block (and corresponding root) have been finalized so
/// it may prune it's storage.
///

View File

@@ -7,7 +7,9 @@ mod metrics;
mod persisted_beacon_chain;
pub mod test_utils;
pub use self::beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessingOutcome};
pub use self::beacon_chain::{
AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BlockProcessingOutcome,
};
pub use self::checkpoint::CheckPoint;
pub use self::errors::{BeaconChainError, BlockProductionError};
pub use lmd_ghost;

View File

@@ -84,14 +84,30 @@ where
{
/// Instantiate a new harness with `validator_count` initial validators.
pub fn new(validator_count: usize) -> Self {
let state_builder = TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(
validator_count,
&E::default_spec(),
);
let (genesis_state, keypairs) = state_builder.build();
Self::from_state_and_keypairs(genesis_state, keypairs)
}
/// Instantiate a new harness with an initial validator for each key supplied.
pub fn from_keypairs(keypairs: Vec<Keypair>) -> Self {
let state_builder = TestingBeaconStateBuilder::from_keypairs(keypairs, &E::default_spec());
let (genesis_state, keypairs) = state_builder.build();
Self::from_state_and_keypairs(genesis_state, keypairs)
}
/// Instantiate a new harness with the given genesis state and a keypair for each of the
/// initial validators in the given state.
pub fn from_state_and_keypairs(genesis_state: BeaconState<E>, keypairs: Vec<Keypair>) -> Self {
let spec = E::default_spec();
let store = Arc::new(MemoryStore::open());
let state_builder =
TestingBeaconStateBuilder::from_default_keypairs_file_if_exists(validator_count, &spec);
let (genesis_state, keypairs) = state_builder.build();
let mut genesis_block = BeaconBlock::empty(&spec);
genesis_block.state_root = Hash256::from_slice(&genesis_state.tree_hash_root());
@@ -178,12 +194,7 @@ where
if let BlockProcessingOutcome::Processed { block_root } = outcome {
head_block_root = Some(block_root);
self.add_attestations_to_op_pool(
&attestation_strategy,
&new_state,
block_root,
slot,
);
self.add_free_attestations(&attestation_strategy, &new_state, block_root, slot);
} else {
panic!("block should be successfully processed: {:?}", outcome);
}
@@ -198,7 +209,7 @@ where
fn get_state_at_slot(&self, state_slot: Slot) -> BeaconState<E> {
let state_root = self
.chain
.rev_iter_state_roots(self.chain.head().beacon_state.slot - 1)
.rev_iter_state_roots()
.find(|(_hash, slot)| *slot == state_slot)
.map(|(hash, _slot)| hash)
.expect("could not find state root");
@@ -263,16 +274,38 @@ where
(block, state)
}
/// Adds attestations to the `BeaconChain` operations pool to be included in future blocks.
/// Adds attestations to the `BeaconChain` operations pool and fork choice.
///
/// The `attestation_strategy` dictates which validators should attest.
fn add_attestations_to_op_pool(
fn add_free_attestations(
&self,
attestation_strategy: &AttestationStrategy,
state: &BeaconState<E>,
head_block_root: Hash256,
head_block_slot: Slot,
) {
self.get_free_attestations(
attestation_strategy,
state,
head_block_root,
head_block_slot,
)
.into_iter()
.for_each(|attestation| {
self.chain
.process_attestation(attestation)
.expect("should process attestation");
});
}
/// Generates a `Vec<Attestation>` for some attestation strategy and head_block.
pub fn get_free_attestations(
&self,
attestation_strategy: &AttestationStrategy,
state: &BeaconState<E>,
head_block_root: Hash256,
head_block_slot: Slot,
) -> Vec<Attestation<E>> {
let spec = &self.spec;
let fork = &state.fork;
@@ -281,6 +314,8 @@ where
AttestationStrategy::SomeValidators(vec) => vec.clone(),
};
let mut vec = vec![];
state
.get_crosslink_committees_at_slot(state.slot)
.expect("should get committees")
@@ -326,19 +361,17 @@ where
agg_sig
};
let attestation = Attestation {
vec.push(Attestation {
aggregation_bits,
data,
custody_bits,
signature,
};
self.chain
.process_attestation(attestation)
.expect("should process attestation");
})
}
}
});
vec
}
/// Creates two forks: