mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-02 16:21:42 +00:00
introduce a smalll refactor and unit test
This commit is contained in:
@@ -10,8 +10,8 @@ use state_processing::{
|
||||
use store::DatabaseBlock;
|
||||
use tracing::{Span, debug};
|
||||
use types::{
|
||||
ChainSpec, EthSpec, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope,
|
||||
consts::gloas::BUILDER_INDEX_SELF_BUILD,
|
||||
ChainSpec, EthSpec, ExecutionPayloadBid, ExecutionPayloadEnvelope, Hash256, SignedBeaconBlock,
|
||||
SignedExecutionPayloadEnvelope, Slot, consts::gloas::BUILDER_INDEX_SELF_BUILD,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -38,6 +38,54 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> {
|
||||
pub genesis_validators_root: Hash256,
|
||||
}
|
||||
|
||||
/// Verify that an execution payload envelope is consistent with its beacon block
|
||||
/// and execution bid. This checks:
|
||||
/// - The envelope slot is not prior to finalization
|
||||
/// - The envelope slot matches the block slot
|
||||
/// - The builder index matches the committed bid
|
||||
/// - The payload block hash matches the committed bid
|
||||
pub(crate) fn verify_envelope_consistency<E: EthSpec>(
|
||||
envelope: &ExecutionPayloadEnvelope<E>,
|
||||
block: &SignedBeaconBlock<E>,
|
||||
execution_bid: &ExecutionPayloadBid<E>,
|
||||
latest_finalized_slot: Slot,
|
||||
) -> Result<(), EnvelopeError> {
|
||||
// Check that the envelope's slot isn't from a slot prior
|
||||
// to the latest finalized slot.
|
||||
if envelope.slot < latest_finalized_slot {
|
||||
return Err(EnvelopeError::PriorToFinalization {
|
||||
payload_slot: envelope.slot,
|
||||
latest_finalized_slot,
|
||||
});
|
||||
}
|
||||
|
||||
// Check that the slot of the envelope matches the slot of the parent block.
|
||||
if envelope.slot != block.slot() {
|
||||
return Err(EnvelopeError::SlotMismatch {
|
||||
block: block.slot(),
|
||||
envelope: envelope.slot,
|
||||
});
|
||||
}
|
||||
|
||||
// Builder index matches committed bid.
|
||||
if envelope.builder_index != execution_bid.builder_index {
|
||||
return Err(EnvelopeError::BuilderIndexMismatch {
|
||||
committed_bid: execution_bid.builder_index,
|
||||
envelope: envelope.builder_index,
|
||||
});
|
||||
}
|
||||
|
||||
// The block hash should match the block hash of the execution bid.
|
||||
if envelope.payload.block_hash != execution_bid.block_hash {
|
||||
return Err(EnvelopeError::BlockHashMismatch {
|
||||
committed_bid: execution_bid.block_hash,
|
||||
envelope: envelope.payload.block_hash,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A wrapper around a `SignedExecutionPayloadEnvelope` that indicates it has been approved for re-gossiping on
|
||||
/// the p2p network.
|
||||
#[derive(Educe)]
|
||||
@@ -54,7 +102,6 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
|
||||
ctx: &GossipVerificationContext<'_, T>,
|
||||
) -> Result<Self, EnvelopeError> {
|
||||
let envelope = &signed_envelope.message;
|
||||
let payload = &envelope.payload;
|
||||
let beacon_block_root = envelope.beacon_block_root;
|
||||
|
||||
// Check that we've seen the beacon block for this envelope and that it passes validation.
|
||||
@@ -94,38 +141,7 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
|
||||
.signed_execution_payload_bid()?
|
||||
.message;
|
||||
|
||||
// check that the envelopes slot isnt from a slot prior
|
||||
// to the latest finalized slot.
|
||||
if envelope.slot < latest_finalized_slot {
|
||||
return Err(EnvelopeError::PriorToFinalization {
|
||||
payload_slot: envelope.slot,
|
||||
latest_finalized_slot,
|
||||
});
|
||||
}
|
||||
|
||||
// check that the slot of the envelope matches the slot of the parent block
|
||||
if envelope.slot != block.slot() {
|
||||
return Err(EnvelopeError::SlotMismatch {
|
||||
block: block.slot(),
|
||||
envelope: envelope.slot,
|
||||
});
|
||||
}
|
||||
|
||||
// builder index matches committed bid
|
||||
if envelope.builder_index != execution_bid.builder_index {
|
||||
return Err(EnvelopeError::BuilderIndexMismatch {
|
||||
committed_bid: execution_bid.builder_index,
|
||||
envelope: envelope.builder_index,
|
||||
});
|
||||
}
|
||||
|
||||
// the block hash should match the block hash of the execution bid
|
||||
if payload.block_hash != execution_bid.block_hash {
|
||||
return Err(EnvelopeError::BlockHashMismatch {
|
||||
committed_bid: execution_bid.block_hash,
|
||||
envelope: payload.block_hash,
|
||||
});
|
||||
}
|
||||
verify_envelope_consistency(envelope, &block, execution_bid, latest_finalized_slot)?;
|
||||
|
||||
// Verify the envelope signature.
|
||||
//
|
||||
@@ -353,3 +369,154 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
.map_err(BeaconChainError::TokioJoin)?
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use bls::Signature;
|
||||
use ssz_types::VariableList;
|
||||
use types::{
|
||||
BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, Eth1Data, ExecutionBlockHash,
|
||||
ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests,
|
||||
Graffiti, Hash256, MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadBid, Slot,
|
||||
SyncAggregate,
|
||||
};
|
||||
|
||||
use super::verify_envelope_consistency;
|
||||
use crate::payload_envelope_verification::EnvelopeError;
|
||||
|
||||
type E = MinimalEthSpec;
|
||||
|
||||
fn make_envelope(
|
||||
slot: Slot,
|
||||
builder_index: u64,
|
||||
block_hash: ExecutionBlockHash,
|
||||
) -> ExecutionPayloadEnvelope<E> {
|
||||
ExecutionPayloadEnvelope {
|
||||
payload: ExecutionPayloadGloas {
|
||||
block_hash,
|
||||
..ExecutionPayloadGloas::default()
|
||||
},
|
||||
execution_requests: ExecutionRequests::default(),
|
||||
builder_index,
|
||||
beacon_block_root: Hash256::ZERO,
|
||||
slot,
|
||||
state_root: Hash256::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_block(slot: Slot) -> SignedBeaconBlock<E> {
|
||||
let block = BeaconBlock::Gloas(BeaconBlockGloas {
|
||||
slot,
|
||||
proposer_index: 0,
|
||||
parent_root: Hash256::ZERO,
|
||||
state_root: Hash256::ZERO,
|
||||
body: BeaconBlockBodyGloas {
|
||||
randao_reveal: Signature::empty(),
|
||||
eth1_data: Eth1Data {
|
||||
deposit_root: Hash256::ZERO,
|
||||
block_hash: Hash256::ZERO,
|
||||
deposit_count: 0,
|
||||
},
|
||||
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: SignedExecutionPayloadBid::empty(),
|
||||
payload_attestations: VariableList::empty(),
|
||||
_phantom: PhantomData,
|
||||
},
|
||||
});
|
||||
SignedBeaconBlock::from_block(block, Signature::empty())
|
||||
}
|
||||
|
||||
fn make_bid(builder_index: u64, block_hash: ExecutionBlockHash) -> ExecutionPayloadBid<E> {
|
||||
ExecutionPayloadBid {
|
||||
builder_index,
|
||||
block_hash,
|
||||
..ExecutionPayloadBid::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_envelope() {
|
||||
let slot = Slot::new(10);
|
||||
let builder_index = 5;
|
||||
let block_hash = ExecutionBlockHash::repeat_byte(0xaa);
|
||||
|
||||
let envelope = make_envelope(slot, builder_index, block_hash);
|
||||
let block = make_block(slot);
|
||||
let bid = make_bid(builder_index, block_hash);
|
||||
|
||||
assert!(verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prior_to_finalization() {
|
||||
let slot = Slot::new(5);
|
||||
let builder_index = 1;
|
||||
let block_hash = ExecutionBlockHash::repeat_byte(0xbb);
|
||||
|
||||
let envelope = make_envelope(slot, builder_index, block_hash);
|
||||
let block = make_block(slot);
|
||||
let bid = make_bid(builder_index, block_hash);
|
||||
let latest_finalized_slot = Slot::new(10);
|
||||
|
||||
let result =
|
||||
verify_envelope_consistency::<E>(&envelope, &block, &bid, latest_finalized_slot);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(EnvelopeError::PriorToFinalization { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slot_mismatch() {
|
||||
let builder_index = 1;
|
||||
let block_hash = ExecutionBlockHash::repeat_byte(0xcc);
|
||||
|
||||
let envelope = make_envelope(Slot::new(10), builder_index, block_hash);
|
||||
let block = make_block(Slot::new(20));
|
||||
let bid = make_bid(builder_index, block_hash);
|
||||
|
||||
let result = verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0));
|
||||
assert!(matches!(result, Err(EnvelopeError::SlotMismatch { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_index_mismatch() {
|
||||
let slot = Slot::new(10);
|
||||
let block_hash = ExecutionBlockHash::repeat_byte(0xdd);
|
||||
|
||||
let envelope = make_envelope(slot, 1, block_hash);
|
||||
let block = make_block(slot);
|
||||
let bid = make_bid(2, block_hash);
|
||||
|
||||
let result = verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(EnvelopeError::BuilderIndexMismatch { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_hash_mismatch() {
|
||||
let slot = Slot::new(10);
|
||||
let builder_index = 1;
|
||||
|
||||
let envelope = make_envelope(slot, builder_index, ExecutionBlockHash::repeat_byte(0xee));
|
||||
let block = make_block(slot);
|
||||
let bid = make_bid(builder_index, ExecutionBlockHash::repeat_byte(0xff));
|
||||
|
||||
let result = verify_envelope_consistency::<E>(&envelope, &block, &bid, Slot::new(0));
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(EnvelopeError::BlockHashMismatch { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,6 @@ 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,
|
||||
|
||||
@@ -1,524 +0,0 @@
|
||||
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