mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-11 18:04:18 +00:00
Trying something out
This commit is contained in:
@@ -4,9 +4,7 @@ use crate::attestation_verification::{
|
||||
batch_verify_unaggregated_attestations,
|
||||
};
|
||||
use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches};
|
||||
use crate::beacon_proposer_cache::{
|
||||
BeaconProposerCache, EpochBlockProposers, ensure_state_can_determine_proposers_for_epoch,
|
||||
};
|
||||
use crate::beacon_proposer_cache::{BeaconProposerCache, EpochBlockProposers};
|
||||
use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob};
|
||||
use crate::block_times_cache::BlockTimesCache;
|
||||
use crate::block_verification::POS_PANDA_BANNER;
|
||||
@@ -6548,62 +6546,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
accessor: impl Fn(&EpochBlockProposers) -> Result<V, BeaconChainError>,
|
||||
state_provider: impl FnOnce() -> Result<(Hash256, BeaconState<T::EthSpec>), E>,
|
||||
) -> Result<V, E> {
|
||||
let cache_entry = self
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.get_or_insert_key(proposal_epoch, shuffling_decision_block);
|
||||
|
||||
// If the cache entry is not initialised, run the code to initialise it inside a OnceCell.
|
||||
// This prevents duplication of work across multiple threads.
|
||||
//
|
||||
// If it is already initialised, then `get_or_try_init` will return immediately without
|
||||
// executing the initialisation code at all.
|
||||
let epoch_block_proposers = cache_entry.get_or_try_init(|| {
|
||||
// Fetch the state on-demand if the required epoch was missing from the cache.
|
||||
// If the caller wants to not compute the state they must return an error here and then
|
||||
// catch it at the call site.
|
||||
let (state_root, mut state) = state_provider()?;
|
||||
|
||||
// Ensure the state can compute proposer duties for `epoch`.
|
||||
ensure_state_can_determine_proposers_for_epoch(
|
||||
&mut state,
|
||||
state_root,
|
||||
proposal_epoch,
|
||||
&self.spec,
|
||||
)?;
|
||||
|
||||
// Sanity check the state.
|
||||
let latest_block_root = state.get_latest_block_root(state_root);
|
||||
let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch(
|
||||
proposal_epoch,
|
||||
latest_block_root,
|
||||
&self.spec,
|
||||
)?;
|
||||
if state_decision_block_root != shuffling_decision_block {
|
||||
return Err(Error::ProposerCacheIncorrectState {
|
||||
state_decision_block_root,
|
||||
requested_decision_block_root: shuffling_decision_block,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
let proposers = state.get_beacon_proposer_indices(proposal_epoch, &self.spec)?;
|
||||
|
||||
// Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have
|
||||
// advanced the state completely into the new epoch.
|
||||
let fork = self.spec.fork_at_epoch(proposal_epoch);
|
||||
|
||||
debug!(
|
||||
?shuffling_decision_block,
|
||||
epoch = %proposal_epoch,
|
||||
"Priming proposer shuffling cache"
|
||||
);
|
||||
|
||||
Ok::<_, E>(EpochBlockProposers::new(proposal_epoch, fork, proposers))
|
||||
})?;
|
||||
|
||||
// Run the accessor function on the computed epoch proposers.
|
||||
accessor(epoch_block_proposers).map_err(Into::into)
|
||||
crate::beacon_proposer_cache::with_proposer_cache(
|
||||
&self.beacon_proposer_cache,
|
||||
&self.spec,
|
||||
shuffling_decision_block,
|
||||
proposal_epoch,
|
||||
accessor,
|
||||
state_provider,
|
||||
)
|
||||
}
|
||||
|
||||
/// Runs the `map_fn` with the committee cache for `shuffling_epoch` from the chain with head
|
||||
|
||||
@@ -12,12 +12,13 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
|
||||
use fork_choice::ExecutionStatus;
|
||||
use lru::LruCache;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use safe_arith::SafeArith;
|
||||
use smallvec::SmallVec;
|
||||
use state_processing::state_advance::partial_state_advance;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
use tracing::{debug, instrument};
|
||||
use typenum::Unsigned;
|
||||
use types::new_non_zero_usize;
|
||||
use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot};
|
||||
@@ -164,6 +165,82 @@ impl BeaconProposerCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Access the proposer cache, computing and caching the proposers if necessary.
|
||||
///
|
||||
/// This is a free function that operates on references to the cache and spec, decoupled from
|
||||
/// `BeaconChain`. The `accessor` is called with the cached `EpochBlockProposers` for the given
|
||||
/// `(proposal_epoch, shuffling_decision_block)` key. If the cache entry is missing, the
|
||||
/// `state_provider` closure is called to produce a state which is then used to compute and
|
||||
/// cache the proposers.
|
||||
pub fn with_proposer_cache<Spec, V, Err>(
|
||||
beacon_proposer_cache: &Mutex<BeaconProposerCache>,
|
||||
spec: &ChainSpec,
|
||||
shuffling_decision_block: Hash256,
|
||||
proposal_epoch: Epoch,
|
||||
accessor: impl Fn(&EpochBlockProposers) -> Result<V, BeaconChainError>,
|
||||
state_provider: impl FnOnce() -> Result<(Hash256, BeaconState<Spec>), Err>,
|
||||
) -> Result<V, Err>
|
||||
where
|
||||
Spec: EthSpec,
|
||||
Err: From<BeaconChainError> + From<BeaconStateError>,
|
||||
{
|
||||
let cache_entry = beacon_proposer_cache
|
||||
.lock()
|
||||
.get_or_insert_key(proposal_epoch, shuffling_decision_block);
|
||||
|
||||
// If the cache entry is not initialised, run the code to initialise it inside a OnceCell.
|
||||
// This prevents duplication of work across multiple threads.
|
||||
//
|
||||
// If it is already initialised, then `get_or_try_init` will return immediately without
|
||||
// executing the initialisation code at all.
|
||||
let epoch_block_proposers = cache_entry.get_or_try_init(|| {
|
||||
// Fetch the state on-demand if the required epoch was missing from the cache.
|
||||
// If the caller wants to not compute the state they must return an error here and then
|
||||
// catch it at the call site.
|
||||
let (state_root, mut state) = state_provider()?;
|
||||
|
||||
// Ensure the state can compute proposer duties for `epoch`.
|
||||
ensure_state_can_determine_proposers_for_epoch(
|
||||
&mut state,
|
||||
state_root,
|
||||
proposal_epoch,
|
||||
spec,
|
||||
)?;
|
||||
|
||||
// Sanity check the state.
|
||||
let latest_block_root = state.get_latest_block_root(state_root);
|
||||
let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch(
|
||||
proposal_epoch,
|
||||
latest_block_root,
|
||||
spec,
|
||||
)?;
|
||||
if state_decision_block_root != shuffling_decision_block {
|
||||
return Err(BeaconChainError::ProposerCacheIncorrectState {
|
||||
state_decision_block_root,
|
||||
requested_decision_block_root: shuffling_decision_block,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
let proposers = state.get_beacon_proposer_indices(proposal_epoch, spec)?;
|
||||
|
||||
// Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have
|
||||
// advanced the state completely into the new epoch.
|
||||
let fork = spec.fork_at_epoch(proposal_epoch);
|
||||
|
||||
debug!(
|
||||
?shuffling_decision_block,
|
||||
epoch = %proposal_epoch,
|
||||
"Priming proposer shuffling cache"
|
||||
);
|
||||
|
||||
Ok::<_, Err>(EpochBlockProposers::new(proposal_epoch, fork, proposers))
|
||||
})?;
|
||||
|
||||
// Run the accessor function on the computed epoch proposers.
|
||||
accessor(epoch_block_proposers).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Compute the proposer duties using the head state without cache.
|
||||
///
|
||||
/// Return:
|
||||
|
||||
@@ -1,27 +1,43 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use educe::Educe;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use slot_clock::SlotClock;
|
||||
use state_processing::{
|
||||
VerifySignatures,
|
||||
envelope_processing::{VerifyStateRoot, process_execution_payload_envelope},
|
||||
};
|
||||
use store::DatabaseBlock;
|
||||
use tracing::{Span, debug};
|
||||
use types::{
|
||||
EthSpec, SignedBeaconBlock, SignedExecutionPayloadEnvelope,
|
||||
ChainSpec, EthSpec, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope,
|
||||
consts::gloas::BUILDER_INDEX_SELF_BUILD,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer,
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, BeaconStore, NotifyExecutionLayer,
|
||||
PayloadVerificationOutcome,
|
||||
beacon_proposer_cache::{self, BeaconProposerCache},
|
||||
canonical_head::CanonicalHead,
|
||||
payload_envelope_verification::{
|
||||
EnvelopeError, EnvelopeImportData, EnvelopeProcessingSnapshot, ExecutionPendingEnvelope,
|
||||
IntoExecutionPendingEnvelope, MaybeAvailableEnvelope, load_snapshot,
|
||||
payload_notifier::PayloadNotifier,
|
||||
},
|
||||
validator_pubkey_cache::ValidatorPubkeyCache,
|
||||
};
|
||||
|
||||
/// Bundles only the dependencies needed for gossip verification of execution payload envelopes,
|
||||
/// decoupling `GossipVerifiedEnvelope::new` from the full `BeaconChain`.
|
||||
pub struct GossipVerificationContext<'a, T: BeaconChainTypes> {
|
||||
pub canonical_head: &'a CanonicalHead<T>,
|
||||
pub store: &'a BeaconStore<T>,
|
||||
pub spec: &'a ChainSpec,
|
||||
pub beacon_proposer_cache: &'a Mutex<BeaconProposerCache>,
|
||||
pub validator_pubkey_cache: &'a RwLock<ValidatorPubkeyCache<T>>,
|
||||
pub genesis_validators_root: Hash256,
|
||||
}
|
||||
|
||||
/// A wrapper around a `SignedExecutionPayloadEnvelope` that indicates it has been approved for re-gossiping on
|
||||
/// the p2p network.
|
||||
#[derive(Educe)]
|
||||
@@ -35,7 +51,7 @@ pub struct GossipVerifiedEnvelope<T: BeaconChainTypes> {
|
||||
impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
|
||||
pub fn new(
|
||||
signed_envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>,
|
||||
chain: &BeaconChain<T>,
|
||||
ctx: &GossipVerificationContext<'_, T>,
|
||||
) -> Result<Self, EnvelopeError> {
|
||||
let envelope = &signed_envelope.message;
|
||||
let payload = &envelope.payload;
|
||||
@@ -48,7 +64,7 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
|
||||
// 2. Blocks we've seen that are invalid (REJECT).
|
||||
//
|
||||
// Presently these two cases are conflated.
|
||||
let fork_choice_read_lock = chain.canonical_head.fork_choice_read_lock();
|
||||
let fork_choice_read_lock = ctx.canonical_head.fork_choice_read_lock();
|
||||
let Some(proto_block) = fork_choice_read_lock.get_block(&beacon_block_root) else {
|
||||
return Err(EnvelopeError::BlockRootUnknown {
|
||||
block_root: beacon_block_root,
|
||||
@@ -64,12 +80,14 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
|
||||
|
||||
// TODO(EIP-7732): check that we haven't seen another valid `SignedExecutionPayloadEnvelope`
|
||||
// for this block root from this builder - envelope status table check
|
||||
let block = chain
|
||||
.get_full_block(&beacon_block_root)?
|
||||
.ok_or_else(|| {
|
||||
EnvelopeError::from(BeaconChainError::MissingBeaconBlock(beacon_block_root))
|
||||
})
|
||||
.map(Arc::new)?;
|
||||
let block = match ctx.store.try_get_full_block(&beacon_block_root)? {
|
||||
Some(DatabaseBlock::Full(block)) => Arc::new(block),
|
||||
Some(DatabaseBlock::Blinded(_)) | None => {
|
||||
return Err(EnvelopeError::from(BeaconChainError::MissingBeaconBlock(
|
||||
beacon_block_root,
|
||||
)));
|
||||
}
|
||||
};
|
||||
let execution_bid = &block
|
||||
.message()
|
||||
.body()
|
||||
@@ -118,13 +136,15 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
|
||||
let block_slot = envelope.slot;
|
||||
let block_epoch = block_slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
let proposer_shuffling_decision_block =
|
||||
proto_block.proposer_shuffling_root_for_child_block(block_epoch, &chain.spec);
|
||||
proto_block.proposer_shuffling_root_for_child_block(block_epoch, ctx.spec);
|
||||
|
||||
let (signature_is_valid, opt_snapshot) = if builder_index == BUILDER_INDEX_SELF_BUILD {
|
||||
// Fast path: self-build envelopes can be verified without loading the state.
|
||||
let envelope_ref = signed_envelope.as_ref();
|
||||
let mut opt_snapshot = None;
|
||||
let proposer = chain.with_proposer_cache::<_, EnvelopeError>(
|
||||
let proposer = beacon_proposer_cache::with_proposer_cache(
|
||||
ctx.beacon_proposer_cache,
|
||||
ctx.spec,
|
||||
proposer_shuffling_decision_block,
|
||||
block_epoch,
|
||||
|proposers| proposers.get_slot::<T::EthSpec>(block_slot),
|
||||
@@ -133,14 +153,14 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
|
||||
%beacon_block_root,
|
||||
"Proposer shuffling cache miss for envelope verification"
|
||||
);
|
||||
let snapshot = load_snapshot(envelope_ref, chain)?;
|
||||
let snapshot = load_snapshot(envelope_ref, ctx.canonical_head, ctx.store)?;
|
||||
opt_snapshot = Some(Box::new(snapshot.clone()));
|
||||
Ok((snapshot.state_root, snapshot.pre_state))
|
||||
Ok::<_, EnvelopeError>((snapshot.state_root, snapshot.pre_state))
|
||||
},
|
||||
)?;
|
||||
let fork = proposer.fork;
|
||||
|
||||
let pubkey_cache = chain.validator_pubkey_cache.read();
|
||||
let pubkey_cache = ctx.validator_pubkey_cache.read();
|
||||
let pubkey = pubkey_cache
|
||||
.get(block.message().proposer_index() as usize)
|
||||
.ok_or_else(|| EnvelopeError::UnknownValidator {
|
||||
@@ -149,16 +169,16 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
|
||||
let is_valid = signed_envelope.verify_signature(
|
||||
pubkey,
|
||||
&fork,
|
||||
chain.genesis_validators_root,
|
||||
&chain.spec,
|
||||
ctx.genesis_validators_root,
|
||||
ctx.spec,
|
||||
);
|
||||
(is_valid, opt_snapshot)
|
||||
} else {
|
||||
// TODO(gloas) if we implement a builder pubkey cache, we'll need to use it here.
|
||||
// External builder: must load the state to get the builder pubkey.
|
||||
let snapshot = load_snapshot(signed_envelope.as_ref(), chain)?;
|
||||
let snapshot = load_snapshot(signed_envelope.as_ref(), ctx.canonical_head, ctx.store)?;
|
||||
let is_valid =
|
||||
signed_envelope.verify_signature_with_state(&snapshot.pre_state, &chain.spec)?;
|
||||
signed_envelope.verify_signature_with_state(&snapshot.pre_state, ctx.spec)?;
|
||||
(is_valid, Some(Box::new(snapshot)))
|
||||
};
|
||||
|
||||
@@ -228,7 +248,11 @@ impl<T: BeaconChainTypes> IntoExecutionPendingEnvelope<T> for GossipVerifiedEnve
|
||||
let snapshot = if let Some(snapshot) = self.snapshot {
|
||||
*snapshot
|
||||
} else {
|
||||
load_snapshot(signed_envelope.as_ref(), chain)?
|
||||
load_snapshot(
|
||||
signed_envelope.as_ref(),
|
||||
&chain.canonical_head,
|
||||
&chain.store,
|
||||
)?
|
||||
};
|
||||
let mut state = snapshot.pre_state;
|
||||
|
||||
@@ -263,6 +287,18 @@ impl<T: BeaconChainTypes> IntoExecutionPendingEnvelope<T> for GossipVerifiedEnve
|
||||
}
|
||||
|
||||
impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// Build a `GossipVerificationContext` from this `BeaconChain`.
|
||||
pub fn gossip_verification_context(&self) -> GossipVerificationContext<'_, T> {
|
||||
GossipVerificationContext {
|
||||
canonical_head: &self.canonical_head,
|
||||
store: &self.store,
|
||||
spec: &self.spec,
|
||||
beacon_proposer_cache: &self.beacon_proposer_cache,
|
||||
validator_pubkey_cache: &self.validator_pubkey_cache,
|
||||
genesis_validators_root: self.genesis_validators_root,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `Ok(GossipVerifiedEnvelope)` if the supplied `envelope` should be forwarded onto the
|
||||
/// gossip network. The envelope is not imported into the chain, it is just partially verified.
|
||||
///
|
||||
@@ -287,7 +323,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let slot = envelope.slot();
|
||||
let beacon_block_root = envelope.message.beacon_block_root;
|
||||
|
||||
match GossipVerifiedEnvelope::new(envelope, &chain) {
|
||||
let ctx = chain.gossip_verification_context();
|
||||
match GossipVerifiedEnvelope::new(envelope, &ctx) {
|
||||
Ok(verified) => {
|
||||
debug!(
|
||||
%slot,
|
||||
|
||||
@@ -39,9 +39,9 @@ use types::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, ExecutionPayloadError,
|
||||
NotifyExecutionLayer, PayloadVerificationOutcome,
|
||||
block_verification::PayloadVerificationHandle,
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, BeaconStore, BlockError,
|
||||
ExecutionPayloadError, NotifyExecutionLayer, PayloadVerificationOutcome,
|
||||
block_verification::PayloadVerificationHandle, canonical_head::CanonicalHead,
|
||||
payload_envelope_verification::gossip_verified_envelope::GossipVerifiedEnvelope,
|
||||
};
|
||||
|
||||
@@ -49,6 +49,9 @@ pub mod gossip_verified_envelope;
|
||||
pub mod import;
|
||||
mod payload_notifier;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub trait IntoExecutionPendingEnvelope<T: BeaconChainTypes>: Sized {
|
||||
fn into_execution_pending_envelope(
|
||||
self,
|
||||
@@ -289,7 +292,8 @@ impl From<EnvelopeProcessingError> for EnvelopeError {
|
||||
#[instrument(skip_all, level = "debug", fields(beacon_block_root = %envelope.beacon_block_root()))]
|
||||
pub(crate) fn load_snapshot<T: BeaconChainTypes>(
|
||||
envelope: &SignedExecutionPayloadEnvelope<T::EthSpec>,
|
||||
chain: &BeaconChain<T>,
|
||||
canonical_head: &CanonicalHead<T>,
|
||||
store: &BeaconStore<T>,
|
||||
) -> Result<EnvelopeProcessingSnapshot<T::EthSpec>, EnvelopeError> {
|
||||
// Reject any envelope if its block is not known to fork choice.
|
||||
//
|
||||
@@ -302,7 +306,7 @@ pub(crate) fn load_snapshot<T: BeaconChainTypes>(
|
||||
// choice, so we will not reject any child of the finalized block (this is relevant during
|
||||
// genesis).
|
||||
|
||||
let fork_choice_read_lock = chain.canonical_head.fork_choice_read_lock();
|
||||
let fork_choice_read_lock = canonical_head.fork_choice_read_lock();
|
||||
let beacon_block_root = envelope.beacon_block_root();
|
||||
let Some(proto_beacon_block) = fork_choice_read_lock.get_block(&beacon_block_root) else {
|
||||
return Err(EnvelopeError::BlockRootUnknown {
|
||||
@@ -317,8 +321,7 @@ pub(crate) fn load_snapshot<T: BeaconChainTypes>(
|
||||
// We can use `get_hot_state` here rather than `get_advanced_hot_state` because the envelope
|
||||
// must be from the same slot as its block (so no advance is required).
|
||||
let cache_state = true;
|
||||
let state = chain
|
||||
.store
|
||||
let state = store
|
||||
.get_hot_state(&block_state_root, cache_state)
|
||||
.map_err(EnvelopeError::from)?
|
||||
.ok_or_else(|| {
|
||||
@@ -342,7 +345,7 @@ impl<T: BeaconChainTypes> IntoExecutionPendingEnvelope<T>
|
||||
chain: &Arc<BeaconChain<T>>,
|
||||
notify_execution_layer: NotifyExecutionLayer,
|
||||
) -> Result<ExecutionPendingEnvelope<T::EthSpec>, EnvelopeError> {
|
||||
GossipVerifiedEnvelope::new(self, chain)?
|
||||
GossipVerifiedEnvelope::new(self, &chain.gossip_verification_context())?
|
||||
.into_execution_pending_envelope(chain, notify_execution_layer)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bls::{FixedBytesExtended, Keypair, Signature};
|
||||
use fork_choice::ForkChoice;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use ssz_types::VariableList;
|
||||
use store::{HotColdDB, KeyValueStore, MemoryStore, StoreConfig};
|
||||
use types::consts::gloas::BUILDER_INDEX_SELF_BUILD;
|
||||
use types::test_utils::generate_deterministic_keypairs;
|
||||
use types::*;
|
||||
|
||||
use crate::BeaconStore;
|
||||
use crate::beacon_fork_choice_store::BeaconForkChoiceStore;
|
||||
use crate::beacon_proposer_cache::BeaconProposerCache;
|
||||
use crate::builder::Witness;
|
||||
use crate::canonical_head::CanonicalHead;
|
||||
use crate::payload_envelope_verification::EnvelopeError;
|
||||
use crate::payload_envelope_verification::gossip_verified_envelope::{
|
||||
GossipVerificationContext, GossipVerifiedEnvelope,
|
||||
};
|
||||
use crate::validator_pubkey_cache::ValidatorPubkeyCache;
|
||||
|
||||
type TestEthSpec = MinimalEthSpec;
|
||||
type TestTypes = Witness<
|
||||
slot_clock::TestingSlotClock,
|
||||
TestEthSpec,
|
||||
MemoryStore<TestEthSpec>,
|
||||
MemoryStore<TestEthSpec>,
|
||||
>;
|
||||
|
||||
/// Test context that holds the minimal state needed for gossip verification.
|
||||
struct TestContext {
|
||||
store: BeaconStore<TestTypes>,
|
||||
canonical_head: CanonicalHead<TestTypes>,
|
||||
beacon_proposer_cache: Mutex<BeaconProposerCache>,
|
||||
validator_pubkey_cache: RwLock<ValidatorPubkeyCache<TestTypes>>,
|
||||
spec: Arc<ChainSpec>,
|
||||
keypairs: Vec<Keypair>,
|
||||
genesis_state: BeaconState<TestEthSpec>,
|
||||
genesis_block_root: Hash256,
|
||||
genesis_validators_root: Hash256,
|
||||
}
|
||||
|
||||
impl TestContext {
|
||||
fn new(validator_count: usize) -> Self {
|
||||
let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::minimal()));
|
||||
let keypairs = generate_deterministic_keypairs(validator_count);
|
||||
|
||||
let mut genesis_state = genesis::interop_genesis_state::<TestEthSpec>(
|
||||
&keypairs,
|
||||
0, // genesis_time
|
||||
Hash256::from_slice(&[0x42; 32]),
|
||||
None, // no execution payload header
|
||||
&spec,
|
||||
)
|
||||
.expect("should create genesis state");
|
||||
|
||||
let genesis_validators_root = genesis_state.genesis_validators_root();
|
||||
|
||||
let store: BeaconStore<TestTypes> = Arc::new(
|
||||
HotColdDB::open_ephemeral(StoreConfig::default(), spec.clone())
|
||||
.expect("should create ephemeral store"),
|
||||
);
|
||||
|
||||
// Initialize store metadata.
|
||||
let genesis_block = BeaconBlock::<TestEthSpec>::empty(&spec);
|
||||
let genesis_block_root = genesis_block.canonical_root();
|
||||
let signed_genesis_block = SignedBeaconBlock::from_block(genesis_block, Signature::empty());
|
||||
|
||||
// Build caches and compute state root before storing.
|
||||
genesis_state
|
||||
.build_caches(&spec)
|
||||
.expect("should build caches");
|
||||
|
||||
// Initialize store metadata ops (must be done before put_state).
|
||||
let ops = vec![
|
||||
store
|
||||
.init_anchor_info(
|
||||
signed_genesis_block.parent_root(),
|
||||
signed_genesis_block.slot(),
|
||||
Slot::new(0),
|
||||
false,
|
||||
)
|
||||
.expect("should init anchor info"),
|
||||
store
|
||||
.init_blob_info(signed_genesis_block.slot())
|
||||
.expect("should init blob info"),
|
||||
store
|
||||
.init_data_column_info(signed_genesis_block.slot())
|
||||
.expect("should init data column info"),
|
||||
];
|
||||
store
|
||||
.hot_db
|
||||
.do_atomically(ops)
|
||||
.expect("should store metadata");
|
||||
|
||||
// Store the genesis block and state.
|
||||
store
|
||||
.put_block(&genesis_block_root, signed_genesis_block.clone())
|
||||
.expect("should store genesis block");
|
||||
let state_root = genesis_state
|
||||
.update_tree_hash_cache()
|
||||
.expect("should compute state root");
|
||||
store
|
||||
.put_state(&state_root, &genesis_state)
|
||||
.expect("should store genesis state");
|
||||
|
||||
// Create BeaconSnapshot and fork choice.
|
||||
let snapshot = crate::BeaconSnapshot {
|
||||
beacon_block: Arc::new(signed_genesis_block),
|
||||
beacon_block_root: genesis_block_root,
|
||||
beacon_state: genesis_state.clone(),
|
||||
};
|
||||
|
||||
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store.clone(), snapshot.clone())
|
||||
.expect("should create fork choice store");
|
||||
|
||||
let fork_choice = ForkChoice::from_anchor(
|
||||
fc_store,
|
||||
genesis_block_root,
|
||||
&snapshot.beacon_block,
|
||||
&snapshot.beacon_state,
|
||||
None,
|
||||
&spec,
|
||||
)
|
||||
.expect("should create fork choice from anchor");
|
||||
|
||||
let canonical_head = CanonicalHead::new(fork_choice, Arc::new(snapshot));
|
||||
|
||||
let validator_pubkey_cache = ValidatorPubkeyCache::new(&genesis_state, store.clone())
|
||||
.expect("should create validator pubkey cache");
|
||||
|
||||
TestContext {
|
||||
store,
|
||||
canonical_head,
|
||||
beacon_proposer_cache: Mutex::new(BeaconProposerCache::default()),
|
||||
validator_pubkey_cache: RwLock::new(validator_pubkey_cache),
|
||||
spec,
|
||||
keypairs,
|
||||
genesis_state,
|
||||
genesis_block_root,
|
||||
genesis_validators_root,
|
||||
}
|
||||
}
|
||||
|
||||
fn gossip_verification_context(&self) -> GossipVerificationContext<'_, TestTypes> {
|
||||
GossipVerificationContext {
|
||||
canonical_head: &self.canonical_head,
|
||||
store: &self.store,
|
||||
spec: &self.spec,
|
||||
beacon_proposer_cache: &self.beacon_proposer_cache,
|
||||
validator_pubkey_cache: &self.validator_pubkey_cache,
|
||||
genesis_validators_root: self.genesis_validators_root,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a gloas block at `slot` with the given proposer, store it, add it to fork choice,
|
||||
/// and return the signed block, block root, and post-state.
|
||||
fn build_and_import_block(
|
||||
&self,
|
||||
slot: Slot,
|
||||
proposer_index: usize,
|
||||
execution_bid: ExecutionPayloadBid<TestEthSpec>,
|
||||
) -> (
|
||||
Arc<SignedBeaconBlock<TestEthSpec>>,
|
||||
Hash256,
|
||||
BeaconState<TestEthSpec>,
|
||||
) {
|
||||
let mut state = self.genesis_state.clone();
|
||||
|
||||
// Advance the state to the target slot.
|
||||
if slot > state.slot() {
|
||||
state_processing::state_advance::complete_state_advance(
|
||||
&mut state, None, slot, &self.spec,
|
||||
)
|
||||
.expect("should advance state");
|
||||
}
|
||||
|
||||
state.build_caches(&self.spec).expect("should build caches");
|
||||
|
||||
// Compute the state root so we can embed it in the block.
|
||||
let state_root = state
|
||||
.update_tree_hash_cache()
|
||||
.expect("should compute state root");
|
||||
|
||||
let signed_bid = SignedExecutionPayloadBid {
|
||||
message: execution_bid,
|
||||
signature: Signature::infinity().expect("should create infinity signature"),
|
||||
};
|
||||
|
||||
// Create the block body with the actual state root.
|
||||
let block = BeaconBlock::Gloas(BeaconBlockGloas {
|
||||
slot,
|
||||
proposer_index: proposer_index as u64,
|
||||
parent_root: self.genesis_block_root,
|
||||
state_root,
|
||||
body: BeaconBlockBodyGloas {
|
||||
randao_reveal: Signature::empty(),
|
||||
eth1_data: state.eth1_data().clone(),
|
||||
graffiti: Graffiti::default(),
|
||||
proposer_slashings: VariableList::empty(),
|
||||
attester_slashings: VariableList::empty(),
|
||||
attestations: VariableList::empty(),
|
||||
deposits: VariableList::empty(),
|
||||
voluntary_exits: VariableList::empty(),
|
||||
sync_aggregate: SyncAggregate::empty(),
|
||||
bls_to_execution_changes: VariableList::empty(),
|
||||
signed_execution_payload_bid: signed_bid,
|
||||
payload_attestations: VariableList::empty(),
|
||||
_phantom: std::marker::PhantomData,
|
||||
},
|
||||
});
|
||||
|
||||
let block_root = block.canonical_root();
|
||||
let proposer_sk = &self.keypairs[proposer_index].sk;
|
||||
let fork = self
|
||||
.spec
|
||||
.fork_at_epoch(slot.epoch(TestEthSpec::slots_per_epoch()));
|
||||
let signed_block = block.sign(proposer_sk, &fork, self.genesis_validators_root, &self.spec);
|
||||
|
||||
// Store block and state.
|
||||
self.store
|
||||
.put_block(&block_root, signed_block.clone())
|
||||
.expect("should store block");
|
||||
self.store
|
||||
.put_state(&state_root, &state)
|
||||
.expect("should store state");
|
||||
|
||||
// Add block to fork choice.
|
||||
let mut fork_choice = self.canonical_head.fork_choice_write_lock();
|
||||
fork_choice
|
||||
.on_block(
|
||||
slot,
|
||||
signed_block.message(),
|
||||
block_root,
|
||||
Duration::from_secs(0),
|
||||
&state,
|
||||
crate::PayloadVerificationStatus::Verified,
|
||||
&self.spec,
|
||||
)
|
||||
.expect("should add block to fork choice");
|
||||
drop(fork_choice);
|
||||
|
||||
(Arc::new(signed_block), block_root, state)
|
||||
}
|
||||
|
||||
/// Build a signed execution payload envelope for the given block.
|
||||
fn build_signed_envelope(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
slot: Slot,
|
||||
builder_index: u64,
|
||||
block_hash: ExecutionBlockHash,
|
||||
signing_key: &bls::SecretKey,
|
||||
) -> Arc<SignedExecutionPayloadEnvelope<TestEthSpec>> {
|
||||
let mut payload = ExecutionPayloadGloas::default();
|
||||
payload.block_hash = block_hash;
|
||||
|
||||
let envelope = ExecutionPayloadEnvelope {
|
||||
payload,
|
||||
execution_requests: ExecutionRequests::default(),
|
||||
builder_index,
|
||||
beacon_block_root: block_root,
|
||||
slot,
|
||||
state_root: Hash256::zero(),
|
||||
};
|
||||
|
||||
let fork = self
|
||||
.spec
|
||||
.fork_at_epoch(slot.epoch(TestEthSpec::slots_per_epoch()));
|
||||
let domain = self.spec.get_domain(
|
||||
slot.epoch(TestEthSpec::slots_per_epoch()),
|
||||
Domain::BeaconBuilder,
|
||||
&fork,
|
||||
self.genesis_validators_root,
|
||||
);
|
||||
let message = envelope.signing_root(domain);
|
||||
let signature = signing_key.sign(message);
|
||||
|
||||
Arc::new(SignedExecutionPayloadEnvelope {
|
||||
message: envelope,
|
||||
signature,
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper: build a block and matching self-build envelope.
|
||||
fn build_block_and_envelope(
|
||||
&self,
|
||||
) -> (
|
||||
Arc<SignedBeaconBlock<TestEthSpec>>,
|
||||
Hash256,
|
||||
Arc<SignedExecutionPayloadEnvelope<TestEthSpec>>,
|
||||
) {
|
||||
let slot = Slot::new(1);
|
||||
let block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32]));
|
||||
|
||||
// Get proposer for slot 1.
|
||||
let mut state = self.genesis_state.clone();
|
||||
state_processing::state_advance::complete_state_advance(&mut state, None, slot, &self.spec)
|
||||
.expect("should advance state");
|
||||
state.build_caches(&self.spec).expect("should build caches");
|
||||
let proposer_index = state
|
||||
.get_beacon_proposer_index(slot, &self.spec)
|
||||
.expect("should get proposer index");
|
||||
|
||||
let bid = ExecutionPayloadBid {
|
||||
builder_index: BUILDER_INDEX_SELF_BUILD,
|
||||
block_hash,
|
||||
slot,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (signed_block, block_root, _post_state) =
|
||||
self.build_and_import_block(slot, proposer_index, bid);
|
||||
|
||||
let proposer_sk = &self.keypairs[proposer_index].sk;
|
||||
let envelope = self.build_signed_envelope(
|
||||
block_root,
|
||||
slot,
|
||||
BUILDER_INDEX_SELF_BUILD,
|
||||
block_hash,
|
||||
proposer_sk,
|
||||
);
|
||||
|
||||
(signed_block, block_root, envelope)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_self_build_envelope() {
|
||||
let ctx = TestContext::new(32);
|
||||
let (_block, _block_root, envelope) = ctx.build_block_and_envelope();
|
||||
let gossip_ctx = ctx.gossip_verification_context();
|
||||
|
||||
let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"valid self-build envelope should pass verification, got: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_block_root() {
|
||||
let ctx = TestContext::new(32);
|
||||
let gossip_ctx = ctx.gossip_verification_context();
|
||||
|
||||
// Build an envelope referencing a block root not in fork choice.
|
||||
let unknown_root = Hash256::from_slice(&[0xff; 32]);
|
||||
let envelope = ctx.build_signed_envelope(
|
||||
unknown_root,
|
||||
Slot::new(1),
|
||||
BUILDER_INDEX_SELF_BUILD,
|
||||
ExecutionBlockHash::from_root(Hash256::zero()),
|
||||
&ctx.keypairs[0].sk,
|
||||
);
|
||||
|
||||
let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx);
|
||||
assert!(
|
||||
matches!(result, Err(EnvelopeError::BlockRootUnknown { .. })),
|
||||
"should reject envelope with unknown block root, got: {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slot_mismatch() {
|
||||
let ctx = TestContext::new(32);
|
||||
let (_block, block_root, _good_envelope) = ctx.build_block_and_envelope();
|
||||
let gossip_ctx = ctx.gossip_verification_context();
|
||||
|
||||
// Build an envelope with a different slot than the block.
|
||||
let wrong_slot = Slot::new(2);
|
||||
let envelope = ctx.build_signed_envelope(
|
||||
block_root,
|
||||
wrong_slot,
|
||||
BUILDER_INDEX_SELF_BUILD,
|
||||
ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32])),
|
||||
&ctx.keypairs[0].sk,
|
||||
);
|
||||
|
||||
let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx);
|
||||
assert!(
|
||||
matches!(result, Err(EnvelopeError::SlotMismatch { .. })),
|
||||
"should reject envelope with slot mismatch, got: {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_index_mismatch() {
|
||||
let ctx = TestContext::new(32);
|
||||
let gossip_ctx = ctx.gossip_verification_context();
|
||||
|
||||
let slot = Slot::new(1);
|
||||
let block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32]));
|
||||
|
||||
// Get proposer for slot 1.
|
||||
let mut state = ctx.genesis_state.clone();
|
||||
state_processing::state_advance::complete_state_advance(&mut state, None, slot, &ctx.spec)
|
||||
.expect("should advance state");
|
||||
state.build_caches(&ctx.spec).expect("should build caches");
|
||||
let proposer_index = state
|
||||
.get_beacon_proposer_index(slot, &ctx.spec)
|
||||
.expect("should get proposer index");
|
||||
|
||||
// Block has builder_index = BUILDER_INDEX_SELF_BUILD
|
||||
let bid = ExecutionPayloadBid {
|
||||
builder_index: BUILDER_INDEX_SELF_BUILD,
|
||||
block_hash,
|
||||
slot,
|
||||
..Default::default()
|
||||
};
|
||||
let (_block, block_root, _post_state) = ctx.build_and_import_block(slot, proposer_index, bid);
|
||||
|
||||
// Envelope has a different builder_index.
|
||||
let wrong_builder_index = 999;
|
||||
let envelope = ctx.build_signed_envelope(
|
||||
block_root,
|
||||
slot,
|
||||
wrong_builder_index,
|
||||
block_hash,
|
||||
&ctx.keypairs[proposer_index].sk,
|
||||
);
|
||||
|
||||
let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx);
|
||||
assert!(
|
||||
matches!(result, Err(EnvelopeError::BuilderIndexMismatch { .. })),
|
||||
"should reject envelope with builder index mismatch, got: {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_hash_mismatch() {
|
||||
let ctx = TestContext::new(32);
|
||||
let gossip_ctx = ctx.gossip_verification_context();
|
||||
|
||||
let slot = Slot::new(1);
|
||||
let block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32]));
|
||||
let wrong_block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xbb; 32]));
|
||||
|
||||
// Get proposer for slot 1.
|
||||
let mut state = ctx.genesis_state.clone();
|
||||
state_processing::state_advance::complete_state_advance(&mut state, None, slot, &ctx.spec)
|
||||
.expect("should advance state");
|
||||
state.build_caches(&ctx.spec).expect("should build caches");
|
||||
let proposer_index = state
|
||||
.get_beacon_proposer_index(slot, &ctx.spec)
|
||||
.expect("should get proposer index");
|
||||
|
||||
// Block has block_hash = 0xaa
|
||||
let bid = ExecutionPayloadBid {
|
||||
builder_index: BUILDER_INDEX_SELF_BUILD,
|
||||
block_hash,
|
||||
slot,
|
||||
..Default::default()
|
||||
};
|
||||
let (_block, block_root, _post_state) = ctx.build_and_import_block(slot, proposer_index, bid);
|
||||
|
||||
// Envelope has a different block_hash.
|
||||
let envelope = ctx.build_signed_envelope(
|
||||
block_root,
|
||||
slot,
|
||||
BUILDER_INDEX_SELF_BUILD,
|
||||
wrong_block_hash,
|
||||
&ctx.keypairs[proposer_index].sk,
|
||||
);
|
||||
|
||||
let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx);
|
||||
assert!(
|
||||
matches!(result, Err(EnvelopeError::BlockHashMismatch { .. })),
|
||||
"should reject envelope with block hash mismatch, got: {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_signature() {
|
||||
let ctx = TestContext::new(32);
|
||||
let gossip_ctx = ctx.gossip_verification_context();
|
||||
|
||||
let slot = Slot::new(1);
|
||||
let block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32]));
|
||||
|
||||
// Get proposer for slot 1.
|
||||
let mut state = ctx.genesis_state.clone();
|
||||
state_processing::state_advance::complete_state_advance(&mut state, None, slot, &ctx.spec)
|
||||
.expect("should advance state");
|
||||
state.build_caches(&ctx.spec).expect("should build caches");
|
||||
let proposer_index = state
|
||||
.get_beacon_proposer_index(slot, &ctx.spec)
|
||||
.expect("should get proposer index");
|
||||
|
||||
let bid = ExecutionPayloadBid {
|
||||
builder_index: BUILDER_INDEX_SELF_BUILD,
|
||||
block_hash,
|
||||
slot,
|
||||
..Default::default()
|
||||
};
|
||||
let (_block, block_root, _post_state) = ctx.build_and_import_block(slot, proposer_index, bid);
|
||||
|
||||
// Sign the envelope with the wrong key (some other validator's key).
|
||||
let wrong_key_index = if proposer_index == 0 { 1 } else { 0 };
|
||||
let envelope = ctx.build_signed_envelope(
|
||||
block_root,
|
||||
slot,
|
||||
BUILDER_INDEX_SELF_BUILD,
|
||||
block_hash,
|
||||
&ctx.keypairs[wrong_key_index].sk,
|
||||
);
|
||||
|
||||
let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx);
|
||||
assert!(
|
||||
matches!(result, Err(EnvelopeError::BadSignature)),
|
||||
"should reject envelope with bad signature, got: {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: `test_prior_to_finalization` is omitted here because advancing finalization requires
|
||||
// attestation-based justification which needs the full `BeaconChainHarness`. The
|
||||
// `PriorToFinalization` code path is tested in the integration tests.
|
||||
Reference in New Issue
Block a user