mirror of
https://github.com/sigp/lighthouse.git
synced 2026-06-30 19:34:37 +00:00
merge with unstable
This commit is contained in:
@@ -18,6 +18,7 @@ use crate::chain_config::ChainConfig;
|
||||
use crate::early_attester_cache::EarlyAttesterCache;
|
||||
use crate::errors::{BeaconChainError as Error, BlockProductionError};
|
||||
use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend};
|
||||
use crate::eth1_finalization_cache::{Eth1FinalizationCache, Eth1FinalizationData};
|
||||
use crate::events::ServerSentEventHandler;
|
||||
use crate::execution_payload::get_execution_payload;
|
||||
use crate::execution_payload::PreparePayloadHandle;
|
||||
@@ -85,7 +86,7 @@ use state_processing::{
|
||||
},
|
||||
per_slot_processing,
|
||||
state_advance::{complete_state_advance, partial_state_advance},
|
||||
BlockSignatureStrategy, SigVerifiedOp, VerifyBlockRoot, VerifyOperation,
|
||||
BlockSignatureStrategy, ConsensusContext, SigVerifiedOp, VerifyBlockRoot, VerifyOperation,
|
||||
};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
@@ -119,6 +120,9 @@ pub const ATTESTATION_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
/// validator pubkey cache.
|
||||
pub const VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
/// The timeout for the eth1 finalization cache
|
||||
pub const ETH1_FINALIZATION_CACHE_LOCK_TIMEOUT: Duration = Duration::from_millis(200);
|
||||
|
||||
// These keys are all zero because they get stored in different columns, see `DBColumn` type.
|
||||
pub const BEACON_CHAIN_DB_KEY: Hash256 = Hash256::zero();
|
||||
pub const OP_POOL_DB_KEY: Hash256 = Hash256::zero();
|
||||
@@ -361,6 +365,8 @@ pub struct BeaconChain<T: BeaconChainTypes> {
|
||||
pub(crate) snapshot_cache: TimeoutRwLock<SnapshotCache<T::EthSpec>>,
|
||||
/// Caches the attester shuffling for a given epoch and shuffling key root.
|
||||
pub shuffling_cache: TimeoutRwLock<ShufflingCache>,
|
||||
/// A cache of eth1 deposit data at epoch boundaries for deposit finalization
|
||||
pub eth1_finalization_cache: TimeoutRwLock<Eth1FinalizationCache>,
|
||||
/// Caches the beacon block proposer shuffling for a given epoch and shuffling key root.
|
||||
pub beacon_proposer_cache: Mutex<BeaconProposerCache>,
|
||||
/// Caches a map of `validator_index -> validator_pubkey`.
|
||||
@@ -2013,60 +2019,75 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
target_epoch: Epoch,
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
) -> bool {
|
||||
let slots_per_epoch = T::EthSpec::slots_per_epoch();
|
||||
let shuffling_lookahead = 1 + self.spec.min_seed_lookahead.as_u64();
|
||||
|
||||
// Shuffling can't have changed if we're in the first few epochs
|
||||
if state.current_epoch() < shuffling_lookahead {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise the shuffling is determined by the block at the end of the target epoch
|
||||
// minus the shuffling lookahead (usually 2). We call this the "pivot".
|
||||
let pivot_slot =
|
||||
if target_epoch == state.previous_epoch() || target_epoch == state.current_epoch() {
|
||||
(target_epoch - shuffling_lookahead).end_slot(slots_per_epoch)
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let state_pivot_block_root = match state.get_block_root(pivot_slot) {
|
||||
Ok(root) => *root,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
&self.log,
|
||||
"Missing pivot block root for attestation";
|
||||
"slot" => pivot_slot,
|
||||
"error" => ?e,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Use fork choice's view of the block DAG to quickly evaluate whether the attestation's
|
||||
// pivot block is the same as the current state's pivot block. If it is, then the
|
||||
// attestation's shuffling is the same as the current state's.
|
||||
// To account for skipped slots, find the first block at *or before* the pivot slot.
|
||||
let fork_choice_lock = self.canonical_head.fork_choice_read_lock();
|
||||
let pivot_block_root = fork_choice_lock
|
||||
.proto_array()
|
||||
.core_proto_array()
|
||||
.iter_block_roots(block_root)
|
||||
.find(|(_, slot)| *slot <= pivot_slot)
|
||||
.map(|(block_root, _)| block_root);
|
||||
drop(fork_choice_lock);
|
||||
|
||||
match pivot_block_root {
|
||||
Some(root) => root == state_pivot_block_root,
|
||||
None => {
|
||||
self.shuffling_is_compatible_result(block_root, target_epoch, state)
|
||||
.unwrap_or_else(|e| {
|
||||
debug!(
|
||||
&self.log,
|
||||
"Discarding attestation because of missing ancestor";
|
||||
"pivot_slot" => pivot_slot.as_u64(),
|
||||
self.log,
|
||||
"Skipping attestation with incompatible shuffling";
|
||||
"block_root" => ?block_root,
|
||||
"target_epoch" => target_epoch,
|
||||
"reason" => ?e,
|
||||
);
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
fn shuffling_is_compatible_result(
|
||||
&self,
|
||||
block_root: &Hash256,
|
||||
target_epoch: Epoch,
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
) -> Result<bool, Error> {
|
||||
// Compute the shuffling ID for the head state in the `target_epoch`.
|
||||
let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), target_epoch)
|
||||
.map_err(|e| Error::BeaconStateError(e.into()))?;
|
||||
let head_shuffling_id =
|
||||
AttestationShufflingId::new(self.genesis_block_root, state, relative_epoch)?;
|
||||
|
||||
// Load the block's shuffling ID from fork choice. We use the variant of `get_block` that
|
||||
// checks descent from the finalized block, so there's one case where we'll spuriously
|
||||
// return `false`: where an attestation for the previous epoch nominates the pivot block
|
||||
// which is the parent block of the finalized block. Such attestations are not useful, so
|
||||
// this doesn't matter.
|
||||
let fork_choice_lock = self.canonical_head.fork_choice_read_lock();
|
||||
let block = fork_choice_lock
|
||||
.get_block(block_root)
|
||||
.ok_or(Error::AttestationHeadNotInForkChoice(*block_root))?;
|
||||
drop(fork_choice_lock);
|
||||
|
||||
let block_shuffling_id = if target_epoch == block.current_epoch_shuffling_id.shuffling_epoch
|
||||
{
|
||||
block.current_epoch_shuffling_id
|
||||
} else if target_epoch == block.next_epoch_shuffling_id.shuffling_epoch {
|
||||
block.next_epoch_shuffling_id
|
||||
} else if target_epoch > block.next_epoch_shuffling_id.shuffling_epoch {
|
||||
AttestationShufflingId {
|
||||
shuffling_epoch: target_epoch,
|
||||
shuffling_decision_block: *block_root,
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
self.log,
|
||||
"Skipping attestation with incompatible shuffling";
|
||||
"block_root" => ?block_root,
|
||||
"target_epoch" => target_epoch,
|
||||
"reason" => "target epoch less than block epoch"
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if head_shuffling_id == block_shuffling_id {
|
||||
Ok(true)
|
||||
} else {
|
||||
debug!(
|
||||
self.log,
|
||||
"Skipping attestation with incompatible shuffling";
|
||||
"block_root" => ?block_root,
|
||||
"target_epoch" => target_epoch,
|
||||
"head_shuffling_id" => ?head_shuffling_id,
|
||||
"block_shuffling_id" => ?block_shuffling_id,
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2538,9 +2559,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
block,
|
||||
block_root,
|
||||
state,
|
||||
parent_block: _,
|
||||
parent_block,
|
||||
confirmed_state_roots,
|
||||
payload_verification_handle,
|
||||
parent_eth1_finalization_data,
|
||||
} = execution_pending_block;
|
||||
|
||||
let PayloadVerificationOutcome {
|
||||
@@ -2592,6 +2614,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
confirmed_state_roots,
|
||||
payload_verification_status,
|
||||
count_unrealized,
|
||||
parent_block,
|
||||
parent_eth1_finalization_data,
|
||||
)
|
||||
},
|
||||
"payload_verification_handle",
|
||||
@@ -2606,6 +2630,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
///
|
||||
/// An error is returned if the block was unable to be imported. It may be partially imported
|
||||
/// (i.e., this function is not atomic).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn import_block(
|
||||
&self,
|
||||
signed_block: Arc<SignedBeaconBlock<T::EthSpec>>,
|
||||
@@ -2614,6 +2639,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
confirmed_state_roots: Vec<Hash256>,
|
||||
payload_verification_status: PayloadVerificationStatus,
|
||||
count_unrealized: CountUnrealized,
|
||||
parent_block: SignedBlindedBeaconBlock<T::EthSpec>,
|
||||
parent_eth1_finalization_data: Eth1FinalizationData,
|
||||
) -> Result<Hash256, BlockError<T::EthSpec>> {
|
||||
let current_slot = self.slot()?;
|
||||
let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
@@ -2994,6 +3021,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let parent_root = block.parent_root();
|
||||
let slot = block.slot();
|
||||
|
||||
let current_eth1_finalization_data = Eth1FinalizationData {
|
||||
eth1_data: state.eth1_data().clone(),
|
||||
eth1_deposit_index: state.eth1_deposit_index(),
|
||||
};
|
||||
let current_finalized_checkpoint = state.finalized_checkpoint();
|
||||
self.snapshot_cache
|
||||
.try_write_for(BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT)
|
||||
.ok_or(Error::SnapshotCacheLockTimeout)
|
||||
@@ -3067,6 +3099,57 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
);
|
||||
}
|
||||
|
||||
// Do not write to eth1 finalization cache for blocks older than 5 epochs
|
||||
// this helps reduce noise during sync
|
||||
if block_delay_total
|
||||
< self.slot_clock.slot_duration() * 5 * (T::EthSpec::slots_per_epoch() as u32)
|
||||
{
|
||||
let parent_block_epoch = parent_block.slot().epoch(T::EthSpec::slots_per_epoch());
|
||||
if parent_block_epoch < current_epoch {
|
||||
// we've crossed epoch boundary, store Eth1FinalizationData
|
||||
let (checkpoint, eth1_finalization_data) =
|
||||
if current_slot % T::EthSpec::slots_per_epoch() == 0 {
|
||||
// current block is the checkpoint
|
||||
(
|
||||
Checkpoint {
|
||||
epoch: current_epoch,
|
||||
root: block_root,
|
||||
},
|
||||
current_eth1_finalization_data,
|
||||
)
|
||||
} else {
|
||||
// parent block is the checkpoint
|
||||
(
|
||||
Checkpoint {
|
||||
epoch: current_epoch,
|
||||
root: parent_block.canonical_root(),
|
||||
},
|
||||
parent_eth1_finalization_data,
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(finalized_eth1_data) = self
|
||||
.eth1_finalization_cache
|
||||
.try_write_for(ETH1_FINALIZATION_CACHE_LOCK_TIMEOUT)
|
||||
.and_then(|mut cache| {
|
||||
cache.insert(checkpoint, eth1_finalization_data);
|
||||
cache.finalize(¤t_finalized_checkpoint)
|
||||
})
|
||||
{
|
||||
if let Some(eth1_chain) = self.eth1_chain.as_ref() {
|
||||
let finalized_deposit_count = finalized_eth1_data.deposit_count;
|
||||
eth1_chain.finalize_eth1_data(finalized_eth1_data);
|
||||
debug!(
|
||||
self.log,
|
||||
"called eth1_chain.finalize_eth1_data()";
|
||||
"epoch" => current_finalized_checkpoint.epoch,
|
||||
"deposit count" => finalized_deposit_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inform the unknown block cache, in case it was waiting on this block.
|
||||
self.pre_finalization_block_cache
|
||||
.block_processed(block_root);
|
||||
@@ -3525,7 +3608,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
|
||||
let slot = state.slot();
|
||||
let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64;
|
||||
|
||||
let sync_aggregate = if matches!(&state, BeaconState::Base(_)) {
|
||||
None
|
||||
@@ -3721,12 +3803,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
ProduceBlockVerification::VerifyRandao => BlockSignatureStrategy::VerifyRandao,
|
||||
ProduceBlockVerification::NoVerification => BlockSignatureStrategy::NoVerification,
|
||||
};
|
||||
// Use a context without block root or proposer index so that both are checked.
|
||||
let mut ctxt = ConsensusContext::new(block.slot());
|
||||
per_block_processing(
|
||||
&mut state,
|
||||
&block,
|
||||
None,
|
||||
signature_strategy,
|
||||
VerifyBlockRoot::True,
|
||||
&mut ctxt,
|
||||
&self.spec,
|
||||
)?;
|
||||
drop(process_timer);
|
||||
@@ -4552,7 +4636,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
///
|
||||
/// If the committee for `(head_block_root, shuffling_epoch)` isn't found in the
|
||||
/// `shuffling_cache`, we will read a state from disk and then update the `shuffling_cache`.
|
||||
pub(crate) fn with_committee_cache<F, R>(
|
||||
pub fn with_committee_cache<F, R>(
|
||||
&self,
|
||||
head_block_root: Hash256,
|
||||
shuffling_epoch: Epoch,
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
//! END
|
||||
//!
|
||||
//! ```
|
||||
use crate::eth1_finalization_cache::Eth1FinalizationData;
|
||||
use crate::execution_payload::{
|
||||
is_optimistic_candidate_block, validate_execution_payload_for_gossip, validate_merge_block,
|
||||
AllowOptimisticImport, PayloadNotifier,
|
||||
@@ -71,7 +72,8 @@ use state_processing::{
|
||||
block_signature_verifier::{BlockSignatureVerifier, Error as BlockSignatureVerifierError},
|
||||
per_block_processing, per_slot_processing,
|
||||
state_advance::partial_state_advance,
|
||||
BlockProcessingError, BlockSignatureStrategy, SlotProcessingError, VerifyBlockRoot,
|
||||
BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError,
|
||||
VerifyBlockRoot,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::fs;
|
||||
@@ -550,7 +552,7 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
|
||||
let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec);
|
||||
|
||||
for (block_root, block) in &chain_segment {
|
||||
signature_verifier.include_all_signatures(block, Some(*block_root))?;
|
||||
signature_verifier.include_all_signatures(block, Some(*block_root), None)?;
|
||||
}
|
||||
|
||||
if signature_verifier.verify().is_err() {
|
||||
@@ -561,10 +563,17 @@ pub fn signature_verify_chain_segment<T: BeaconChainTypes>(
|
||||
|
||||
let mut signature_verified_blocks = chain_segment
|
||||
.into_iter()
|
||||
.map(|(block_root, block)| SignatureVerifiedBlock {
|
||||
block,
|
||||
block_root,
|
||||
parent: None,
|
||||
.map(|(block_root, block)| {
|
||||
// Proposer index has already been verified above during signature verification.
|
||||
let consensus_context = ConsensusContext::new(block.slot())
|
||||
.set_current_block_root(block_root)
|
||||
.set_proposer_index(block.message().proposer_index());
|
||||
SignatureVerifiedBlock {
|
||||
block,
|
||||
block_root,
|
||||
parent: None,
|
||||
consensus_context,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -583,6 +592,7 @@ pub struct GossipVerifiedBlock<T: BeaconChainTypes> {
|
||||
pub block: Arc<SignedBeaconBlock<T::EthSpec>>,
|
||||
pub block_root: Hash256,
|
||||
parent: Option<PreProcessingSnapshot<T::EthSpec>>,
|
||||
consensus_context: ConsensusContext<T::EthSpec>,
|
||||
}
|
||||
|
||||
/// A wrapper around a `SignedBeaconBlock` that indicates that all signatures (except the deposit
|
||||
@@ -591,6 +601,7 @@ pub struct SignatureVerifiedBlock<T: BeaconChainTypes> {
|
||||
block: Arc<SignedBeaconBlock<T::EthSpec>>,
|
||||
block_root: Hash256,
|
||||
parent: Option<PreProcessingSnapshot<T::EthSpec>>,
|
||||
consensus_context: ConsensusContext<T::EthSpec>,
|
||||
}
|
||||
|
||||
/// Used to await the result of executing payload with a remote EE.
|
||||
@@ -613,6 +624,7 @@ pub struct ExecutionPendingBlock<T: BeaconChainTypes> {
|
||||
pub block_root: Hash256,
|
||||
pub state: BeaconState<T::EthSpec>,
|
||||
pub parent_block: SignedBeaconBlock<T::EthSpec, BlindedPayload<T::EthSpec>>,
|
||||
pub parent_eth1_finalization_data: Eth1FinalizationData,
|
||||
pub confirmed_state_roots: Vec<Hash256>,
|
||||
pub payload_verification_handle: PayloadVerificationHandle<T::EthSpec>,
|
||||
}
|
||||
@@ -864,10 +876,16 @@ impl<T: BeaconChainTypes> GossipVerifiedBlock<T> {
|
||||
// Validate the block's execution_payload (if any).
|
||||
validate_execution_payload_for_gossip(&parent_block, block.message(), chain)?;
|
||||
|
||||
// Having checked the proposer index and the block root we can cache them.
|
||||
let consensus_context = ConsensusContext::new(block.slot())
|
||||
.set_current_block_root(block_root)
|
||||
.set_proposer_index(block.message().proposer_index());
|
||||
|
||||
Ok(Self {
|
||||
block,
|
||||
block_root,
|
||||
parent,
|
||||
consensus_context,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -927,10 +945,13 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> {
|
||||
|
||||
let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec);
|
||||
|
||||
signature_verifier.include_all_signatures(&block, Some(block_root))?;
|
||||
signature_verifier.include_all_signatures(&block, Some(block_root), None)?;
|
||||
|
||||
if signature_verifier.verify().is_ok() {
|
||||
Ok(Self {
|
||||
consensus_context: ConsensusContext::new(block.slot())
|
||||
.set_current_block_root(block_root)
|
||||
.set_proposer_index(block.message().proposer_index()),
|
||||
block,
|
||||
block_root,
|
||||
parent: Some(parent),
|
||||
@@ -973,13 +994,18 @@ impl<T: BeaconChainTypes> SignatureVerifiedBlock<T> {
|
||||
|
||||
let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec);
|
||||
|
||||
signature_verifier.include_all_signatures_except_proposal(&block)?;
|
||||
// Gossip verification has already checked the proposer index. Use it to check the RANDAO
|
||||
// signature.
|
||||
let verified_proposer_index = Some(block.message().proposer_index());
|
||||
signature_verifier
|
||||
.include_all_signatures_except_proposal(&block, verified_proposer_index)?;
|
||||
|
||||
if signature_verifier.verify().is_ok() {
|
||||
Ok(Self {
|
||||
block,
|
||||
block_root: from.block_root,
|
||||
parent: Some(parent),
|
||||
consensus_context: from.consensus_context,
|
||||
})
|
||||
} else {
|
||||
Err(BlockError::InvalidSignature)
|
||||
@@ -1016,8 +1042,14 @@ impl<T: BeaconChainTypes> IntoExecutionPendingBlock<T> for SignatureVerifiedBloc
|
||||
.map_err(|e| BlockSlashInfo::SignatureValid(header.clone(), e))?
|
||||
};
|
||||
|
||||
ExecutionPendingBlock::from_signature_verified_components(block, block_root, parent, chain)
|
||||
.map_err(|e| BlockSlashInfo::SignatureValid(header, e))
|
||||
ExecutionPendingBlock::from_signature_verified_components(
|
||||
block,
|
||||
block_root,
|
||||
parent,
|
||||
self.consensus_context,
|
||||
chain,
|
||||
)
|
||||
.map_err(|e| BlockSlashInfo::SignatureValid(header, e))
|
||||
}
|
||||
|
||||
fn block(&self) -> &SignedBeaconBlock<T::EthSpec> {
|
||||
@@ -1058,6 +1090,7 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
|
||||
block: Arc<SignedBeaconBlock<T::EthSpec>>,
|
||||
block_root: Hash256,
|
||||
parent: PreProcessingSnapshot<T::EthSpec>,
|
||||
mut consensus_context: ConsensusContext<T::EthSpec>,
|
||||
chain: &Arc<BeaconChain<T>>,
|
||||
) -> Result<Self, BlockError<T::EthSpec>> {
|
||||
if let Some(parent) = chain
|
||||
@@ -1134,6 +1167,11 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
|
||||
.into());
|
||||
}
|
||||
|
||||
let parent_eth1_finalization_data = Eth1FinalizationData {
|
||||
eth1_data: state.eth1_data().clone(),
|
||||
eth1_deposit_index: state.eth1_deposit_index(),
|
||||
};
|
||||
|
||||
let distance = block.slot().as_u64().saturating_sub(state.slot().as_u64());
|
||||
for _ in 0..distance {
|
||||
let state_root = if parent.beacon_block.slot() == state.slot() {
|
||||
@@ -1341,10 +1379,10 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
|
||||
if let Err(err) = per_block_processing(
|
||||
&mut state,
|
||||
&block,
|
||||
Some(block_root),
|
||||
// Signatures were verified earlier in this function.
|
||||
BlockSignatureStrategy::NoVerification,
|
||||
VerifyBlockRoot::True,
|
||||
&mut consensus_context,
|
||||
&chain.spec,
|
||||
) {
|
||||
match err {
|
||||
@@ -1389,6 +1427,7 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
|
||||
block_root,
|
||||
state,
|
||||
parent_block: parent.beacon_block,
|
||||
parent_eth1_finalization_data,
|
||||
confirmed_state_roots,
|
||||
payload_verification_handle,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::beacon_chain::{CanonicalHead, BEACON_CHAIN_DB_KEY, ETH1_CACHE_DB_KEY, OP_POOL_DB_KEY};
|
||||
use crate::eth1_chain::{CachingEth1Backend, SszEth1};
|
||||
use crate::eth1_finalization_cache::Eth1FinalizationCache;
|
||||
use crate::fork_choice_signal::ForkChoiceSignalTx;
|
||||
use crate::fork_revert::{reset_fork_choice_to_finalization, revert_to_fork_boundary};
|
||||
use crate::head_tracker::HeadTracker;
|
||||
@@ -795,6 +796,7 @@ where
|
||||
head_for_snapshot_cache,
|
||||
)),
|
||||
shuffling_cache: TimeoutRwLock::new(ShufflingCache::new()),
|
||||
eth1_finalization_cache: TimeoutRwLock::new(Eth1FinalizationCache::new(log.clone())),
|
||||
beacon_proposer_cache: <_>::default(),
|
||||
block_times_cache: <_>::default(),
|
||||
pre_finalization_block_cache: <_>::default(),
|
||||
@@ -897,7 +899,7 @@ where
|
||||
.ok_or("dummy_eth1_backend requires a log")?;
|
||||
|
||||
let backend =
|
||||
CachingEth1Backend::new(Eth1Config::default(), log.clone(), self.spec.clone());
|
||||
CachingEth1Backend::new(Eth1Config::default(), log.clone(), self.spec.clone())?;
|
||||
|
||||
self.eth1_chain = Some(Eth1Chain::new_dummy(backend));
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ use store::{DBColumn, Error as StoreError, StoreItem};
|
||||
use task_executor::TaskExecutor;
|
||||
use types::{
|
||||
BeaconState, BeaconStateError, ChainSpec, Deposit, Eth1Data, EthSpec, Hash256, Slot, Unsigned,
|
||||
DEPOSIT_TREE_DEPTH,
|
||||
};
|
||||
|
||||
type BlockNumber = u64;
|
||||
@@ -170,8 +169,8 @@ fn get_sync_status<T: EthSpec>(
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct SszEth1 {
|
||||
use_dummy_backend: bool,
|
||||
backend_bytes: Vec<u8>,
|
||||
pub use_dummy_backend: bool,
|
||||
pub backend_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl StoreItem for SszEth1 {
|
||||
@@ -305,6 +304,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Set in motion the finalization of `Eth1Data`. This method is called during block import
|
||||
/// so it should be fast.
|
||||
pub fn finalize_eth1_data(&self, eth1_data: Eth1Data) {
|
||||
self.backend.finalize_eth1_data(eth1_data);
|
||||
}
|
||||
|
||||
/// Consumes `self`, returning the backend.
|
||||
pub fn into_backend(self) -> T {
|
||||
self.backend
|
||||
@@ -335,6 +340,10 @@ pub trait Eth1ChainBackend<T: EthSpec>: Sized + Send + Sync {
|
||||
/// beacon node eth1 cache is.
|
||||
fn latest_cached_block(&self) -> Option<Eth1Block>;
|
||||
|
||||
/// Set in motion the finalization of `Eth1Data`. This method is called during block import
|
||||
/// so it should be fast.
|
||||
fn finalize_eth1_data(&self, eth1_data: Eth1Data);
|
||||
|
||||
/// Returns the block at the head of the chain (ignoring follow distance, etc). Used to obtain
|
||||
/// an idea of how up-to-date the remote eth1 node is.
|
||||
fn head_block(&self) -> Option<Eth1Block>;
|
||||
@@ -389,6 +398,8 @@ impl<T: EthSpec> Eth1ChainBackend<T> for DummyEth1ChainBackend<T> {
|
||||
None
|
||||
}
|
||||
|
||||
fn finalize_eth1_data(&self, _eth1_data: Eth1Data) {}
|
||||
|
||||
fn head_block(&self) -> Option<Eth1Block> {
|
||||
None
|
||||
}
|
||||
@@ -431,12 +442,13 @@ impl<T: EthSpec> CachingEth1Backend<T> {
|
||||
/// Instantiates `self` with empty caches.
|
||||
///
|
||||
/// Does not connect to the eth1 node or start any tasks to keep the cache updated.
|
||||
pub fn new(config: Eth1Config, log: Logger, spec: ChainSpec) -> Self {
|
||||
Self {
|
||||
core: HttpService::new(config, log.clone(), spec),
|
||||
pub fn new(config: Eth1Config, log: Logger, spec: ChainSpec) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
core: HttpService::new(config, log.clone(), spec)
|
||||
.map_err(|e| format!("Failed to create eth1 http service: {:?}", e))?,
|
||||
log,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Starts the routine which connects to the external eth1 node and updates the caches.
|
||||
@@ -546,7 +558,7 @@ impl<T: EthSpec> Eth1ChainBackend<T> for CachingEth1Backend<T> {
|
||||
.deposits()
|
||||
.read()
|
||||
.cache
|
||||
.get_deposits(next, last, deposit_count, DEPOSIT_TREE_DEPTH)
|
||||
.get_deposits(next, last, deposit_count)
|
||||
.map_err(|e| Error::BackendError(format!("Failed to get deposits: {:?}", e)))
|
||||
.map(|(_deposit_root, deposits)| deposits)
|
||||
}
|
||||
@@ -557,6 +569,12 @@ impl<T: EthSpec> Eth1ChainBackend<T> for CachingEth1Backend<T> {
|
||||
self.core.latest_cached_block()
|
||||
}
|
||||
|
||||
/// This only writes the eth1_data to a temporary cache so that the service
|
||||
/// thread can later do the actual finalizing of the deposit tree.
|
||||
fn finalize_eth1_data(&self, eth1_data: Eth1Data) {
|
||||
self.core.set_to_finalize(Some(eth1_data));
|
||||
}
|
||||
|
||||
fn head_block(&self) -> Option<Eth1Block> {
|
||||
self.core.head_block()
|
||||
}
|
||||
@@ -730,11 +748,9 @@ mod test {
|
||||
};
|
||||
|
||||
let log = null_logger().unwrap();
|
||||
Eth1Chain::new(CachingEth1Backend::new(
|
||||
eth1_config,
|
||||
log,
|
||||
MainnetEthSpec::default_spec(),
|
||||
))
|
||||
Eth1Chain::new(
|
||||
CachingEth1Backend::new(eth1_config, log, MainnetEthSpec::default_spec()).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_deposit_log(i: u64, spec: &ChainSpec) -> DepositLog {
|
||||
|
||||
498
beacon_node/beacon_chain/src/eth1_finalization_cache.rs
Normal file
498
beacon_node/beacon_chain/src/eth1_finalization_cache.rs
Normal file
@@ -0,0 +1,498 @@
|
||||
use slog::{debug, Logger};
|
||||
use std::cmp;
|
||||
use std::collections::BTreeMap;
|
||||
use types::{Checkpoint, Epoch, Eth1Data, Hash256 as Root};
|
||||
|
||||
/// The default size of the cache.
|
||||
/// The beacon chain only looks at the last 4 epochs for finalization.
|
||||
/// Add 1 for current epoch and 4 earlier epochs.
|
||||
pub const DEFAULT_ETH1_CACHE_SIZE: usize = 5;
|
||||
|
||||
/// These fields are named the same as the corresponding fields in the `BeaconState`
|
||||
/// as this structure stores these values from the `BeaconState` at a `Checkpoint`
|
||||
#[derive(Clone)]
|
||||
pub struct Eth1FinalizationData {
|
||||
pub eth1_data: Eth1Data,
|
||||
pub eth1_deposit_index: u64,
|
||||
}
|
||||
|
||||
impl Eth1FinalizationData {
|
||||
/// Ensures the deposit finalization conditions have been met. See:
|
||||
/// https://eips.ethereum.org/EIPS/eip-4881#deposit-finalization-conditions
|
||||
fn fully_imported(&self) -> bool {
|
||||
self.eth1_deposit_index >= self.eth1_data.deposit_count
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements map from Checkpoint -> Eth1CacheData
|
||||
pub struct CheckpointMap {
|
||||
capacity: usize,
|
||||
// There shouldn't be more than a couple of potential checkpoints at the same
|
||||
// epoch. Searching through a vector for the matching Root should be faster
|
||||
// than using another map from Root->Eth1CacheData
|
||||
store: BTreeMap<Epoch, Vec<(Root, Eth1FinalizationData)>>,
|
||||
}
|
||||
|
||||
impl Default for CheckpointMap {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a map of `Eth1CacheData` referenced by `Checkpoint`
|
||||
///
|
||||
/// ## Cache Queuing
|
||||
///
|
||||
/// The cache keeps a maximum number of (`capacity`) epochs. Because there may be
|
||||
/// forks at the epoch boundary, it's possible that there exists more than one
|
||||
/// `Checkpoint` for the same `Epoch`. This cache will store all checkpoints for
|
||||
/// a given `Epoch`. When adding data for a new `Checkpoint` would cause the number
|
||||
/// of `Epoch`s stored to exceed `capacity`, the data for oldest `Epoch` is dropped
|
||||
impl CheckpointMap {
|
||||
pub fn new() -> Self {
|
||||
CheckpointMap {
|
||||
capacity: DEFAULT_ETH1_CACHE_SIZE,
|
||||
store: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
CheckpointMap {
|
||||
capacity: cmp::max(1, capacity),
|
||||
store: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, checkpoint: Checkpoint, eth1_finalization_data: Eth1FinalizationData) {
|
||||
self.store
|
||||
.entry(checkpoint.epoch)
|
||||
.or_insert_with(Vec::new)
|
||||
.push((checkpoint.root, eth1_finalization_data));
|
||||
|
||||
// faster to reduce size after the fact than do pre-checking to see
|
||||
// if the current data would increase the size of the BTreeMap
|
||||
while self.store.len() > self.capacity {
|
||||
let oldest_stored_epoch = self.store.keys().next().cloned().unwrap();
|
||||
self.store.remove(&oldest_stored_epoch);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, checkpoint: &Checkpoint) -> Option<&Eth1FinalizationData> {
|
||||
match self.store.get(&checkpoint.epoch) {
|
||||
Some(vec) => {
|
||||
for (root, data) in vec {
|
||||
if *root == checkpoint.root {
|
||||
return Some(data);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.store.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// This cache stores `Eth1CacheData` that could potentially be finalized within 4
|
||||
/// future epochs.
|
||||
pub struct Eth1FinalizationCache {
|
||||
by_checkpoint: CheckpointMap,
|
||||
pending_eth1: BTreeMap<u64, Eth1Data>,
|
||||
last_finalized: Option<Eth1Data>,
|
||||
log: Logger,
|
||||
}
|
||||
|
||||
/// Provides a cache of `Eth1CacheData` at epoch boundaries. This is used to
|
||||
/// finalize deposits when a new epoch is finalized.
|
||||
///
|
||||
impl Eth1FinalizationCache {
|
||||
pub fn new(log: Logger) -> Self {
|
||||
Eth1FinalizationCache {
|
||||
by_checkpoint: CheckpointMap::new(),
|
||||
pending_eth1: BTreeMap::new(),
|
||||
last_finalized: None,
|
||||
log,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_capacity(log: Logger, capacity: usize) -> Self {
|
||||
Eth1FinalizationCache {
|
||||
by_checkpoint: CheckpointMap::with_capacity(capacity),
|
||||
pending_eth1: BTreeMap::new(),
|
||||
last_finalized: None,
|
||||
log,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, checkpoint: Checkpoint, eth1_finalization_data: Eth1FinalizationData) {
|
||||
if !eth1_finalization_data.fully_imported() {
|
||||
self.pending_eth1.insert(
|
||||
eth1_finalization_data.eth1_data.deposit_count,
|
||||
eth1_finalization_data.eth1_data.clone(),
|
||||
);
|
||||
debug!(
|
||||
self.log,
|
||||
"Eth1Cache: inserted pending eth1";
|
||||
"eth1_data.deposit_count" => eth1_finalization_data.eth1_data.deposit_count,
|
||||
"eth1_deposit_index" => eth1_finalization_data.eth1_deposit_index,
|
||||
);
|
||||
}
|
||||
self.by_checkpoint
|
||||
.insert(checkpoint, eth1_finalization_data);
|
||||
}
|
||||
|
||||
pub fn finalize(&mut self, checkpoint: &Checkpoint) -> Option<Eth1Data> {
|
||||
if let Some(eth1_finalized_data) = self.by_checkpoint.get(checkpoint) {
|
||||
let finalized_deposit_index = eth1_finalized_data.eth1_deposit_index;
|
||||
let mut result = None;
|
||||
while let Some(pending_count) = self.pending_eth1.keys().next().cloned() {
|
||||
if finalized_deposit_index >= pending_count {
|
||||
result = self.pending_eth1.remove(&pending_count);
|
||||
debug!(
|
||||
self.log,
|
||||
"Eth1Cache: dropped pending eth1";
|
||||
"pending_count" => pending_count,
|
||||
"finalized_deposit_index" => finalized_deposit_index,
|
||||
);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if eth1_finalized_data.fully_imported() {
|
||||
result = Some(eth1_finalized_data.eth1_data.clone())
|
||||
}
|
||||
if result.is_some() {
|
||||
self.last_finalized = result;
|
||||
}
|
||||
self.last_finalized.clone()
|
||||
} else {
|
||||
debug!(
|
||||
self.log,
|
||||
"Eth1Cache: cache miss";
|
||||
"epoch" => checkpoint.epoch,
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn by_checkpoint(&self) -> &CheckpointMap {
|
||||
&self.by_checkpoint
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn pending_eth1(&self) -> &BTreeMap<u64, Eth1Data> {
|
||||
&self.pending_eth1
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use sloggers::null::NullLoggerBuilder;
|
||||
use sloggers::Build;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const SLOTS_PER_EPOCH: u64 = 32;
|
||||
const MAX_DEPOSITS: u64 = 16;
|
||||
const EPOCHS_PER_ETH1_VOTING_PERIOD: u64 = 64;
|
||||
|
||||
fn eth1cache() -> Eth1FinalizationCache {
|
||||
let log_builder = NullLoggerBuilder;
|
||||
Eth1FinalizationCache::new(log_builder.build().expect("should build log"))
|
||||
}
|
||||
|
||||
fn random_eth1_data(deposit_count: u64) -> Eth1Data {
|
||||
Eth1Data {
|
||||
deposit_root: Root::random(),
|
||||
deposit_count,
|
||||
block_hash: Root::random(),
|
||||
}
|
||||
}
|
||||
|
||||
fn random_checkpoint(epoch: u64) -> Checkpoint {
|
||||
Checkpoint {
|
||||
epoch: epoch.into(),
|
||||
root: Root::random(),
|
||||
}
|
||||
}
|
||||
|
||||
fn random_checkpoints(n: usize) -> Vec<Checkpoint> {
|
||||
let mut result = Vec::with_capacity(n);
|
||||
for epoch in 0..n {
|
||||
result.push(random_checkpoint(epoch as u64))
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fully_imported_deposits() {
|
||||
let epochs = 16;
|
||||
let deposits_imported = 128;
|
||||
|
||||
let eth1data = random_eth1_data(deposits_imported);
|
||||
let checkpoints = random_checkpoints(epochs as usize);
|
||||
let mut eth1cache = eth1cache();
|
||||
|
||||
for epoch in 4..epochs {
|
||||
assert_eq!(
|
||||
eth1cache.by_checkpoint().len(),
|
||||
cmp::min((epoch - 4) as usize, DEFAULT_ETH1_CACHE_SIZE),
|
||||
"Unexpected cache size"
|
||||
);
|
||||
|
||||
let checkpoint = checkpoints
|
||||
.get(epoch as usize)
|
||||
.expect("should get checkpoint");
|
||||
eth1cache.insert(
|
||||
*checkpoint,
|
||||
Eth1FinalizationData {
|
||||
eth1_data: eth1data.clone(),
|
||||
eth1_deposit_index: deposits_imported,
|
||||
},
|
||||
);
|
||||
|
||||
let finalized_checkpoint = checkpoints
|
||||
.get((epoch - 4) as usize)
|
||||
.expect("should get finalized checkpoint");
|
||||
assert!(
|
||||
eth1cache.pending_eth1().is_empty(),
|
||||
"Deposits are fully imported so pending cache should be empty"
|
||||
);
|
||||
if epoch < 8 {
|
||||
assert_eq!(
|
||||
eth1cache.finalize(finalized_checkpoint),
|
||||
None,
|
||||
"Should have cache miss"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
eth1cache.finalize(finalized_checkpoint),
|
||||
Some(eth1data.clone()),
|
||||
"Should have cache hit"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partially_imported_deposits() {
|
||||
let epochs = 16;
|
||||
let initial_deposits_imported = 1024;
|
||||
let deposits_imported_per_epoch = MAX_DEPOSITS * SLOTS_PER_EPOCH;
|
||||
let full_import_epoch = 13;
|
||||
let total_deposits =
|
||||
initial_deposits_imported + deposits_imported_per_epoch * full_import_epoch;
|
||||
|
||||
let eth1data = random_eth1_data(total_deposits);
|
||||
let checkpoints = random_checkpoints(epochs as usize);
|
||||
let mut eth1cache = eth1cache();
|
||||
|
||||
for epoch in 0..epochs {
|
||||
assert_eq!(
|
||||
eth1cache.by_checkpoint().len(),
|
||||
cmp::min(epoch as usize, DEFAULT_ETH1_CACHE_SIZE),
|
||||
"Unexpected cache size"
|
||||
);
|
||||
|
||||
let checkpoint = checkpoints
|
||||
.get(epoch as usize)
|
||||
.expect("should get checkpoint");
|
||||
let deposits_imported = cmp::min(
|
||||
total_deposits,
|
||||
initial_deposits_imported + deposits_imported_per_epoch * epoch,
|
||||
);
|
||||
eth1cache.insert(
|
||||
*checkpoint,
|
||||
Eth1FinalizationData {
|
||||
eth1_data: eth1data.clone(),
|
||||
eth1_deposit_index: deposits_imported,
|
||||
},
|
||||
);
|
||||
|
||||
if epoch >= 4 {
|
||||
let finalized_epoch = epoch - 4;
|
||||
let finalized_checkpoint = checkpoints
|
||||
.get(finalized_epoch as usize)
|
||||
.expect("should get finalized checkpoint");
|
||||
if finalized_epoch < full_import_epoch {
|
||||
assert_eq!(
|
||||
eth1cache.finalize(finalized_checkpoint),
|
||||
None,
|
||||
"Deposits not fully finalized so cache should return no Eth1Data",
|
||||
);
|
||||
assert_eq!(
|
||||
eth1cache.pending_eth1().len(),
|
||||
1,
|
||||
"Deposits not fully finalized. Pending eth1 cache should have 1 entry"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
eth1cache.finalize(finalized_checkpoint),
|
||||
Some(eth1data.clone()),
|
||||
"Deposits fully imported and finalized. Cache should return Eth1Data. finalized_deposits[{}]",
|
||||
(initial_deposits_imported + deposits_imported_per_epoch * finalized_epoch),
|
||||
);
|
||||
assert!(
|
||||
eth1cache.pending_eth1().is_empty(),
|
||||
"Deposits fully imported and finalized. Pending cache should be empty"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fork_at_epoch_boundary() {
|
||||
let epochs = 12;
|
||||
let deposits_imported = 128;
|
||||
|
||||
let eth1data = random_eth1_data(deposits_imported);
|
||||
let checkpoints = random_checkpoints(epochs as usize);
|
||||
let mut forks = HashMap::new();
|
||||
let mut eth1cache = eth1cache();
|
||||
|
||||
for epoch in 0..epochs {
|
||||
assert_eq!(
|
||||
eth1cache.by_checkpoint().len(),
|
||||
cmp::min(epoch as usize, DEFAULT_ETH1_CACHE_SIZE),
|
||||
"Unexpected cache size"
|
||||
);
|
||||
|
||||
let checkpoint = checkpoints
|
||||
.get(epoch as usize)
|
||||
.expect("should get checkpoint");
|
||||
eth1cache.insert(
|
||||
*checkpoint,
|
||||
Eth1FinalizationData {
|
||||
eth1_data: eth1data.clone(),
|
||||
eth1_deposit_index: deposits_imported,
|
||||
},
|
||||
);
|
||||
// lets put a fork at every third epoch
|
||||
if epoch % 3 == 0 {
|
||||
let fork = random_checkpoint(epoch);
|
||||
eth1cache.insert(
|
||||
fork,
|
||||
Eth1FinalizationData {
|
||||
eth1_data: eth1data.clone(),
|
||||
eth1_deposit_index: deposits_imported,
|
||||
},
|
||||
);
|
||||
forks.insert(epoch as usize, fork);
|
||||
}
|
||||
|
||||
assert!(
|
||||
eth1cache.pending_eth1().is_empty(),
|
||||
"Deposits are fully imported so pending cache should be empty"
|
||||
);
|
||||
if epoch >= 4 {
|
||||
let finalized_epoch = (epoch - 4) as usize;
|
||||
let finalized_checkpoint = if finalized_epoch % 3 == 0 {
|
||||
forks.get(&finalized_epoch).expect("should get fork")
|
||||
} else {
|
||||
checkpoints
|
||||
.get(finalized_epoch)
|
||||
.expect("should get checkpoint")
|
||||
};
|
||||
assert_eq!(
|
||||
eth1cache.finalize(finalized_checkpoint),
|
||||
Some(eth1data.clone()),
|
||||
"Should have cache hit"
|
||||
);
|
||||
if finalized_epoch >= 3 {
|
||||
let dropped_epoch = finalized_epoch - 3;
|
||||
if let Some(dropped_checkpoint) = forks.get(&dropped_epoch) {
|
||||
// got checkpoint for an old fork that should no longer
|
||||
// be in the cache because it is from too long ago
|
||||
assert_eq!(
|
||||
eth1cache.finalize(dropped_checkpoint),
|
||||
None,
|
||||
"Should have cache miss"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn massive_deposit_queue() {
|
||||
// Simulating a situation where deposits don't get imported within an eth1 voting period
|
||||
let eth1_voting_periods = 8;
|
||||
let initial_deposits_imported = 1024;
|
||||
let deposits_imported_per_epoch = MAX_DEPOSITS * SLOTS_PER_EPOCH;
|
||||
let initial_deposit_queue =
|
||||
deposits_imported_per_epoch * EPOCHS_PER_ETH1_VOTING_PERIOD * 2 + 32;
|
||||
let new_deposits_per_voting_period =
|
||||
EPOCHS_PER_ETH1_VOTING_PERIOD * deposits_imported_per_epoch / 2;
|
||||
|
||||
let mut epoch_data = BTreeMap::new();
|
||||
let mut eth1s_by_count = BTreeMap::new();
|
||||
let mut eth1cache = eth1cache();
|
||||
let mut last_period_deposits = initial_deposits_imported;
|
||||
for period in 0..eth1_voting_periods {
|
||||
let period_deposits = initial_deposits_imported
|
||||
+ initial_deposit_queue
|
||||
+ period * new_deposits_per_voting_period;
|
||||
let period_eth1_data = random_eth1_data(period_deposits);
|
||||
eth1s_by_count.insert(period_eth1_data.deposit_count, period_eth1_data.clone());
|
||||
|
||||
for epoch_mod_period in 0..EPOCHS_PER_ETH1_VOTING_PERIOD {
|
||||
let epoch = period * EPOCHS_PER_ETH1_VOTING_PERIOD + epoch_mod_period;
|
||||
let checkpoint = random_checkpoint(epoch);
|
||||
let deposits_imported = cmp::min(
|
||||
period_deposits,
|
||||
last_period_deposits + deposits_imported_per_epoch * epoch_mod_period,
|
||||
);
|
||||
eth1cache.insert(
|
||||
checkpoint,
|
||||
Eth1FinalizationData {
|
||||
eth1_data: period_eth1_data.clone(),
|
||||
eth1_deposit_index: deposits_imported,
|
||||
},
|
||||
);
|
||||
epoch_data.insert(epoch, (checkpoint, deposits_imported));
|
||||
|
||||
if epoch >= 4 {
|
||||
let finalized_epoch = epoch - 4;
|
||||
let (finalized_checkpoint, finalized_deposits) = epoch_data
|
||||
.get(&finalized_epoch)
|
||||
.expect("should get epoch data");
|
||||
|
||||
let pending_eth1s = eth1s_by_count.range((finalized_deposits + 1)..).count();
|
||||
let last_finalized_eth1 = eth1s_by_count
|
||||
.range(0..(finalized_deposits + 1))
|
||||
.map(|(_, eth1)| eth1)
|
||||
.last()
|
||||
.cloned();
|
||||
assert_eq!(
|
||||
eth1cache.finalize(finalized_checkpoint),
|
||||
last_finalized_eth1,
|
||||
"finalized checkpoint mismatch",
|
||||
);
|
||||
assert_eq!(
|
||||
eth1cache.pending_eth1().len(),
|
||||
pending_eth1s,
|
||||
"pending eth1 mismatch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// remove unneeded stuff from old epochs
|
||||
while epoch_data.len() > DEFAULT_ETH1_CACHE_SIZE {
|
||||
let oldest_stored_epoch = epoch_data
|
||||
.keys()
|
||||
.next()
|
||||
.cloned()
|
||||
.expect("should get oldest epoch");
|
||||
epoch_data.remove(&oldest_stored_epoch);
|
||||
}
|
||||
last_period_deposits = period_deposits;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ use proto_array::CountUnrealizedFull;
|
||||
use slog::{info, warn, Logger};
|
||||
use state_processing::state_advance::complete_state_advance;
|
||||
use state_processing::{
|
||||
per_block_processing, per_block_processing::BlockSignatureStrategy, VerifyBlockRoot,
|
||||
per_block_processing, per_block_processing::BlockSignatureStrategy, ConsensusContext,
|
||||
VerifyBlockRoot,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -172,12 +173,14 @@ pub fn reset_fork_choice_to_finalization<E: EthSpec, Hot: ItemStore<E>, Cold: It
|
||||
complete_state_advance(&mut state, None, block.slot(), spec)
|
||||
.map_err(|e| format!("State advance failed: {:?}", e))?;
|
||||
|
||||
let mut ctxt = ConsensusContext::new(block.slot())
|
||||
.set_proposer_index(block.message().proposer_index());
|
||||
per_block_processing(
|
||||
&mut state,
|
||||
&block,
|
||||
None,
|
||||
BlockSignatureStrategy::NoVerification,
|
||||
VerifyBlockRoot::True,
|
||||
&mut ctxt,
|
||||
spec,
|
||||
)
|
||||
.map_err(|e| format!("Error replaying block: {:?}", e))?;
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod chain_config;
|
||||
mod early_attester_cache;
|
||||
mod errors;
|
||||
pub mod eth1_chain;
|
||||
mod eth1_finalization_cache;
|
||||
pub mod events;
|
||||
pub mod execution_payload;
|
||||
pub mod fork_choice_signal;
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
mod migration_schema_v10;
|
||||
mod migration_schema_v11;
|
||||
mod migration_schema_v12;
|
||||
mod migration_schema_v13;
|
||||
mod migration_schema_v6;
|
||||
mod migration_schema_v7;
|
||||
mod migration_schema_v8;
|
||||
mod migration_schema_v9;
|
||||
mod types;
|
||||
|
||||
use crate::beacon_chain::{BeaconChainTypes, FORK_CHOICE_DB_KEY};
|
||||
use crate::beacon_chain::{BeaconChainTypes, ETH1_CACHE_DB_KEY, FORK_CHOICE_DB_KEY};
|
||||
use crate::eth1_chain::SszEth1;
|
||||
use crate::persisted_fork_choice::{
|
||||
PersistedForkChoiceV1, PersistedForkChoiceV10, PersistedForkChoiceV11, PersistedForkChoiceV7,
|
||||
PersistedForkChoiceV8,
|
||||
@@ -24,6 +26,7 @@ use store::{Error as StoreError, StoreItem};
|
||||
/// Migrate the database from one schema version to another, applying all requisite mutations.
|
||||
pub fn migrate_schema<T: BeaconChainTypes>(
|
||||
db: Arc<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>,
|
||||
deposit_contract_deploy_block: u64,
|
||||
datadir: &Path,
|
||||
from: SchemaVersion,
|
||||
to: SchemaVersion,
|
||||
@@ -31,19 +34,51 @@ pub fn migrate_schema<T: BeaconChainTypes>(
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(), StoreError> {
|
||||
match (from, to) {
|
||||
// Migrating from the current schema version to iself is always OK, a no-op.
|
||||
// Migrating from the current schema version to itself is always OK, a no-op.
|
||||
(_, _) if from == to && to == CURRENT_SCHEMA_VERSION => Ok(()),
|
||||
// Upgrade across multiple versions by recursively migrating one step at a time.
|
||||
(_, _) if from.as_u64() + 1 < to.as_u64() => {
|
||||
let next = SchemaVersion(from.as_u64() + 1);
|
||||
migrate_schema::<T>(db.clone(), datadir, from, next, log.clone(), spec)?;
|
||||
migrate_schema::<T>(db, datadir, next, to, log, spec)
|
||||
migrate_schema::<T>(
|
||||
db.clone(),
|
||||
deposit_contract_deploy_block,
|
||||
datadir,
|
||||
from,
|
||||
next,
|
||||
log.clone(),
|
||||
spec,
|
||||
)?;
|
||||
migrate_schema::<T>(
|
||||
db,
|
||||
deposit_contract_deploy_block,
|
||||
datadir,
|
||||
next,
|
||||
to,
|
||||
log,
|
||||
spec,
|
||||
)
|
||||
}
|
||||
// Downgrade across multiple versions by recursively migrating one step at a time.
|
||||
(_, _) if to.as_u64() + 1 < from.as_u64() => {
|
||||
let next = SchemaVersion(from.as_u64() - 1);
|
||||
migrate_schema::<T>(db.clone(), datadir, from, next, log.clone(), spec)?;
|
||||
migrate_schema::<T>(db, datadir, next, to, log, spec)
|
||||
migrate_schema::<T>(
|
||||
db.clone(),
|
||||
deposit_contract_deploy_block,
|
||||
datadir,
|
||||
from,
|
||||
next,
|
||||
log.clone(),
|
||||
spec,
|
||||
)?;
|
||||
migrate_schema::<T>(
|
||||
db,
|
||||
deposit_contract_deploy_block,
|
||||
datadir,
|
||||
next,
|
||||
to,
|
||||
log,
|
||||
spec,
|
||||
)
|
||||
}
|
||||
|
||||
//
|
||||
@@ -207,6 +242,55 @@ pub fn migrate_schema<T: BeaconChainTypes>(
|
||||
let ops = migration_schema_v12::downgrade_from_v12::<T>(db.clone(), log)?;
|
||||
db.store_schema_version_atomically(to, ops)
|
||||
}
|
||||
(SchemaVersion(12), SchemaVersion(13)) => {
|
||||
let mut ops = vec![];
|
||||
if let Some(persisted_eth1_v1) = db.get_item::<SszEth1>(Ð1_CACHE_DB_KEY)? {
|
||||
let upgraded_eth1_cache =
|
||||
match migration_schema_v13::update_eth1_cache(persisted_eth1_v1) {
|
||||
Ok(upgraded_eth1) => upgraded_eth1,
|
||||
Err(e) => {
|
||||
warn!(log, "Failed to deserialize SszEth1CacheV1"; "error" => ?e);
|
||||
warn!(log, "Reinitializing eth1 cache");
|
||||
migration_schema_v13::reinitialized_eth1_cache_v13(
|
||||
deposit_contract_deploy_block,
|
||||
)
|
||||
}
|
||||
};
|
||||
ops.push(upgraded_eth1_cache.as_kv_store_op(ETH1_CACHE_DB_KEY));
|
||||
}
|
||||
|
||||
db.store_schema_version_atomically(to, ops)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
(SchemaVersion(13), SchemaVersion(12)) => {
|
||||
let mut ops = vec![];
|
||||
if let Some(persisted_eth1_v13) = db.get_item::<SszEth1>(Ð1_CACHE_DB_KEY)? {
|
||||
let downgraded_eth1_cache = match migration_schema_v13::downgrade_eth1_cache(
|
||||
persisted_eth1_v13,
|
||||
) {
|
||||
Ok(Some(downgraded_eth1)) => downgraded_eth1,
|
||||
Ok(None) => {
|
||||
warn!(log, "Unable to downgrade eth1 cache from newer version: reinitializing eth1 cache");
|
||||
migration_schema_v13::reinitialized_eth1_cache_v1(
|
||||
deposit_contract_deploy_block,
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(log, "Unable to downgrade eth1 cache from newer version: failed to deserialize SszEth1CacheV13"; "error" => ?e);
|
||||
warn!(log, "Reinitializing eth1 cache");
|
||||
migration_schema_v13::reinitialized_eth1_cache_v1(
|
||||
deposit_contract_deploy_block,
|
||||
)
|
||||
}
|
||||
};
|
||||
ops.push(downgraded_eth1_cache.as_kv_store_op(ETH1_CACHE_DB_KEY));
|
||||
}
|
||||
|
||||
db.store_schema_version_atomically(to, ops)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// Anything else is an error.
|
||||
(_, _) => Err(HotColdDBError::UnsupportedSchemaVersion {
|
||||
target_version: to,
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
use crate::eth1_chain::SszEth1;
|
||||
use eth1::{BlockCache, SszDepositCacheV1, SszDepositCacheV13, SszEth1CacheV1, SszEth1CacheV13};
|
||||
use ssz::{Decode, Encode};
|
||||
use state_processing::common::DepositDataTree;
|
||||
use store::Error;
|
||||
use types::DEPOSIT_TREE_DEPTH;
|
||||
|
||||
pub fn update_eth1_cache(persisted_eth1_v1: SszEth1) -> Result<SszEth1, Error> {
|
||||
if persisted_eth1_v1.use_dummy_backend {
|
||||
// backend_bytes is empty when using dummy backend
|
||||
return Ok(persisted_eth1_v1);
|
||||
}
|
||||
|
||||
let SszEth1 {
|
||||
use_dummy_backend,
|
||||
backend_bytes,
|
||||
} = persisted_eth1_v1;
|
||||
|
||||
let ssz_eth1_cache_v1 = SszEth1CacheV1::from_ssz_bytes(&backend_bytes)?;
|
||||
let SszEth1CacheV1 {
|
||||
block_cache,
|
||||
deposit_cache: deposit_cache_v1,
|
||||
last_processed_block,
|
||||
} = ssz_eth1_cache_v1;
|
||||
|
||||
let SszDepositCacheV1 {
|
||||
logs,
|
||||
leaves,
|
||||
deposit_contract_deploy_block,
|
||||
deposit_roots,
|
||||
} = deposit_cache_v1;
|
||||
|
||||
let deposit_cache_v13 = SszDepositCacheV13 {
|
||||
logs,
|
||||
leaves,
|
||||
deposit_contract_deploy_block,
|
||||
finalized_deposit_count: 0,
|
||||
finalized_block_height: deposit_contract_deploy_block.saturating_sub(1),
|
||||
deposit_tree_snapshot: None,
|
||||
deposit_roots,
|
||||
};
|
||||
|
||||
let ssz_eth1_cache_v13 = SszEth1CacheV13 {
|
||||
block_cache,
|
||||
deposit_cache: deposit_cache_v13,
|
||||
last_processed_block,
|
||||
};
|
||||
|
||||
let persisted_eth1_v13 = SszEth1 {
|
||||
use_dummy_backend,
|
||||
backend_bytes: ssz_eth1_cache_v13.as_ssz_bytes(),
|
||||
};
|
||||
|
||||
Ok(persisted_eth1_v13)
|
||||
}
|
||||
|
||||
pub fn downgrade_eth1_cache(persisted_eth1_v13: SszEth1) -> Result<Option<SszEth1>, Error> {
|
||||
if persisted_eth1_v13.use_dummy_backend {
|
||||
// backend_bytes is empty when using dummy backend
|
||||
return Ok(Some(persisted_eth1_v13));
|
||||
}
|
||||
|
||||
let SszEth1 {
|
||||
use_dummy_backend,
|
||||
backend_bytes,
|
||||
} = persisted_eth1_v13;
|
||||
|
||||
let ssz_eth1_cache_v13 = SszEth1CacheV13::from_ssz_bytes(&backend_bytes)?;
|
||||
let SszEth1CacheV13 {
|
||||
block_cache,
|
||||
deposit_cache: deposit_cache_v13,
|
||||
last_processed_block,
|
||||
} = ssz_eth1_cache_v13;
|
||||
|
||||
let SszDepositCacheV13 {
|
||||
logs,
|
||||
leaves,
|
||||
deposit_contract_deploy_block,
|
||||
finalized_deposit_count,
|
||||
finalized_block_height: _,
|
||||
deposit_tree_snapshot,
|
||||
deposit_roots,
|
||||
} = deposit_cache_v13;
|
||||
|
||||
if finalized_deposit_count == 0 && deposit_tree_snapshot.is_none() {
|
||||
// This tree was never finalized and can be directly downgraded to v1 without re-initializing
|
||||
let deposit_cache_v1 = SszDepositCacheV1 {
|
||||
logs,
|
||||
leaves,
|
||||
deposit_contract_deploy_block,
|
||||
deposit_roots,
|
||||
};
|
||||
let ssz_eth1_cache_v1 = SszEth1CacheV1 {
|
||||
block_cache,
|
||||
deposit_cache: deposit_cache_v1,
|
||||
last_processed_block,
|
||||
};
|
||||
return Ok(Some(SszEth1 {
|
||||
use_dummy_backend,
|
||||
backend_bytes: ssz_eth1_cache_v1.as_ssz_bytes(),
|
||||
}));
|
||||
}
|
||||
// deposit cache was finalized; can't downgrade
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn reinitialized_eth1_cache_v13(deposit_contract_deploy_block: u64) -> SszEth1 {
|
||||
let empty_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH);
|
||||
let deposit_cache_v13 = SszDepositCacheV13 {
|
||||
logs: vec![],
|
||||
leaves: vec![],
|
||||
deposit_contract_deploy_block,
|
||||
finalized_deposit_count: 0,
|
||||
finalized_block_height: deposit_contract_deploy_block.saturating_sub(1),
|
||||
deposit_tree_snapshot: empty_tree.get_snapshot(),
|
||||
deposit_roots: vec![empty_tree.root()],
|
||||
};
|
||||
|
||||
let ssz_eth1_cache_v13 = SszEth1CacheV13 {
|
||||
block_cache: BlockCache::default(),
|
||||
deposit_cache: deposit_cache_v13,
|
||||
last_processed_block: None,
|
||||
};
|
||||
|
||||
SszEth1 {
|
||||
use_dummy_backend: false,
|
||||
backend_bytes: ssz_eth1_cache_v13.as_ssz_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reinitialized_eth1_cache_v1(deposit_contract_deploy_block: u64) -> SszEth1 {
|
||||
let empty_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH);
|
||||
let deposit_cache_v1 = SszDepositCacheV1 {
|
||||
logs: vec![],
|
||||
leaves: vec![],
|
||||
deposit_contract_deploy_block,
|
||||
deposit_roots: vec![empty_tree.root()],
|
||||
};
|
||||
|
||||
let ssz_eth1_cache_v1 = SszEth1CacheV1 {
|
||||
block_cache: BlockCache::default(),
|
||||
deposit_cache: deposit_cache_v1,
|
||||
last_processed_block: None,
|
||||
};
|
||||
|
||||
SszEth1 {
|
||||
use_dummy_backend: false,
|
||||
backend_bytes: ssz_eth1_cache_v1.as_ssz_bytes(),
|
||||
}
|
||||
}
|
||||
@@ -1432,8 +1432,9 @@ where
|
||||
// Building proofs
|
||||
let mut proofs = vec![];
|
||||
for i in 0..leaves.len() {
|
||||
let (_, mut proof) =
|
||||
tree.generate_proof(i, self.spec.deposit_contract_tree_depth as usize);
|
||||
let (_, mut proof) = tree
|
||||
.generate_proof(i, self.spec.deposit_contract_tree_depth as usize)
|
||||
.expect("should generate proof");
|
||||
proof.push(Hash256::from_slice(&int_to_bytes32(leaves.len() as u64)));
|
||||
proofs.push(proof);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user