mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-28 02:03:32 +00:00
Fix database pruning post-Gloas - Fix DB pruning logic (and state summaries DAG) - Get the `beacon_chain` tests running with `FORK_NAME=gloas` 🎉 Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>
750 lines
23 KiB
Rust
750 lines
23 KiB
Rust
use std::sync::Arc;
|
|
|
|
use std::time::Duration;
|
|
|
|
use bls::{Keypair, PublicKeyBytes, Signature};
|
|
use ethereum_hashing::hash;
|
|
use fork_choice::ForkChoice;
|
|
use genesis::{generate_deterministic_keypairs, interop_genesis_state};
|
|
use kzg::KzgCommitment;
|
|
use slot_clock::{SlotClock, TestingSlotClock};
|
|
use ssz::Encode;
|
|
use ssz_types::VariableList;
|
|
use state_processing::genesis::genesis_block;
|
|
use store::{HotColdDB, StoreConfig};
|
|
use types::{
|
|
Address, ChainSpec, Checkpoint, Domain, Epoch, EthSpec, ExecutionBlockHash,
|
|
ExecutionPayloadBid, Hash256, MinimalEthSpec, ProposerPreferences, SignedBeaconBlock,
|
|
SignedExecutionPayloadBid, SignedProposerPreferences, SignedRoot, Slot,
|
|
};
|
|
|
|
use proto_array::{Block as ProtoBlock, ExecutionStatus, PayloadStatus};
|
|
use types::AttestationShufflingId;
|
|
|
|
use crate::{
|
|
beacon_fork_choice_store::BeaconForkChoiceStore,
|
|
beacon_snapshot::BeaconSnapshot,
|
|
canonical_head::CanonicalHead,
|
|
payload_bid_verification::{
|
|
PayloadBidError,
|
|
gossip_verified_bid::{GossipVerificationContext, GossipVerifiedPayloadBid},
|
|
payload_bid_cache::GossipVerifiedPayloadBidCache,
|
|
},
|
|
proposer_preferences_verification::{
|
|
gossip_verified_proposer_preferences::GossipVerifiedProposerPreferences,
|
|
proposer_preference_cache::GossipVerifiedProposerPreferenceCache,
|
|
},
|
|
test_utils::{EphemeralHarnessType, fork_name_from_env, test_spec},
|
|
};
|
|
|
|
type E = MinimalEthSpec;
|
|
type T = EphemeralHarnessType<E>;
|
|
|
|
/// Number of regular validators (must be >= min_genesis_active_validator_count for MinimalEthSpec).
|
|
const NUM_VALIDATORS: usize = 64;
|
|
/// Number of builders to register.
|
|
const NUM_BUILDERS: usize = 4;
|
|
/// Balance given to each builder (min_deposit_amount + extra to cover bids in tests).
|
|
const BUILDER_BALANCE: u64 = 2_000_000_000;
|
|
|
|
struct TestContext {
|
|
canonical_head: CanonicalHead<T>,
|
|
bid_cache: GossipVerifiedPayloadBidCache<T>,
|
|
preferences_cache: GossipVerifiedProposerPreferenceCache,
|
|
slot_clock: TestingSlotClock,
|
|
keypairs: Vec<Keypair>,
|
|
spec: ChainSpec,
|
|
genesis_block_root: Hash256,
|
|
inactive_builder_index: u64,
|
|
}
|
|
|
|
fn builder_withdrawal_credentials(pubkey: &bls::PublicKey, spec: &ChainSpec) -> Hash256 {
|
|
let fake_execution_address = &hash(&pubkey.as_ssz_bytes())[0..20];
|
|
let mut credentials = [0u8; 32];
|
|
credentials[0] = spec.builder_withdrawal_prefix_byte;
|
|
credentials[12..].copy_from_slice(fake_execution_address);
|
|
Hash256::from_slice(&credentials)
|
|
}
|
|
|
|
impl TestContext {
|
|
fn new() -> Self {
|
|
let spec = test_spec::<E>();
|
|
let store = Arc::new(
|
|
HotColdDB::open_ephemeral(StoreConfig::default(), Arc::new(spec.clone()))
|
|
.expect("should open ephemeral store"),
|
|
);
|
|
|
|
let keypairs = generate_deterministic_keypairs(NUM_VALIDATORS);
|
|
|
|
let mut state =
|
|
interop_genesis_state::<E>(&keypairs, 0, Hash256::repeat_byte(0x42), None, &spec)
|
|
.expect("should build genesis state");
|
|
|
|
// Register builders in the builder registry.
|
|
for keypair in keypairs.iter().take(NUM_BUILDERS) {
|
|
let creds = builder_withdrawal_credentials(&keypair.pk, &spec);
|
|
state
|
|
.add_builder_to_registry(
|
|
PublicKeyBytes::from(keypair.pk.clone()),
|
|
creds,
|
|
BUILDER_BALANCE,
|
|
Slot::new(0),
|
|
&spec,
|
|
)
|
|
.expect("should register builder");
|
|
}
|
|
|
|
// Bump finalized checkpoint epoch so builders are considered active
|
|
// (is_active_builder requires deposit_epoch < finalized_checkpoint.epoch).
|
|
*state.finalized_checkpoint_mut() = Checkpoint {
|
|
epoch: Epoch::new(1),
|
|
root: Hash256::ZERO,
|
|
};
|
|
|
|
let inactive_keypair = &keypairs[NUM_BUILDERS];
|
|
let inactive_creds = builder_withdrawal_credentials(&inactive_keypair.pk, &spec);
|
|
let inactive_builder_index = state
|
|
.add_builder_to_registry(
|
|
PublicKeyBytes::from(inactive_keypair.pk.clone()),
|
|
inactive_creds,
|
|
BUILDER_BALANCE,
|
|
Slot::new(E::slots_per_epoch()),
|
|
&spec,
|
|
)
|
|
.expect("should register inactive builder");
|
|
|
|
let mut block = genesis_block(&state, &spec).expect("should build genesis block");
|
|
*block.state_root_mut() = state
|
|
.update_tree_hash_cache()
|
|
.expect("should hash genesis state");
|
|
let signed_block = SignedBeaconBlock::from_block(block, Signature::empty());
|
|
let block_root = signed_block.canonical_root();
|
|
|
|
let snapshot = BeaconSnapshot::new(
|
|
Arc::new(signed_block.clone()),
|
|
None,
|
|
block_root,
|
|
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, block_root, &signed_block, &state, None, &spec)
|
|
.expect("should create fork choice");
|
|
|
|
let canonical_head =
|
|
CanonicalHead::new(fork_choice, Arc::new(snapshot), PayloadStatus::Pending);
|
|
|
|
let slot_clock = TestingSlotClock::new(
|
|
Slot::new(0),
|
|
Duration::from_secs(0),
|
|
spec.get_slot_duration(),
|
|
);
|
|
|
|
Self {
|
|
canonical_head,
|
|
bid_cache: GossipVerifiedPayloadBidCache::default(),
|
|
preferences_cache: GossipVerifiedProposerPreferenceCache::default(),
|
|
slot_clock,
|
|
keypairs,
|
|
spec,
|
|
genesis_block_root: block_root,
|
|
inactive_builder_index,
|
|
}
|
|
}
|
|
|
|
fn sign_bid(&self, bid: ExecutionPayloadBid<E>) -> Arc<SignedExecutionPayloadBid<E>> {
|
|
let head = self.canonical_head.cached_head();
|
|
let state = &head.snapshot.beacon_state;
|
|
let domain = self.spec.get_domain(
|
|
bid.slot.epoch(E::slots_per_epoch()),
|
|
Domain::BeaconBuilder,
|
|
&state.fork(),
|
|
state.genesis_validators_root(),
|
|
);
|
|
let message = bid.signing_root(domain);
|
|
let signature = self.keypairs[bid.builder_index as usize].sk.sign(message);
|
|
Arc::new(SignedExecutionPayloadBid {
|
|
message: bid,
|
|
signature,
|
|
})
|
|
}
|
|
|
|
fn gossip_ctx(&self) -> GossipVerificationContext<'_, T> {
|
|
GossipVerificationContext {
|
|
canonical_head: &self.canonical_head,
|
|
gossip_verified_payload_bid_cache: &self.bid_cache,
|
|
gossip_verified_proposer_preferences_cache: &self.preferences_cache,
|
|
slot_clock: &self.slot_clock,
|
|
spec: &self.spec,
|
|
}
|
|
}
|
|
|
|
fn insert_non_canonical_block(&self) -> Hash256 {
|
|
let shuffling_id = AttestationShufflingId {
|
|
shuffling_epoch: Epoch::new(0),
|
|
shuffling_decision_block: self.genesis_block_root,
|
|
};
|
|
let fork_block_root = Hash256::repeat_byte(0xab);
|
|
let mut fc = self.canonical_head.fork_choice_write_lock();
|
|
fc.proto_array_mut()
|
|
.process_block::<E>(
|
|
ProtoBlock {
|
|
slot: Slot::new(1),
|
|
root: fork_block_root,
|
|
parent_root: Some(self.genesis_block_root),
|
|
target_root: fork_block_root,
|
|
current_epoch_shuffling_id: shuffling_id.clone(),
|
|
next_epoch_shuffling_id: shuffling_id,
|
|
state_root: Hash256::ZERO,
|
|
justified_checkpoint: Checkpoint {
|
|
epoch: Epoch::new(0),
|
|
root: self.genesis_block_root,
|
|
},
|
|
finalized_checkpoint: Checkpoint {
|
|
epoch: Epoch::new(0),
|
|
root: self.genesis_block_root,
|
|
},
|
|
execution_status: ExecutionStatus::irrelevant(),
|
|
unrealized_justified_checkpoint: None,
|
|
unrealized_finalized_checkpoint: None,
|
|
execution_payload_parent_hash: Some(ExecutionBlockHash::zero()),
|
|
execution_payload_block_hash: Some(ExecutionBlockHash::repeat_byte(0xab)),
|
|
proposer_index: Some(0),
|
|
},
|
|
Slot::new(1),
|
|
&self.spec,
|
|
Duration::from_secs(0),
|
|
)
|
|
.expect("should insert fork block");
|
|
fork_block_root
|
|
}
|
|
}
|
|
|
|
fn make_signed_bid(
|
|
slot: Slot,
|
|
builder_index: u64,
|
|
fee_recipient: Address,
|
|
gas_limit: u64,
|
|
value: u64,
|
|
parent_block_root: Hash256,
|
|
) -> Arc<SignedExecutionPayloadBid<E>> {
|
|
Arc::new(SignedExecutionPayloadBid {
|
|
message: ExecutionPayloadBid {
|
|
slot,
|
|
builder_index,
|
|
fee_recipient,
|
|
gas_limit,
|
|
value,
|
|
parent_block_root,
|
|
..ExecutionPayloadBid::default()
|
|
},
|
|
signature: Signature::empty(),
|
|
})
|
|
}
|
|
|
|
fn make_signed_preferences(
|
|
proposal_slot: Slot,
|
|
validator_index: u64,
|
|
fee_recipient: Address,
|
|
gas_limit: u64,
|
|
) -> Arc<SignedProposerPreferences> {
|
|
Arc::new(SignedProposerPreferences {
|
|
message: ProposerPreferences {
|
|
proposal_slot,
|
|
validator_index,
|
|
fee_recipient,
|
|
gas_limit,
|
|
},
|
|
signature: Signature::empty(),
|
|
})
|
|
}
|
|
|
|
fn seed_preferences(ctx: &TestContext, slot: Slot, fee_recipient: Address, gas_limit: u64) {
|
|
let prefs = GossipVerifiedProposerPreferences {
|
|
signed_preferences: make_signed_preferences(slot, 0, fee_recipient, gas_limit),
|
|
};
|
|
ctx.preferences_cache.insert_preferences(prefs);
|
|
}
|
|
|
|
#[test]
|
|
fn no_proposer_preferences_for_slot() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let bid = make_signed_bid(
|
|
Slot::new(0),
|
|
0,
|
|
Address::ZERO,
|
|
30_000_000,
|
|
100,
|
|
Hash256::ZERO,
|
|
);
|
|
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::NoProposerPreferences { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn builder_already_seen_for_slot() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let bid = make_signed_bid(slot, 42, Address::ZERO, 30_000_000, 100, Hash256::ZERO);
|
|
let verified = GossipVerifiedPayloadBid {
|
|
signed_bid: bid.clone(),
|
|
};
|
|
ctx.bid_cache.insert_seen_builder(&verified);
|
|
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::BuilderAlreadySeen {
|
|
builder_index: 42,
|
|
..
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn bid_value_below_cached() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let high_bid = GossipVerifiedPayloadBid {
|
|
signed_bid: make_signed_bid(slot, 99, Address::ZERO, 30_000_000, 500, Hash256::ZERO),
|
|
};
|
|
ctx.bid_cache.insert_highest_bid(high_bid);
|
|
|
|
let low_bid = make_signed_bid(slot, 1, Address::ZERO, 30_000_000, 100, Hash256::ZERO);
|
|
let result = GossipVerifiedPayloadBid::new(low_bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::BidValueBelowCached { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_bid_slot() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(5);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let bid = make_signed_bid(
|
|
slot,
|
|
0,
|
|
Address::ZERO,
|
|
30_000_000,
|
|
100,
|
|
ctx.genesis_block_root,
|
|
);
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::InvalidBidSlot { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn fee_recipient_mismatch() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::repeat_byte(0xaa), 30_000_000);
|
|
|
|
let bid = make_signed_bid(
|
|
slot,
|
|
0,
|
|
Address::ZERO,
|
|
30_000_000,
|
|
100,
|
|
ctx.genesis_block_root,
|
|
);
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(result, Err(PayloadBidError::InvalidFeeRecipient)));
|
|
}
|
|
|
|
#[test]
|
|
fn gas_limit_mismatch() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let bid = make_signed_bid(
|
|
slot,
|
|
0,
|
|
Address::ZERO,
|
|
50_000_000,
|
|
100,
|
|
ctx.genesis_block_root,
|
|
);
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(result, Err(PayloadBidError::InvalidGasLimit)));
|
|
}
|
|
|
|
#[test]
|
|
fn execution_payment_nonzero() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let bid = Arc::new(SignedExecutionPayloadBid {
|
|
message: ExecutionPayloadBid {
|
|
slot,
|
|
gas_limit: 30_000_000,
|
|
execution_payment: 42,
|
|
parent_block_root: ctx.genesis_block_root,
|
|
..ExecutionPayloadBid::default()
|
|
},
|
|
signature: Signature::empty(),
|
|
});
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::ExecutionPaymentNonZero { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_builder_index() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
// Use a builder_index that doesn't exist in the registry.
|
|
let bid = make_signed_bid(
|
|
slot,
|
|
9999,
|
|
Address::ZERO,
|
|
30_000_000,
|
|
100,
|
|
ctx.genesis_block_root,
|
|
);
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::InvalidBuilder {
|
|
builder_index: 9999
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn inactive_builder() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let bid = make_signed_bid(
|
|
slot,
|
|
ctx.inactive_builder_index,
|
|
Address::ZERO,
|
|
30_000_000,
|
|
100,
|
|
ctx.genesis_block_root,
|
|
);
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::InvalidBuilder { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn builder_cant_cover_bid() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
// Builder index 0 exists but bid value far exceeds their balance.
|
|
let bid = make_signed_bid(
|
|
slot,
|
|
0,
|
|
Address::ZERO,
|
|
30_000_000,
|
|
u64::MAX,
|
|
ctx.genesis_block_root,
|
|
);
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::BuilderCantCoverBid { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parent_block_root_unknown() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
// Parent block root not in fork choice.
|
|
let unknown_root = Hash256::repeat_byte(0xff);
|
|
let bid = make_signed_bid(slot, 0, Address::ZERO, 30_000_000, 0, unknown_root);
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(result.is_err(), "expected error, got Ok");
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
matches!(err, PayloadBidError::ParentBlockRootUnknown { .. }),
|
|
"expected ParentBlockRootUnknown, got: {err:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parent_block_root_not_canonical() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let fork_root = ctx.insert_non_canonical_block();
|
|
let bid = make_signed_bid(slot, 0, Address::ZERO, 30_000_000, 0, fork_root);
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(result.is_err(), "expected error, got Ok");
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
matches!(err, PayloadBidError::ParentBlockRootNotCanonical { .. }),
|
|
"expected ParentBlockRootNotCanonical, got: {err:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_blob_kzg_commitments() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let max_blobs = ctx
|
|
.spec
|
|
.max_blobs_per_block(slot.epoch(E::slots_per_epoch())) as usize;
|
|
let commitments: Vec<KzgCommitment> = (0..=max_blobs)
|
|
.map(|_| KzgCommitment::empty_for_testing())
|
|
.collect();
|
|
|
|
let bid = Arc::new(SignedExecutionPayloadBid {
|
|
message: ExecutionPayloadBid {
|
|
slot,
|
|
builder_index: 0,
|
|
fee_recipient: Address::ZERO,
|
|
gas_limit: 30_000_000,
|
|
value: 0,
|
|
parent_block_root: ctx.genesis_block_root,
|
|
blob_kzg_commitments: VariableList::new(commitments).unwrap(),
|
|
..ExecutionPayloadBid::default()
|
|
},
|
|
signature: Signature::empty(),
|
|
});
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::InvalidBlobKzgCommitments { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn bad_signature() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
// All checks pass but signature is empty/invalid.
|
|
let bid = make_signed_bid(
|
|
slot,
|
|
0,
|
|
Address::ZERO,
|
|
30_000_000,
|
|
0,
|
|
ctx.genesis_block_root,
|
|
);
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(matches!(result, Err(PayloadBidError::BadSignature)));
|
|
assert!(!ctx.bid_cache.seen_builder_index(&slot, 0));
|
|
assert!(
|
|
ctx.bid_cache
|
|
.get_highest_bid(slot, ExecutionBlockHash::zero(), ctx.genesis_block_root)
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_bid() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let bid = ctx.sign_bid(ExecutionPayloadBid {
|
|
slot,
|
|
builder_index: 0,
|
|
fee_recipient: Address::ZERO,
|
|
gas_limit: 30_000_000,
|
|
value: 0,
|
|
parent_block_root: ctx.genesis_block_root,
|
|
..ExecutionPayloadBid::default()
|
|
});
|
|
let result = GossipVerifiedPayloadBid::new(bid, &gossip);
|
|
assert!(
|
|
result.is_ok(),
|
|
"expected Ok, got: {:?}",
|
|
result.unwrap_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn two_builders_coexist_in_cache() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
let bid_0 = ctx.sign_bid(ExecutionPayloadBid {
|
|
slot,
|
|
builder_index: 0,
|
|
fee_recipient: Address::ZERO,
|
|
gas_limit: 30_000_000,
|
|
value: 0,
|
|
parent_block_root: ctx.genesis_block_root,
|
|
..ExecutionPayloadBid::default()
|
|
});
|
|
let result_0 = GossipVerifiedPayloadBid::new(bid_0, &gossip);
|
|
assert!(
|
|
result_0.is_ok(),
|
|
"builder 0 should pass: {:?}",
|
|
result_0.unwrap_err()
|
|
);
|
|
|
|
// Builder 1 must bid strictly higher than builder 0's cached value.
|
|
let bid_1 = ctx.sign_bid(ExecutionPayloadBid {
|
|
slot,
|
|
builder_index: 1,
|
|
fee_recipient: Address::ZERO,
|
|
gas_limit: 30_000_000,
|
|
value: 1,
|
|
parent_block_root: ctx.genesis_block_root,
|
|
..ExecutionPayloadBid::default()
|
|
});
|
|
let result_1 = GossipVerifiedPayloadBid::new(bid_1, &gossip);
|
|
assert!(
|
|
result_1.is_ok(),
|
|
"builder 1 should pass: {:?}",
|
|
result_1.unwrap_err()
|
|
);
|
|
|
|
// Both builders should be seen.
|
|
assert!(ctx.bid_cache.seen_builder_index(&slot, 0));
|
|
assert!(ctx.bid_cache.seen_builder_index(&slot, 1));
|
|
|
|
let highest = ctx
|
|
.bid_cache
|
|
.get_highest_bid(slot, ExecutionBlockHash::zero(), ctx.genesis_block_root)
|
|
.expect("should have highest bid");
|
|
assert_eq!(highest.message.value, 1);
|
|
assert_eq!(highest.message.builder_index, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn bid_equal_to_cached_value_rejected() {
|
|
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
|
|
return;
|
|
}
|
|
let ctx = TestContext::new();
|
|
let gossip = ctx.gossip_ctx();
|
|
let slot = Slot::new(0);
|
|
seed_preferences(&ctx, slot, Address::ZERO, 30_000_000);
|
|
|
|
// Seed a cached bid with value 100.
|
|
let high_bid = GossipVerifiedPayloadBid {
|
|
signed_bid: make_signed_bid(
|
|
slot,
|
|
99,
|
|
Address::ZERO,
|
|
30_000_000,
|
|
100,
|
|
ctx.genesis_block_root,
|
|
),
|
|
};
|
|
ctx.bid_cache.insert_highest_bid(high_bid);
|
|
|
|
// Submit a bid with exactly the same value — should be rejected.
|
|
let equal_bid = make_signed_bid(
|
|
slot,
|
|
1,
|
|
Address::ZERO,
|
|
30_000_000,
|
|
100,
|
|
ctx.genesis_block_root,
|
|
);
|
|
let result = GossipVerifiedPayloadBid::new(equal_bid, &gossip);
|
|
assert!(matches!(
|
|
result,
|
|
Err(PayloadBidError::BidValueBelowCached {
|
|
cached_value: 100,
|
|
incoming_value: 100,
|
|
})
|
|
));
|
|
}
|