mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-17 11:52:42 +00:00
Merge branch 'master' into proto-array + more changes
This commit is contained in:
@@ -7,8 +7,8 @@ use crate::fork_choice::{Error as ForkChoiceError, ForkChoice};
|
||||
use crate::head_tracker::HeadTracker;
|
||||
use crate::metrics;
|
||||
use crate::persisted_beacon_chain::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY};
|
||||
use crate::timeout_rw_lock::TimeoutRwLock;
|
||||
use operation_pool::{OperationPool, PersistedOperationPool};
|
||||
use parking_lot::RwLock;
|
||||
use slog::{debug, error, info, trace, warn, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use ssz::Encode;
|
||||
@@ -22,6 +22,7 @@ use state_processing::per_block_processing::{
|
||||
use state_processing::{
|
||||
per_block_processing, per_slot_processing, BlockProcessingError, BlockSignatureStrategy,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::fs;
|
||||
use std::io::prelude::*;
|
||||
use std::sync::Arc;
|
||||
@@ -29,7 +30,7 @@ use std::time::{Duration, Instant};
|
||||
use store::iter::{
|
||||
BlockRootsIterator, ReverseBlockRootIterator, ReverseStateRootIterator, StateRootsIterator,
|
||||
};
|
||||
use store::{Error as DBError, Migrate, Store};
|
||||
use store::{BlockRootTree, Error as DBError, Migrate, Store};
|
||||
use tree_hash::TreeHash;
|
||||
use types::*;
|
||||
|
||||
@@ -37,7 +38,7 @@ use types::*;
|
||||
// Must be 32-bytes or panic.
|
||||
//
|
||||
// |-------must be this long------|
|
||||
pub const GRAFFITI: &str = "sigp/lighthouse-0.1.0-prerelease";
|
||||
pub const GRAFFITI: &str = "sigp/lighthouse-0.1.1-prerelease";
|
||||
|
||||
/// If true, everytime a block is processed the pre-state, post-state and block are written to SSZ
|
||||
/// files in the temp directory.
|
||||
@@ -48,6 +49,10 @@ const WRITE_BLOCK_PROCESSING_SSZ: bool = cfg!(feature = "write_ssz_files");
|
||||
/// Maximum block slot number. Block with slots bigger than this constant will NOT be processed.
|
||||
const MAXIMUM_BLOCK_SLOT_NUMBER: u64 = 4_294_967_296; // 2^32
|
||||
|
||||
/// The time-out before failure during an operation to take a read/write RwLock on the canonical
|
||||
/// head.
|
||||
const HEAD_LOCK_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum BlockProcessingOutcome {
|
||||
/// Block was valid and imported into the block graph.
|
||||
@@ -102,6 +107,7 @@ pub struct HeadInfo {
|
||||
pub block_root: Hash256,
|
||||
pub state_root: Hash256,
|
||||
pub finalized_checkpoint: types::Checkpoint,
|
||||
pub fork: Fork,
|
||||
}
|
||||
|
||||
pub trait BeaconChainTypes: Send + Sync + 'static {
|
||||
@@ -129,7 +135,7 @@ pub struct BeaconChain<T: BeaconChainTypes> {
|
||||
/// Provides information from the Ethereum 1 (PoW) chain.
|
||||
pub eth1_chain: Option<Eth1Chain<T::Eth1Chain, T::EthSpec>>,
|
||||
/// Stores a "snapshot" of the chain at the time the head-of-the-chain block was received.
|
||||
pub(crate) canonical_head: RwLock<CheckPoint<T::EthSpec>>,
|
||||
pub(crate) canonical_head: TimeoutRwLock<CheckPoint<T::EthSpec>>,
|
||||
/// The root of the genesis block.
|
||||
pub genesis_block_root: Hash256,
|
||||
/// A state-machine that is updated with information from the network and chooses a canonical
|
||||
@@ -141,6 +147,8 @@ pub struct BeaconChain<T: BeaconChainTypes> {
|
||||
pub(crate) head_tracker: HeadTracker,
|
||||
/// Provides a small cache of `BeaconState` and `BeaconBlock`.
|
||||
pub(crate) checkpoint_cache: CheckPointCache<T::EthSpec>,
|
||||
/// Cache of block roots for all known forks post-finalization.
|
||||
pub block_root_tree: Arc<BlockRootTree>,
|
||||
/// Logging to CLI, etc.
|
||||
pub(crate) log: Logger,
|
||||
}
|
||||
@@ -152,7 +160,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
pub fn persist(&self) -> Result<(), Error> {
|
||||
let timer = metrics::start_timer(&metrics::PERSIST_CHAIN);
|
||||
|
||||
let canonical_head = self.head();
|
||||
let canonical_head = self.head()?;
|
||||
|
||||
let finalized_checkpoint = {
|
||||
let beacon_block_root = canonical_head.beacon_state.finalized_checkpoint.root;
|
||||
@@ -162,8 +170,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
.ok_or_else(|| Error::MissingBeaconBlock(beacon_block_root))?;
|
||||
let beacon_state_root = beacon_block.state_root;
|
||||
let beacon_state = self
|
||||
.store
|
||||
.get_state(&beacon_state_root, Some(beacon_block.slot))?
|
||||
.get_state_caching(&beacon_state_root, Some(beacon_block.slot))?
|
||||
.ok_or_else(|| Error::MissingBeaconState(beacon_state_root))?;
|
||||
|
||||
CheckPoint {
|
||||
@@ -181,6 +188,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
genesis_block_root: self.genesis_block_root,
|
||||
ssz_head_tracker: self.head_tracker.to_ssz_container(),
|
||||
fork_choice: self.fork_choice.as_ssz_container(),
|
||||
block_root_tree: self.block_root_tree.as_ssz_container(),
|
||||
};
|
||||
|
||||
let key = Hash256::from_slice(&BEACON_CHAIN_DB_KEY.as_bytes());
|
||||
@@ -254,26 +262,32 @@ 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) -> ReverseBlockRootIterator<T::EthSpec, T::Store> {
|
||||
let head = self.head();
|
||||
pub fn rev_iter_block_roots(
|
||||
&self,
|
||||
) -> Result<ReverseBlockRootIterator<T::EthSpec, T::Store>, Error> {
|
||||
let head = self.head()?;
|
||||
|
||||
let iter = BlockRootsIterator::owned(self.store.clone(), head.beacon_state);
|
||||
|
||||
ReverseBlockRootIterator::new((head.beacon_block_root, head.beacon_block.slot), iter)
|
||||
Ok(ReverseBlockRootIterator::new(
|
||||
(head.beacon_block_root, head.beacon_block.slot),
|
||||
iter,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn forwards_iter_block_roots(
|
||||
&self,
|
||||
start_slot: Slot,
|
||||
) -> <T::Store as Store<T::EthSpec>>::ForwardsBlockRootsIterator {
|
||||
let local_head = self.head();
|
||||
T::Store::forwards_block_roots_iterator(
|
||||
) -> Result<<T::Store as Store<T::EthSpec>>::ForwardsBlockRootsIterator, Error> {
|
||||
let local_head = self.head()?;
|
||||
|
||||
Ok(T::Store::forwards_block_roots_iterator(
|
||||
self.store.clone(),
|
||||
start_slot,
|
||||
local_head.beacon_state,
|
||||
local_head.beacon_block_root,
|
||||
&self.spec,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
/// Traverse backwards from `block_root` to find the block roots of its ancestors.
|
||||
@@ -323,13 +337,18 @@ 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) -> ReverseStateRootIterator<T::EthSpec, T::Store> {
|
||||
let head = self.head();
|
||||
pub fn rev_iter_state_roots(
|
||||
&self,
|
||||
) -> Result<ReverseStateRootIterator<T::EthSpec, T::Store>, Error> {
|
||||
let head = self.head()?;
|
||||
let slot = head.beacon_state.slot;
|
||||
|
||||
let iter = StateRootsIterator::owned(self.store.clone(), head.beacon_state);
|
||||
|
||||
ReverseStateRootIterator::new((head.beacon_state_root, slot), iter)
|
||||
Ok(ReverseStateRootIterator::new(
|
||||
(head.beacon_state_root, slot),
|
||||
iter,
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns the block at the given root, if any.
|
||||
@@ -351,7 +370,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// May return a database error.
|
||||
pub fn block_at_slot(&self, slot: Slot) -> Result<Option<BeaconBlock<T::EthSpec>>, Error> {
|
||||
let root = self
|
||||
.rev_iter_block_roots()
|
||||
.rev_iter_block_roots()?
|
||||
.find(|(_, this_slot)| *this_slot == slot)
|
||||
.map(|(root, _)| root);
|
||||
|
||||
@@ -392,7 +411,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// ## Errors
|
||||
///
|
||||
/// May return a database error.
|
||||
fn get_block_caching(
|
||||
pub fn get_block_caching(
|
||||
&self,
|
||||
block_root: &Hash256,
|
||||
) -> Result<Option<BeaconBlock<T::EthSpec>>, Error> {
|
||||
@@ -408,7 +427,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// ## Errors
|
||||
///
|
||||
/// May return a database error.
|
||||
fn get_state_caching(
|
||||
pub fn get_state_caching(
|
||||
&self,
|
||||
state_root: &Hash256,
|
||||
slot: Option<Slot>,
|
||||
@@ -429,7 +448,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// ## Errors
|
||||
///
|
||||
/// May return a database error.
|
||||
fn get_state_caching_only_with_committee_caches(
|
||||
pub fn get_state_caching_only_with_committee_caches(
|
||||
&self,
|
||||
state_root: &Hash256,
|
||||
slot: Option<Slot>,
|
||||
@@ -450,22 +469,29 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// It is important to note that the `beacon_state` returned may not match the present slot. It
|
||||
/// is the state as it was when the head block was received, which could be some slots prior to
|
||||
/// now.
|
||||
pub fn head(&self) -> CheckPoint<T::EthSpec> {
|
||||
self.canonical_head.read().clone()
|
||||
pub fn head(&self) -> Result<CheckPoint<T::EthSpec>, Error> {
|
||||
self.canonical_head
|
||||
.try_read_for(HEAD_LOCK_TIMEOUT)
|
||||
.ok_or_else(|| Error::CanonicalHeadLockTimeout)
|
||||
.map(|v| v.clone())
|
||||
}
|
||||
|
||||
/// Returns info representing the head block and state.
|
||||
///
|
||||
/// A summarized version of `Self::head` that involves less cloning.
|
||||
pub fn head_info(&self) -> HeadInfo {
|
||||
let head = self.canonical_head.read();
|
||||
pub fn head_info(&self) -> Result<HeadInfo, Error> {
|
||||
let head = self
|
||||
.canonical_head
|
||||
.try_read_for(HEAD_LOCK_TIMEOUT)
|
||||
.ok_or_else(|| Error::CanonicalHeadLockTimeout)?;
|
||||
|
||||
HeadInfo {
|
||||
Ok(HeadInfo {
|
||||
slot: head.beacon_block.slot,
|
||||
block_root: head.beacon_block_root,
|
||||
state_root: head.beacon_state_root,
|
||||
finalized_checkpoint: head.beacon_state.finalized_checkpoint.clone(),
|
||||
}
|
||||
fork: head.beacon_state.fork.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the current heads of the `BeaconChain`. For the canonical head, see `Self::head`.
|
||||
@@ -480,7 +506,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// Returns `None` when the state is not found in the database or there is an error skipping
|
||||
/// to a future state.
|
||||
pub fn state_at_slot(&self, slot: Slot) -> Result<BeaconState<T::EthSpec>, Error> {
|
||||
let head_state = self.head().beacon_state;
|
||||
let head_state = self.head()?.beacon_state;
|
||||
|
||||
if slot == head_state.slot {
|
||||
Ok(head_state)
|
||||
@@ -532,7 +558,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
Ok(state)
|
||||
} else {
|
||||
let state_root = self
|
||||
.rev_iter_state_roots()
|
||||
.rev_iter_state_roots()?
|
||||
.take_while(|(_root, current_slot)| *current_slot >= slot)
|
||||
.find(|(_root, current_slot)| *current_slot == slot)
|
||||
.map(|(root, _slot)| root)
|
||||
@@ -557,29 +583,33 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
|
||||
/// Returns the slot of the highest block in the canonical chain.
|
||||
pub fn best_slot(&self) -> Slot {
|
||||
self.canonical_head.read().beacon_block.slot
|
||||
pub fn best_slot(&self) -> Result<Slot, Error> {
|
||||
self.canonical_head
|
||||
.try_read_for(HEAD_LOCK_TIMEOUT)
|
||||
.map(|head| head.beacon_block.slot)
|
||||
.ok_or_else(|| Error::CanonicalHeadLockTimeout)
|
||||
}
|
||||
|
||||
/// Returns the validator index (if any) for the given public key.
|
||||
///
|
||||
/// Information is retrieved from the present `beacon_state.validators`.
|
||||
pub fn validator_index(&self, pubkey: &PublicKeyBytes) -> Option<usize> {
|
||||
for (i, validator) in self.head().beacon_state.validators.iter().enumerate() {
|
||||
pub fn validator_index(&self, pubkey: &PublicKeyBytes) -> Result<Option<usize>, Error> {
|
||||
for (i, validator) in self.head()?.beacon_state.validators.iter().enumerate() {
|
||||
if validator.pubkey == *pubkey {
|
||||
return Some(i);
|
||||
return Ok(Some(i));
|
||||
}
|
||||
}
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the block canonical root of the current canonical chain at a given slot.
|
||||
///
|
||||
/// Returns None if a block doesn't exist at the slot.
|
||||
pub fn root_at_slot(&self, target_slot: Slot) -> Option<Hash256> {
|
||||
self.rev_iter_block_roots()
|
||||
pub fn root_at_slot(&self, target_slot: Slot) -> Result<Option<Hash256>, Error> {
|
||||
Ok(self
|
||||
.rev_iter_block_roots()?
|
||||
.find(|(_root, slot)| *slot == target_slot)
|
||||
.map(|(root, _slot)| root)
|
||||
.map(|(root, _slot)| root))
|
||||
}
|
||||
|
||||
/// Reads the slot clock (see `self.read_slot_clock()` and returns the number of slots since
|
||||
@@ -601,10 +631,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// present epoch is available.
|
||||
pub fn block_proposer(&self, slot: Slot) -> Result<usize, Error> {
|
||||
let epoch = |slot: Slot| slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
let head_state = &self.head().beacon_state;
|
||||
let head_state = &self.head()?.beacon_state;
|
||||
|
||||
let mut state = if epoch(slot) == epoch(head_state.slot) {
|
||||
self.head().beacon_state.clone()
|
||||
self.head()?.beacon_state.clone()
|
||||
} else {
|
||||
self.state_at_slot(slot)?
|
||||
};
|
||||
@@ -634,10 +664,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
epoch: Epoch,
|
||||
) -> Result<Option<(Slot, u64)>, Error> {
|
||||
let as_epoch = |slot: Slot| slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
let head_state = &self.head().beacon_state;
|
||||
let head_state = &self.head()?.beacon_state;
|
||||
|
||||
let mut state = if epoch == as_epoch(head_state.slot) {
|
||||
self.head().beacon_state.clone()
|
||||
self.head()?.beacon_state.clone()
|
||||
} else {
|
||||
self.state_at_slot(epoch.start_slot(T::EthSpec::slots_per_epoch()))?
|
||||
};
|
||||
@@ -670,7 +700,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
index: CommitteeIndex,
|
||||
) -> Result<Attestation<T::EthSpec>, Error> {
|
||||
let state = self.state_at_slot(slot)?;
|
||||
let head = self.head();
|
||||
let head = self.head()?;
|
||||
|
||||
let data = self.produce_attestation_data_for_block(
|
||||
index,
|
||||
@@ -697,7 +727,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
index: CommitteeIndex,
|
||||
) -> Result<AttestationData, Error> {
|
||||
let state = self.state_at_slot(slot)?;
|
||||
let head = self.head();
|
||||
let head = self.head()?;
|
||||
|
||||
self.produce_attestation_data_for_block(
|
||||
index,
|
||||
@@ -858,37 +888,40 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let result = if let Some(attestation_head_block) =
|
||||
self.get_block_caching(&attestation.data.beacon_block_root)?
|
||||
{
|
||||
// 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
|
||||
.get_state_caching_only_with_committee_caches(
|
||||
&attestation_head_block.state_root,
|
||||
Some(attestation_head_block.slot),
|
||||
)?
|
||||
.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()
|
||||
// If the attestation points to a block in the same epoch in which it was made,
|
||||
// then it is sufficient to load the state from that epoch's boundary, because
|
||||
// the epoch-variable fields like the justified checkpoints cannot have changed
|
||||
// between the epoch boundary and when the attestation was made. If conversely,
|
||||
// the attestation points to a block in a prior epoch, then it is necessary to
|
||||
// load the full state corresponding to its block, and transition it to the
|
||||
// attestation's epoch.
|
||||
let attestation_epoch = attestation.data.target.epoch;
|
||||
let slots_per_epoch = T::EthSpec::slots_per_epoch();
|
||||
let mut state = if attestation_epoch
|
||||
== attestation_head_block.slot.epoch(slots_per_epoch)
|
||||
{
|
||||
per_slot_processing(&mut state, None, &self.spec)?;
|
||||
}
|
||||
self.store
|
||||
.load_epoch_boundary_state(&attestation_head_block.state_root)?
|
||||
.ok_or_else(|| Error::MissingBeaconState(attestation_head_block.state_root))?
|
||||
} else {
|
||||
let mut state = self
|
||||
.store
|
||||
.get_state(
|
||||
&attestation_head_block.state_root,
|
||||
Some(attestation_head_block.slot),
|
||||
)?
|
||||
.ok_or_else(|| Error::MissingBeaconState(attestation_head_block.state_root))?;
|
||||
|
||||
// Fastforward the state to the epoch in which the attestation was made.
|
||||
// NOTE: this looks like a potential DoS vector, we should probably limit
|
||||
// the amount we're willing to fastforward without a valid signature.
|
||||
for _ in state.slot.as_u64()..attestation_epoch.start_slot(slots_per_epoch).as_u64()
|
||||
{
|
||||
per_slot_processing(&mut state, None, &self.spec)?;
|
||||
}
|
||||
|
||||
state
|
||||
};
|
||||
|
||||
state.build_committee_cache(RelativeEpoch::Current, &self.spec)?;
|
||||
|
||||
@@ -913,7 +946,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
//
|
||||
// This is likely overly restrictive, we could store the attestation for later
|
||||
// processing.
|
||||
let head_epoch = self.head_info().slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
let head_epoch = self.head_info()?.slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
|
||||
// Only log a warning if our head is in a reasonable place to verify this attestation.
|
||||
@@ -975,7 +1008,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
// - 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_info().finalized_checkpoint.epoch,
|
||||
self.head_info()?.finalized_checkpoint.epoch,
|
||||
state.finalized_checkpoint.epoch,
|
||||
);
|
||||
|
||||
@@ -1172,7 +1205,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES);
|
||||
|
||||
let finalized_slot = self
|
||||
.head_info()
|
||||
.head_info()?
|
||||
.finalized_checkpoint
|
||||
.epoch
|
||||
.start_slot(T::EthSpec::slots_per_epoch());
|
||||
@@ -1211,14 +1244,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the block is already known. We know it is post-finalization, so it is
|
||||
// sufficient to check the block root tree.
|
||||
if self.block_root_tree.is_known_block_root(&block_root) {
|
||||
return Ok(BlockProcessingOutcome::BlockIsAlreadyKnown);
|
||||
}
|
||||
|
||||
// Records the time taken to load the block and state from the database during block
|
||||
// processing.
|
||||
let db_read_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_READ);
|
||||
|
||||
if self.store.exists::<BeaconBlock<T::EthSpec>>(&block_root)? {
|
||||
return Ok(BlockProcessingOutcome::BlockIsAlreadyKnown);
|
||||
}
|
||||
|
||||
// Load the blocks parent block from the database, returning invalid if that block is not
|
||||
// found.
|
||||
let parent_block: BeaconBlock<T::EthSpec> =
|
||||
@@ -1350,6 +1385,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
metrics::stop_timer(db_write_timer);
|
||||
|
||||
self.block_root_tree
|
||||
.add_block_root(block_root, block.parent_root, block.slot)?;
|
||||
|
||||
self.head_tracker.register_block(block_root, &block);
|
||||
|
||||
let fork_choice_register_timer =
|
||||
@@ -1380,12 +1418,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
//
|
||||
// A block that was just imported is likely to be referenced by the next block that we
|
||||
// import.
|
||||
self.checkpoint_cache.insert(&CheckPoint {
|
||||
self.checkpoint_cache.insert(Cow::Owned(CheckPoint {
|
||||
beacon_block_root: block_root,
|
||||
beacon_block: block,
|
||||
beacon_state_root: state_root,
|
||||
beacon_state: state,
|
||||
});
|
||||
}));
|
||||
|
||||
metrics::stop_timer(full_timer);
|
||||
|
||||
@@ -1514,7 +1552,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let beacon_block_root = self.fork_choice.find_head(&self)?;
|
||||
|
||||
// If a new head was chosen.
|
||||
let result = if beacon_block_root != self.head_info().block_root {
|
||||
let result = if beacon_block_root != self.head_info()?.block_root {
|
||||
metrics::inc_counter(&metrics::FORK_CHOICE_CHANGED_HEAD);
|
||||
|
||||
let beacon_block: BeaconBlock<T::EthSpec> = self
|
||||
@@ -1526,15 +1564,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
.get_state_caching(&beacon_state_root, Some(beacon_block.slot))?
|
||||
.ok_or_else(|| Error::MissingBeaconState(beacon_state_root))?;
|
||||
|
||||
let previous_slot = self.head_info().slot;
|
||||
let previous_slot = self.head_info()?.slot;
|
||||
let new_slot = beacon_block.slot;
|
||||
|
||||
// Note: this will declare a re-org if we skip `SLOTS_PER_HISTORICAL_ROOT` blocks
|
||||
// between calls to fork choice without swapping between chains. This seems like an
|
||||
// extreme-enough scenario that a warning is fine.
|
||||
let is_reorg = self.head_info().block_root
|
||||
let is_reorg = self.head_info()?.block_root
|
||||
!= beacon_state
|
||||
.get_block_root(self.head_info().slot)
|
||||
.get_block_root(self.head_info()?.slot)
|
||||
.map(|root| *root)
|
||||
.unwrap_or_else(|_| Hash256::random());
|
||||
|
||||
@@ -1544,7 +1582,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
warn!(
|
||||
self.log,
|
||||
"Beacon chain re-org";
|
||||
"previous_head" => format!("{}", self.head_info().block_root),
|
||||
"previous_head" => format!("{}", self.head_info()?.block_root),
|
||||
"previous_slot" => previous_slot,
|
||||
"new_head_parent" => format!("{}", beacon_block.parent_root),
|
||||
"new_head" => format!("{}", beacon_block_root),
|
||||
@@ -1563,7 +1601,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
);
|
||||
};
|
||||
|
||||
let old_finalized_epoch = self.head_info().finalized_checkpoint.epoch;
|
||||
let old_finalized_epoch = self.head_info()?.finalized_checkpoint.epoch;
|
||||
let new_finalized_epoch = beacon_state.finalized_checkpoint.epoch;
|
||||
let finalized_root = beacon_state.finalized_checkpoint.root;
|
||||
|
||||
@@ -1574,7 +1612,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
new_epoch: new_finalized_epoch,
|
||||
})
|
||||
} else {
|
||||
let previous_head_beacon_block_root = self.canonical_head.read().beacon_block_root;
|
||||
let previous_head_beacon_block_root = self
|
||||
.canonical_head
|
||||
.try_read_for(HEAD_LOCK_TIMEOUT)
|
||||
.ok_or_else(|| Error::CanonicalHeadLockTimeout)?
|
||||
.beacon_block_root;
|
||||
let current_head_beacon_block_root = beacon_block_root;
|
||||
|
||||
let mut new_head = CheckPoint {
|
||||
@@ -1591,11 +1633,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
// Store the head in the checkpoint cache.
|
||||
//
|
||||
// The head block is likely to be referenced by the next imported block.
|
||||
self.checkpoint_cache.insert(&new_head);
|
||||
self.checkpoint_cache.insert(Cow::Borrowed(&new_head));
|
||||
|
||||
// Update the checkpoint that stores the head of the chain at the time it received the
|
||||
// block.
|
||||
*self.canonical_head.write() = new_head;
|
||||
*self
|
||||
.canonical_head
|
||||
.try_write_for(HEAD_LOCK_TIMEOUT)
|
||||
.ok_or_else(|| Error::CanonicalHeadLockTimeout)? = new_head;
|
||||
|
||||
metrics::stop_timer(timer);
|
||||
|
||||
@@ -1653,8 +1698,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
.process_finalization(&finalized_block, finalized_block_root)?;
|
||||
|
||||
let finalized_state = self
|
||||
.store
|
||||
.get_state(&finalized_block.state_root, Some(finalized_block.slot))?
|
||||
.get_state_caching_only_with_committee_caches(
|
||||
&finalized_block.state_root,
|
||||
Some(finalized_block.slot),
|
||||
)?
|
||||
.ok_or_else(|| Error::MissingBeaconState(finalized_block.state_root))?;
|
||||
|
||||
self.op_pool.prune_all(&finalized_state, &self.spec);
|
||||
@@ -1667,6 +1714,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
max_finality_distance,
|
||||
);
|
||||
|
||||
// Prune in-memory block root tree.
|
||||
self.block_root_tree.prune_to(
|
||||
finalized_block_root,
|
||||
self.heads().into_iter().map(|(block_root, _)| block_root),
|
||||
);
|
||||
|
||||
let _ = self.event_handler.register(EventKind::BeaconFinalization {
|
||||
epoch: new_finalized_epoch,
|
||||
root: finalized_block_root,
|
||||
@@ -1691,10 +1744,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let mut dump = vec![];
|
||||
|
||||
let mut last_slot = CheckPoint {
|
||||
beacon_block: self.head().beacon_block.clone(),
|
||||
beacon_block_root: self.head().beacon_block_root,
|
||||
beacon_state: self.head().beacon_state.clone(),
|
||||
beacon_state_root: self.head().beacon_state_root,
|
||||
beacon_block: self.head()?.beacon_block.clone(),
|
||||
beacon_block_root: self.head()?.beacon_block_root,
|
||||
beacon_state: self.head()?.beacon_state.clone(),
|
||||
beacon_state_root: self.head()?.beacon_state_root,
|
||||
};
|
||||
|
||||
dump.push(last_slot.clone());
|
||||
|
||||
@@ -3,20 +3,20 @@ use crate::eth1_chain::CachingEth1Backend;
|
||||
use crate::events::NullEventHandler;
|
||||
use crate::head_tracker::HeadTracker;
|
||||
use crate::persisted_beacon_chain::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY};
|
||||
use crate::timeout_rw_lock::TimeoutRwLock;
|
||||
use crate::{
|
||||
BeaconChain, BeaconChainTypes, CheckPoint, Eth1Chain, Eth1ChainBackend, EventHandler,
|
||||
ForkChoice,
|
||||
};
|
||||
use eth1::Config as Eth1Config;
|
||||
use operation_pool::OperationPool;
|
||||
use parking_lot::RwLock;
|
||||
use proto_array_fork_choice::ProtoArrayForkChoice;
|
||||
use slog::{info, Logger};
|
||||
use slot_clock::{SlotClock, TestingSlotClock};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use store::Store;
|
||||
use store::{BlockRootTree, Store};
|
||||
use types::{BeaconBlock, BeaconState, ChainSpec, EthSpec, Hash256, Slot};
|
||||
|
||||
/// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing
|
||||
@@ -72,6 +72,7 @@ pub struct BeaconChainBuilder<T: BeaconChainTypes> {
|
||||
slot_clock: Option<T::SlotClock>,
|
||||
persisted_beacon_chain: Option<PersistedBeaconChain<T>>,
|
||||
head_tracker: Option<HeadTracker>,
|
||||
block_root_tree: Option<Arc<BlockRootTree>>,
|
||||
spec: ChainSpec,
|
||||
log: Option<Logger>,
|
||||
}
|
||||
@@ -105,6 +106,7 @@ where
|
||||
slot_clock: None,
|
||||
persisted_beacon_chain: None,
|
||||
head_tracker: None,
|
||||
block_root_tree: None,
|
||||
spec: TEthSpec::default_spec(),
|
||||
log: None,
|
||||
}
|
||||
@@ -187,6 +189,7 @@ where
|
||||
HeadTracker::from_ssz_container(&p.ssz_head_tracker)
|
||||
.map_err(|e| format!("Failed to decode head tracker for database: {:?}", e))?,
|
||||
);
|
||||
self.block_root_tree = Some(Arc::new(p.block_root_tree.clone().into()));
|
||||
self.persisted_beacon_chain = Some(p);
|
||||
|
||||
Ok(self)
|
||||
@@ -229,6 +232,11 @@ where
|
||||
)
|
||||
})?;
|
||||
|
||||
self.block_root_tree = Some(Arc::new(BlockRootTree::new(
|
||||
beacon_block_root,
|
||||
beacon_block.slot,
|
||||
)));
|
||||
|
||||
self.finalized_checkpoint = Some(CheckPoint {
|
||||
beacon_block_root,
|
||||
beacon_block,
|
||||
@@ -319,7 +327,7 @@ where
|
||||
.op_pool
|
||||
.ok_or_else(|| "Cannot build without op pool".to_string())?,
|
||||
eth1_chain: self.eth1_chain,
|
||||
canonical_head: RwLock::new(canonical_head),
|
||||
canonical_head: TimeoutRwLock::new(canonical_head),
|
||||
genesis_block_root: self
|
||||
.genesis_block_root
|
||||
.ok_or_else(|| "Cannot build without a genesis block root".to_string())?,
|
||||
@@ -330,16 +338,23 @@ where
|
||||
.event_handler
|
||||
.ok_or_else(|| "Cannot build without an event handler".to_string())?,
|
||||
head_tracker: self.head_tracker.unwrap_or_default(),
|
||||
block_root_tree: self
|
||||
.block_root_tree
|
||||
.ok_or_else(|| "Cannot build without a block root tree".to_string())?,
|
||||
checkpoint_cache: CheckPointCache::default(),
|
||||
log: log.clone(),
|
||||
};
|
||||
|
||||
let head = beacon_chain
|
||||
.head()
|
||||
.map_err(|e| format!("Failed to get head: {:?}", e))?;
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Beacon chain initialized";
|
||||
"head_state" => format!("{}", beacon_chain.head().beacon_state_root),
|
||||
"head_block" => format!("{}", beacon_chain.head().beacon_block_root),
|
||||
"head_slot" => format!("{}", beacon_chain.head().beacon_block.slot),
|
||||
"head_state" => format!("{}", head.beacon_state_root),
|
||||
"head_block" => format!("{}", head.beacon_block_root),
|
||||
"head_slot" => format!("{}", head.beacon_block.slot),
|
||||
);
|
||||
|
||||
Ok(beacon_chain)
|
||||
@@ -563,7 +578,8 @@ mod test {
|
||||
.build()
|
||||
.expect("should build");
|
||||
|
||||
let head = chain.head();
|
||||
let head = chain.head().expect("should get head");
|
||||
|
||||
let state = head.beacon_state;
|
||||
let block = head.beacon_block;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::checkpoint::CheckPoint;
|
||||
use crate::metrics;
|
||||
use parking_lot::RwLock;
|
||||
use std::borrow::Cow;
|
||||
use types::{BeaconBlock, BeaconState, EthSpec, Hash256};
|
||||
|
||||
const CACHE_SIZE: usize = 4;
|
||||
@@ -34,7 +35,7 @@ impl<T: EthSpec> Default for CheckPointCache<T> {
|
||||
}
|
||||
|
||||
impl<T: EthSpec> CheckPointCache<T> {
|
||||
pub fn insert(&self, checkpoint: &CheckPoint<T>) {
|
||||
pub fn insert(&self, checkpoint: Cow<CheckPoint<T>>) {
|
||||
if self
|
||||
.inner
|
||||
.read()
|
||||
@@ -50,10 +51,10 @@ impl<T: EthSpec> CheckPointCache<T> {
|
||||
let mut inner = self.inner.write();
|
||||
|
||||
if inner.checkpoints.len() < inner.limit {
|
||||
inner.checkpoints.push(checkpoint.clone())
|
||||
inner.checkpoints.push(checkpoint.into_owned())
|
||||
} else {
|
||||
let i = inner.oldest; // to satisfy the borrow checker.
|
||||
inner.checkpoints[i] = checkpoint.clone();
|
||||
inner.checkpoints[i] = checkpoint.into_owned();
|
||||
inner.oldest += 1;
|
||||
inner.oldest %= inner.limit;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use state_processing::per_block_processing::errors::AttestationValidationError;
|
||||
use state_processing::BlockProcessingError;
|
||||
use state_processing::SlotProcessingError;
|
||||
use std::time::Duration;
|
||||
use store::block_root_tree::BlockRootTreeError;
|
||||
use types::*;
|
||||
|
||||
macro_rules! easy_from_to {
|
||||
@@ -48,11 +49,14 @@ pub enum BeaconChainError {
|
||||
/// Returned when an internal check fails, indicating corrupt data.
|
||||
InvariantViolated(String),
|
||||
SszTypesError(SszTypesError),
|
||||
CanonicalHeadLockTimeout,
|
||||
BlockRootTreeError(BlockRootTreeError),
|
||||
}
|
||||
|
||||
easy_from_to!(SlotProcessingError, BeaconChainError);
|
||||
easy_from_to!(AttestationValidationError, BeaconChainError);
|
||||
easy_from_to!(SszTypesError, BeaconChainError);
|
||||
easy_from_to!(BlockRootTreeError, BeaconChainError);
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum BlockProductionError {
|
||||
|
||||
@@ -94,8 +94,16 @@ impl JustificationManager {
|
||||
) -> Result<()> {
|
||||
let new_checkpoint = &state.current_justified_checkpoint;
|
||||
|
||||
// Only proceeed if the new checkpoint is better than our best checkpoint.
|
||||
if new_checkpoint.epoch > self.best_justified_checkpoint.epoch {
|
||||
// TODO: add check about block.slot <= justified epoch start slot.
|
||||
|
||||
// Only proceeed if the new checkpoint is better than our current checkpoint.
|
||||
if new_checkpoint.epoch > self.justified_checkpoint.epoch {
|
||||
let new_checkpoint_balances = CheckpointBalances {
|
||||
epoch: state.current_justified_checkpoint.epoch,
|
||||
root: state.current_justified_checkpoint.root,
|
||||
balances: state.balances.clone().into(),
|
||||
};
|
||||
|
||||
// From the given state, read the block root at first slot of
|
||||
// `self.justified_checkpoint.epoch`. If that root matches, then
|
||||
// `new_justified_checkpoint` is a descendant of `self.justified_checkpoint` and we may
|
||||
@@ -109,12 +117,15 @@ impl JustificationManager {
|
||||
.start_slot(T::EthSpec::slots_per_epoch()),
|
||||
)?;
|
||||
|
||||
// If the new justified checkpoint is an ancestor of the current justified checkpoint,
|
||||
// it is always safe to change it.
|
||||
if new_checkpoint_ancestor == Some(self.justified_checkpoint.root) {
|
||||
self.best_justified_checkpoint = CheckpointBalances {
|
||||
epoch: state.current_justified_checkpoint.epoch,
|
||||
root: state.current_justified_checkpoint.root,
|
||||
balances: state.balances.clone().into(),
|
||||
};
|
||||
self.justified_checkpoint = new_checkpoint_balances.clone()
|
||||
}
|
||||
|
||||
if new_checkpoint.epoch > self.best_justified_checkpoint.epoch {
|
||||
// Always update the best checkpoint, if it's better.
|
||||
self.best_justified_checkpoint = new_checkpoint_balances;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +212,14 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
|
||||
pub fn find_head(&self, chain: &BeaconChain<T>) -> Result<Hash256> {
|
||||
let timer = metrics::start_timer(&metrics::FORK_CHOICE_FIND_HEAD_TIMES);
|
||||
|
||||
let remove_alias = |root| {
|
||||
if root == Hash256::zero() {
|
||||
self.genesis_block_root
|
||||
} else {
|
||||
root
|
||||
}
|
||||
};
|
||||
|
||||
self.justification_manager.write().update(chain)?;
|
||||
|
||||
let justified_checkpoint = self
|
||||
@@ -214,9 +233,9 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
|
||||
.backend
|
||||
.find_head(
|
||||
justified_checkpoint.epoch,
|
||||
justified_checkpoint.root,
|
||||
remove_alias(justified_checkpoint.root),
|
||||
finalized_checkpoint.epoch,
|
||||
finalized_checkpoint.root,
|
||||
remove_alias(finalized_checkpoint.root),
|
||||
&justified_checkpoint.balances,
|
||||
)
|
||||
.map_err(Into::into);
|
||||
@@ -244,6 +263,12 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
|
||||
.process_state(state, chain)?;
|
||||
self.justification_manager.write().update(chain)?;
|
||||
|
||||
// TODO: be more stringent about changing the finalized checkpoint (i.e., check for
|
||||
// reversion and stuff).
|
||||
if state.finalized_checkpoint.epoch > self.finalized_checkpoint.read().epoch {
|
||||
*self.finalized_checkpoint.write() = state.finalized_checkpoint.clone();
|
||||
}
|
||||
|
||||
// Note: we never count the block as a latest message, only attestations.
|
||||
for attestation in &block.body.attestations {
|
||||
// If the `data.beacon_block_root` block is not known to the fork choice, simply ignore
|
||||
@@ -332,12 +357,19 @@ impl<T: BeaconChainTypes> ForkChoice<T> {
|
||||
finalized_block: &BeaconBlock<T::EthSpec>,
|
||||
finalized_block_root: Hash256,
|
||||
) -> Result<()> {
|
||||
self.backend
|
||||
.update_finalized_root(
|
||||
finalized_block.slot.epoch(T::EthSpec::slots_per_epoch()),
|
||||
finalized_block_root,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
// Only prune if it won't remove our current justified epoch.
|
||||
if self.justification_manager.read().justified_checkpoint.epoch
|
||||
>= finalized_block.slot.epoch(T::EthSpec::slots_per_epoch())
|
||||
{
|
||||
self.backend
|
||||
.update_finalized_root(
|
||||
finalized_block.slot.epoch(T::EthSpec::slots_per_epoch()),
|
||||
finalized_block_root,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `SszForkChoice` which contains the current state of `Self`.
|
||||
|
||||
@@ -14,6 +14,7 @@ mod head_tracker;
|
||||
mod metrics;
|
||||
mod persisted_beacon_chain;
|
||||
pub mod test_utils;
|
||||
mod timeout_rw_lock;
|
||||
|
||||
pub use self::beacon_chain::{
|
||||
AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BlockProcessingOutcome,
|
||||
|
||||
@@ -199,10 +199,9 @@ lazy_static! {
|
||||
/// Scrape the `beacon_chain` for metrics that are not constantly updated (e.g., the present slot,
|
||||
/// head state info, etc) and update the Prometheus `DEFAULT_REGISTRY`.
|
||||
pub fn scrape_for_metrics<T: BeaconChainTypes>(beacon_chain: &BeaconChain<T>) {
|
||||
scrape_head_state::<T>(
|
||||
&beacon_chain.head().beacon_state,
|
||||
beacon_chain.head().beacon_state_root,
|
||||
);
|
||||
if let Ok(head) = beacon_chain.head() {
|
||||
scrape_head_state::<T>(&head.beacon_state, head.beacon_state_root)
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrape the given `state` assuming it's the head state, updating the `DEFAULT_REGISTRY`.
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{BeaconChainTypes, CheckPoint};
|
||||
use operation_pool::PersistedOperationPool;
|
||||
use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use store::{DBColumn, Error as StoreError, SimpleStoreItem};
|
||||
use store::{DBColumn, Error as StoreError, SimpleStoreItem, SszBlockRootTree};
|
||||
use types::Hash256;
|
||||
|
||||
/// 32-byte key for accessing the `PersistedBeaconChain`.
|
||||
@@ -18,6 +18,7 @@ pub struct PersistedBeaconChain<T: BeaconChainTypes> {
|
||||
pub genesis_block_root: Hash256,
|
||||
pub ssz_head_tracker: SszHeadTracker,
|
||||
pub fork_choice: SszForkChoice,
|
||||
pub block_root_tree: SszBlockRootTree,
|
||||
}
|
||||
|
||||
impl<T: BeaconChainTypes> SimpleStoreItem for PersistedBeaconChain<T> {
|
||||
|
||||
@@ -459,7 +459,12 @@ where
|
||||
honest_fork_blocks: usize,
|
||||
faulty_fork_blocks: usize,
|
||||
) -> (Hash256, Hash256) {
|
||||
let initial_head_slot = self.chain.head().beacon_block.slot;
|
||||
let initial_head_slot = self
|
||||
.chain
|
||||
.head()
|
||||
.expect("should get head")
|
||||
.beacon_block
|
||||
.slot;
|
||||
|
||||
// Move to the next slot so we may produce some more blocks on the head.
|
||||
self.advance_slot();
|
||||
|
||||
20
beacon_node/beacon_chain/src/timeout_rw_lock.rs
Normal file
20
beacon_node/beacon_chain/src/timeout_rw_lock.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use std::time::Duration;
|
||||
|
||||
/// A simple wrapper around `parking_lot::RwLock` that only permits read/write access with a
|
||||
/// time-out (i.e., no indefinitely-blocking operations).
|
||||
pub struct TimeoutRwLock<T>(RwLock<T>);
|
||||
|
||||
impl<T> TimeoutRwLock<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self(RwLock::new(inner))
|
||||
}
|
||||
|
||||
pub fn try_read_for(&self, timeout: Duration) -> Option<RwLockReadGuard<T>> {
|
||||
self.0.try_read_for(timeout)
|
||||
}
|
||||
|
||||
pub fn try_write_for(&self, timeout: Duration) -> Option<RwLockWriteGuard<T>> {
|
||||
self.0.try_write_for(timeout)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user