Merge branch 'capella' into unstable

This commit is contained in:
Michael Sproul
2023-02-22 10:25:45 +11:00
267 changed files with 11892 additions and 2096 deletions
+2
View File
@@ -37,6 +37,7 @@ sysinfo = "0.26.5"
system_health = { path = "../../common/system_health" }
directory = { path = "../../common/directory" }
eth2_serde_utils = "0.1.1"
operation_pool = { path = "../operation_pool" }
[dev-dependencies]
store = { path = "../store" }
@@ -46,6 +47,7 @@ logging = { path = "../../common/logging" }
serde_json = "1.0.58"
proto_array = { path = "../../consensus/proto_array" }
unused_port = {path = "../../common/unused_port"}
genesis = { path = "../genesis" }
[[test]]
name = "bn_http_api_tests"
+1 -1
View File
@@ -4,7 +4,7 @@ use lru::LruCache;
use slog::{debug, warn, Logger};
use state_processing::BlockReplayer;
use std::sync::Arc;
use types::BlindedBeaconBlock;
use types::beacon_block::BlindedBeaconBlock;
use warp_utils::reject::{
beacon_chain_error, beacon_state_error, custom_bad_request, custom_server_error,
};
+112 -4
View File
@@ -36,6 +36,7 @@ use eth2::types::{
use lighthouse_network::{types::SyncState, EnrExt, NetworkGlobals, PeerId, PubsubMessage};
use lighthouse_version::version_with_platform;
use network::{NetworkMessage, NetworkSenders, ValidatorSubscriptionMessage};
use operation_pool::ReceivedPreCapella;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use slog::{crit, debug, error, info, warn, Logger};
@@ -56,9 +57,9 @@ use types::{
Attestation, AttestationData, AttesterSlashing, BeaconStateError, BlindedPayload,
CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, FullPayload,
ProposerPreparationData, ProposerSlashing, RelativeEpoch, SignedAggregateAndProof,
SignedBeaconBlock, SignedBlindedBeaconBlock, SignedContributionAndProof,
SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncCommitteeMessage,
SyncContributionData,
SignedBeaconBlock, SignedBlindedBeaconBlock, SignedBlsToExecutionChange,
SignedContributionAndProof, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot,
SyncCommitteeMessage, SyncContributionData,
};
use version::{
add_consensus_version_header, execution_optimistic_fork_versioned_response,
@@ -1122,7 +1123,9 @@ pub fn serve<T: BeaconChainTypes>(
chain: Arc<BeaconChain<T>>,
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
log: Logger| async move {
publish_blocks::publish_block(None, block, chain, &network_tx, log)
// need to have cached the blob sidecar somewhere in the beacon chain
// to publish
publish_blocks::publish_block(None, block, None, chain, &network_tx, log)
.await
.map(|()| warp::reply())
},
@@ -1654,6 +1657,109 @@ pub fn serve<T: BeaconChainTypes>(
},
);
// GET beacon/pool/bls_to_execution_changes
let get_beacon_pool_bls_to_execution_changes = beacon_pool_path
.clone()
.and(warp::path("bls_to_execution_changes"))
.and(warp::path::end())
.and_then(|chain: Arc<BeaconChain<T>>| {
blocking_json_task(move || {
let address_changes = chain.op_pool.get_all_bls_to_execution_changes();
Ok(api_types::GenericResponse::from(address_changes))
})
});
// POST beacon/pool/bls_to_execution_changes
let post_beacon_pool_bls_to_execution_changes = beacon_pool_path
.clone()
.and(warp::path("bls_to_execution_changes"))
.and(warp::path::end())
.and(warp::body::json())
.and(network_tx_filter.clone())
.and(log_filter.clone())
.and_then(
|chain: Arc<BeaconChain<T>>,
address_changes: Vec<SignedBlsToExecutionChange>,
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
log: Logger| {
blocking_json_task(move || {
let mut failures = vec![];
for (index, address_change) in address_changes.into_iter().enumerate() {
let validator_index = address_change.message.validator_index;
match chain.verify_bls_to_execution_change_for_http_api(address_change) {
Ok(ObservationOutcome::New(verified_address_change)) => {
let validator_index =
verified_address_change.as_inner().message.validator_index;
let address = verified_address_change
.as_inner()
.message
.to_execution_address;
// New to P2P *and* op pool, gossip immediately if post-Capella.
let received_pre_capella = if chain.current_slot_is_post_capella().unwrap_or(false) {
ReceivedPreCapella::No
} else {
ReceivedPreCapella::Yes
};
if matches!(received_pre_capella, ReceivedPreCapella::No) {
publish_pubsub_message(
&network_tx,
PubsubMessage::BlsToExecutionChange(Box::new(
verified_address_change.as_inner().clone(),
)),
)?;
}
// Import to op pool (may return `false` if there's a race).
let imported =
chain.import_bls_to_execution_change(verified_address_change, received_pre_capella);
info!(
log,
"Processed BLS to execution change";
"validator_index" => validator_index,
"address" => ?address,
"published" => matches!(received_pre_capella, ReceivedPreCapella::No),
"imported" => imported,
);
}
Ok(ObservationOutcome::AlreadyKnown) => {
debug!(
log,
"BLS to execution change already known";
"validator_index" => validator_index,
);
}
Err(e) => {
warn!(
log,
"Invalid BLS to execution change";
"validator_index" => validator_index,
"reason" => ?e,
"source" => "HTTP",
);
failures.push(api_types::Failure::new(
index,
format!("invalid: {e:?}"),
));
}
}
}
if failures.is_empty() {
Ok(())
} else {
Err(warp_utils::reject::indexed_bad_request(
"some BLS to execution changes failed to verify".into(),
failures,
))
}
})
},
);
// GET beacon/deposit_snapshot
let get_beacon_deposit_snapshot = eth_v1
.and(warp::path("beacon"))
@@ -3470,6 +3576,7 @@ pub fn serve<T: BeaconChainTypes>(
.or(get_beacon_pool_attester_slashings.boxed())
.or(get_beacon_pool_proposer_slashings.boxed())
.or(get_beacon_pool_voluntary_exits.boxed())
.or(get_beacon_pool_bls_to_execution_changes.boxed())
.or(get_beacon_deposit_snapshot.boxed())
.or(get_beacon_rewards_blocks.boxed())
.or(get_config_fork_schedule.boxed())
@@ -3523,6 +3630,7 @@ pub fn serve<T: BeaconChainTypes>(
.or(post_beacon_pool_proposer_slashings.boxed())
.or(post_beacon_pool_voluntary_exits.boxed())
.or(post_beacon_pool_sync_committees.boxed())
.or(post_beacon_pool_bls_to_execution_changes.boxed())
.or(post_beacon_rewards_attestations.boxed())
.or(post_beacon_rewards_sync_committee.boxed())
.or(post_validator_duties_attester.boxed())
+12
View File
@@ -41,4 +41,16 @@ lazy_static::lazy_static! {
"http_api_block_published_very_late_total",
"The count of times a block was published beyond the attestation deadline"
);
pub static ref HTTP_API_BLOB_BROADCAST_DELAY_TIMES: Result<Histogram> = try_create_histogram(
"http_api_blob_broadcast_delay_times",
"Time between start of the slot and when the blob was broadcast"
);
pub static ref HTTP_API_BLOB_PUBLISHED_LATE_TOTAL: Result<IntCounter> = try_create_int_counter(
"http_api_blob_published_late_total",
"The count of times a blob was published beyond more than half way to the attestation deadline"
);
pub static ref HTTP_API_BLOB_PUBLISHED_VERY_LATE_TOTAL: Result<IntCounter> = try_create_int_counter(
"http_api_blob_published_very_late_total",
"The count of times a blob was published beyond the attestation deadline"
);
}
+36 -7
View File
@@ -3,7 +3,7 @@ use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now};
use beacon_chain::{
BeaconChain, BeaconChainTypes, BlockError, CountUnrealized, NotifyExecutionLayer,
};
use lighthouse_network::PubsubMessage;
use lighthouse_network::{PubsubMessage, SignedBeaconBlockAndBlobsSidecar};
use network::NetworkMessage;
use slog::{error, info, warn, Logger};
use slot_clock::SlotClock;
@@ -11,8 +11,8 @@ use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender;
use tree_hash::TreeHash;
use types::{
BlindedPayload, ExecPayload, ExecutionBlockHash, ExecutionPayload, FullPayload, Hash256,
SignedBeaconBlock,
AbstractExecPayload, BlindedPayload, BlobsSidecar, EthSpec, ExecPayload, ExecutionBlockHash,
FullPayload, Hash256, SignedBeaconBlock,
};
use warp::Rejection;
@@ -20,6 +20,7 @@ use warp::Rejection;
pub async fn publish_block<T: BeaconChainTypes>(
block_root: Option<Hash256>,
block: Arc<SignedBeaconBlock<T::EthSpec>>,
blobs_sidecar: Option<Arc<BlobsSidecar<T::EthSpec>>>,
chain: Arc<BeaconChain<T>>,
network_tx: &UnboundedSender<NetworkMessage<T::EthSpec>>,
log: Logger,
@@ -28,7 +29,24 @@ pub async fn publish_block<T: BeaconChainTypes>(
// Send the block, regardless of whether or not it is valid. The API
// specification is very clear that this is the desired behaviour.
crate::publish_pubsub_message(network_tx, PubsubMessage::BeaconBlock(block.clone()))?;
let message = match &*block {
SignedBeaconBlock::Eip4844(block) => {
if let Some(sidecar) = blobs_sidecar {
PubsubMessage::BeaconBlockAndBlobsSidecars(Arc::new(
SignedBeaconBlockAndBlobsSidecar {
beacon_block: block.clone(),
blobs_sidecar: (*sidecar).clone(),
},
))
} else {
//TODO(pawan): return an empty sidecar instead
return Err(warp_utils::reject::broadcast_without_import(String::new()));
}
}
_ => PubsubMessage::BeaconBlock(block.clone()),
};
crate::publish_pubsub_message(network_tx, message)?;
// Determine the delay after the start of the slot, register it with metrics.
let delay = get_block_delay_ms(seen_timestamp, block.message(), &chain.slot_clock);
@@ -142,6 +160,7 @@ pub async fn publish_blinded_block<T: BeaconChainTypes>(
publish_block::<T>(
Some(block_root),
Arc::new(full_block),
None,
chain,
network_tx,
log,
@@ -165,12 +184,22 @@ async fn reconstruct_block<T: BeaconChainTypes>(
// If the execution block hash is zero, use an empty payload.
let full_payload = if payload_header.block_hash() == ExecutionBlockHash::zero() {
ExecutionPayload::default()
FullPayload::default_at_fork(
chain
.spec
.fork_name_at_epoch(block.slot().epoch(T::EthSpec::slots_per_epoch())),
)
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(
"Default payload construction error: {e:?}"
))
})?
.into()
// If we already have an execution payload with this transactions root cached, use it.
} else if let Some(cached_payload) =
el.get_payload_by_root(&payload_header.tree_hash_root())
{
info!(log, "Reconstructing a full block using a local payload"; "block_hash" => ?cached_payload.block_hash);
info!(log, "Reconstructing a full block using a local payload"; "block_hash" => ?cached_payload.block_hash());
cached_payload
// Otherwise, this means we are attempting a blind block proposal.
} else {
@@ -183,7 +212,7 @@ async fn reconstruct_block<T: BeaconChainTypes>(
e
))
})?;
info!(log, "Successfully published a block to the builder network"; "block_hash" => ?full_payload.block_hash);
info!(log, "Successfully published a block to the builder network"; "block_hash" => ?full_payload.block_hash());
full_payload
};
+4 -4
View File
@@ -1,9 +1,9 @@
use crate::api_types::{
EndpointVersion, ExecutionOptimisticForkVersionedResponse, ForkVersionedResponse,
};
use crate::api_types::EndpointVersion;
use eth2::CONSENSUS_VERSION_HEADER;
use serde::Serialize;
use types::{ForkName, InconsistentFork};
use types::{
ExecutionOptimisticForkVersionedResponse, ForkName, ForkVersionedResponse, InconsistentFork,
};
use warp::reply::{self, Reply, WithHeader};
pub const V1: EndpointVersion = EndpointVersion(1);
+22 -6
View File
@@ -1,5 +1,7 @@
use beacon_chain::{
test_utils::{BeaconChainHarness, BoxedMutator, EphemeralHarnessType},
test_utils::{
BeaconChainHarness, BoxedMutator, Builder as HarnessBuilder, EphemeralHarnessType,
},
BeaconChain, BeaconChainTypes,
};
use directory::DEFAULT_ROOT_DIR;
@@ -55,25 +57,39 @@ pub struct ApiServer<E: EthSpec, SFut: Future<Output = ()>> {
pub external_peer_id: PeerId,
}
type Initializer<E> = Box<
dyn FnOnce(HarnessBuilder<EphemeralHarnessType<E>>) -> HarnessBuilder<EphemeralHarnessType<E>>,
>;
type Mutator<E> = BoxedMutator<E, MemoryStore<E>, MemoryStore<E>>;
impl<E: EthSpec> InteractiveTester<E> {
pub async fn new(spec: Option<ChainSpec>, validator_count: usize) -> Self {
Self::new_with_mutator(spec, validator_count, None).await
Self::new_with_initializer_and_mutator(spec, validator_count, None, None).await
}
pub async fn new_with_mutator(
pub async fn new_with_initializer_and_mutator(
spec: Option<ChainSpec>,
validator_count: usize,
initializer: Option<Initializer<E>>,
mutator: Option<Mutator<E>>,
) -> Self {
let mut harness_builder = BeaconChainHarness::builder(E::default())
.spec_or_default(spec)
.deterministic_keypairs(validator_count)
.logger(test_logger())
.mock_execution_layer()
.fresh_ephemeral_store();
.mock_execution_layer();
harness_builder = if let Some(initializer) = initializer {
// Apply custom initialization provided by the caller.
initializer(harness_builder)
} else {
// Apply default initial configuration.
harness_builder
.deterministic_keypairs(validator_count)
.fresh_ephemeral_store()
};
// Add a mutator for the beacon chain builder which will be called in
// `HarnessBuilder::build`.
if let Some(mutator) = mutator {
harness_builder = harness_builder.initial_mutator(mutator);
}
+235 -3
View File
@@ -1,8 +1,16 @@
//! Tests for API behaviour across fork boundaries.
use crate::common::*;
use beacon_chain::{test_utils::RelativeSyncCommittee, StateSkipConfig};
use eth2::types::{StateId, SyncSubcommittee};
use types::{ChainSpec, Epoch, EthSpec, MinimalEthSpec, Slot};
use beacon_chain::{
test_utils::{RelativeSyncCommittee, DEFAULT_ETH1_BLOCK_HASH, HARNESS_GENESIS_TIME},
StateSkipConfig,
};
use eth2::types::{IndexedErrorMessage, StateId, SyncSubcommittee};
use genesis::{bls_withdrawal_credentials, interop_genesis_state_with_withdrawal_credentials};
use std::collections::HashSet;
use types::{
test_utils::{generate_deterministic_keypair, generate_deterministic_keypairs},
Address, ChainSpec, Epoch, EthSpec, Hash256, MinimalEthSpec, Slot,
};
type E = MinimalEthSpec;
@@ -12,6 +20,14 @@ fn altair_spec(altair_fork_epoch: Epoch) -> ChainSpec {
spec
}
fn capella_spec(capella_fork_epoch: Epoch) -> ChainSpec {
let mut spec = E::default_spec();
spec.altair_fork_epoch = Some(Epoch::new(0));
spec.bellatrix_fork_epoch = Some(Epoch::new(0));
spec.capella_fork_epoch = Some(capella_fork_epoch);
spec
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn sync_committee_duties_across_fork() {
let validator_count = E::sync_committee_size();
@@ -307,3 +323,219 @@ async fn sync_committee_indices_across_fork() {
);
}
}
/// Assert that an HTTP API error has the given status code and indexed errors for the given indices.
fn assert_server_indexed_error(error: eth2::Error, status_code: u16, indices: Vec<usize>) {
let eth2::Error::ServerIndexedMessage(IndexedErrorMessage {
code,
failures,
..
}) = error else {
panic!("wrong error, expected ServerIndexedMessage, got: {error:?}")
};
assert_eq!(code, status_code);
assert_eq!(failures.len(), indices.len());
for (index, failure) in indices.into_iter().zip(failures) {
assert_eq!(failure.index, index as u64);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn bls_to_execution_changes_update_all_around_capella_fork() {
let validator_count = 128;
let fork_epoch = Epoch::new(2);
let spec = capella_spec(fork_epoch);
let max_bls_to_execution_changes = E::max_bls_to_execution_changes();
// Use a genesis state with entirely BLS withdrawal credentials.
// Offset keypairs by `validator_count` to create keys distinct from the signing keys.
let validator_keypairs = generate_deterministic_keypairs(validator_count);
let withdrawal_keypairs = (0..validator_count)
.map(|i| Some(generate_deterministic_keypair(i + validator_count)))
.collect::<Vec<_>>();
let withdrawal_credentials = withdrawal_keypairs
.iter()
.map(|keypair| bls_withdrawal_credentials(&keypair.as_ref().unwrap().pk, &spec))
.collect::<Vec<_>>();
let genesis_state = interop_genesis_state_with_withdrawal_credentials(
&validator_keypairs,
&withdrawal_credentials,
HARNESS_GENESIS_TIME,
Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH),
None,
&spec,
)
.unwrap();
let tester = InteractiveTester::<E>::new_with_initializer_and_mutator(
Some(spec.clone()),
validator_count,
Some(Box::new(|harness_builder| {
harness_builder
.keypairs(validator_keypairs)
.withdrawal_keypairs(withdrawal_keypairs)
.genesis_state_ephemeral_store(genesis_state)
})),
None,
)
.await;
let harness = &tester.harness;
let client = &tester.client;
let all_validators = harness.get_all_validators();
let all_validators_u64 = all_validators.iter().map(|x| *x as u64).collect::<Vec<_>>();
// Create a bunch of valid address changes.
let valid_address_changes = all_validators_u64
.iter()
.map(|&validator_index| {
harness.make_bls_to_execution_change(
validator_index,
Address::from_low_u64_be(validator_index),
)
})
.collect::<Vec<_>>();
// Address changes which conflict with `valid_address_changes` on the address chosen.
let conflicting_address_changes = all_validators_u64
.iter()
.map(|&validator_index| {
harness.make_bls_to_execution_change(
validator_index,
Address::from_low_u64_be(validator_index + 1),
)
})
.collect::<Vec<_>>();
// Address changes signed with the wrong key.
let wrong_key_address_changes = all_validators_u64
.iter()
.map(|&validator_index| {
// Use the correct pubkey.
let pubkey = &harness.get_withdrawal_keypair(validator_index).pk;
// And the wrong secret key.
let secret_key = &harness
.get_withdrawal_keypair((validator_index + 1) % validator_count as u64)
.sk;
harness.make_bls_to_execution_change_with_keys(
validator_index,
Address::from_low_u64_be(validator_index),
pubkey,
secret_key,
)
})
.collect::<Vec<_>>();
// Submit some changes before Capella. Just enough to fill two blocks.
let num_pre_capella = validator_count / 4;
let blocks_filled_pre_capella = 2;
assert_eq!(
num_pre_capella,
blocks_filled_pre_capella * max_bls_to_execution_changes
);
client
.post_beacon_pool_bls_to_execution_changes(&valid_address_changes[..num_pre_capella])
.await
.unwrap();
let expected_received_pre_capella_messages = valid_address_changes[..num_pre_capella].to_vec();
// Conflicting changes for the same validators should all fail.
let error = client
.post_beacon_pool_bls_to_execution_changes(&conflicting_address_changes[..num_pre_capella])
.await
.unwrap_err();
assert_server_indexed_error(error, 400, (0..num_pre_capella).collect());
// Re-submitting the same changes should be accepted.
client
.post_beacon_pool_bls_to_execution_changes(&valid_address_changes[..num_pre_capella])
.await
.unwrap();
// Invalid changes signed with the wrong keys should all be rejected without affecting the seen
// indices filters (apply ALL of them).
let error = client
.post_beacon_pool_bls_to_execution_changes(&wrong_key_address_changes)
.await
.unwrap_err();
assert_server_indexed_error(error, 400, all_validators.clone());
// Advance to right before Capella.
let capella_slot = fork_epoch.start_slot(E::slots_per_epoch());
harness.extend_to_slot(capella_slot - 1).await;
assert_eq!(harness.head_slot(), capella_slot - 1);
assert_eq!(
harness
.chain
.op_pool
.get_bls_to_execution_changes_received_pre_capella(
&harness.chain.head_snapshot().beacon_state,
&spec,
)
.into_iter()
.collect::<HashSet<_>>(),
HashSet::from_iter(expected_received_pre_capella_messages.into_iter()),
"all pre-capella messages should be queued for capella broadcast"
);
// Add Capella blocks which should be full of BLS to execution changes.
for i in 0..validator_count / max_bls_to_execution_changes {
let head_block_root = harness.extend_slots(1).await;
let head_block = harness
.chain
.get_block(&head_block_root)
.await
.unwrap()
.unwrap();
let bls_to_execution_changes = head_block
.message()
.body()
.bls_to_execution_changes()
.unwrap();
// Block should be full.
assert_eq!(
bls_to_execution_changes.len(),
max_bls_to_execution_changes,
"block not full on iteration {i}"
);
// Included changes should be the ones from `valid_address_changes` in any order.
for address_change in bls_to_execution_changes.iter() {
assert!(valid_address_changes.contains(address_change));
}
// After the initial 2 blocks, add the rest of the changes using a large
// request containing all the valid, all the conflicting and all the invalid.
// Despite the invalid and duplicate messages, the new ones should still get picked up by
// the pool.
if i == blocks_filled_pre_capella - 1 {
let all_address_changes: Vec<_> = [
valid_address_changes.clone(),
conflicting_address_changes.clone(),
wrong_key_address_changes.clone(),
]
.concat();
let error = client
.post_beacon_pool_bls_to_execution_changes(&all_address_changes)
.await
.unwrap_err();
assert_server_indexed_error(
error,
400,
(validator_count..3 * validator_count).collect(),
);
}
}
// Eventually all validators should have eth1 withdrawal credentials.
let head_state = harness.get_current_state();
for validator in head_state.validators() {
assert!(validator.has_eth1_withdrawal_credential(&spec));
}
}
+15 -10
View File
@@ -5,7 +5,7 @@ use beacon_chain::{
test_utils::{AttestationStrategy, BlockStrategy},
};
use eth2::types::DepositContractData;
use execution_layer::{ForkChoiceState, PayloadAttributes};
use execution_layer::{ForkchoiceState, PayloadAttributes};
use parking_lot::Mutex;
use slot_clock::SlotClock;
use state_processing::state_advance::complete_state_advance;
@@ -55,7 +55,7 @@ struct ForkChoiceUpdates {
#[derive(Debug, Clone)]
struct ForkChoiceUpdateMetadata {
received_at: Duration,
state: ForkChoiceState,
state: ForkchoiceState,
payload_attributes: Option<PayloadAttributes>,
}
@@ -86,7 +86,7 @@ impl ForkChoiceUpdates {
.payload_attributes
.as_ref()
.map_or(false, |payload_attributes| {
payload_attributes.timestamp == proposal_timestamp
payload_attributes.timestamp() == proposal_timestamp
})
})
.cloned()
@@ -278,9 +278,10 @@ pub async fn proposer_boost_re_org_test(
let num_empty_votes = Some(attesters_per_slot * percent_empty_votes / 100);
let num_head_votes = Some(attesters_per_slot * percent_head_votes / 100);
let tester = InteractiveTester::<E>::new_with_mutator(
let tester = InteractiveTester::<E>::new_with_initializer_and_mutator(
Some(spec),
validator_count,
None,
Some(Box::new(move |builder| {
builder
.proposer_re_org_threshold(Some(ReOrgThreshold(re_org_threshold)))
@@ -342,7 +343,7 @@ pub async fn proposer_boost_re_org_test(
.lock()
.set_forkchoice_updated_hook(Box::new(move |state, payload_attributes| {
let received_at = chain_inner.slot_clock.now_duration().unwrap();
let state = ForkChoiceState::from(state);
let state = ForkchoiceState::from(state);
let payload_attributes = payload_attributes.map(Into::into);
let update = ForkChoiceUpdateMetadata {
received_at,
@@ -521,16 +522,20 @@ pub async fn proposer_boost_re_org_test(
if !misprediction {
assert_eq!(
lookahead, payload_lookahead,
lookahead,
payload_lookahead,
"lookahead={lookahead:?}, timestamp={}, prev_randao={:?}",
payload_attribs.timestamp, payload_attribs.prev_randao,
payload_attribs.timestamp(),
payload_attribs.prev_randao(),
);
} else {
// On a misprediction we issue the first fcU 500ms before creating a block!
assert_eq!(
lookahead, fork_choice_lookahead,
lookahead,
fork_choice_lookahead,
"timestamp={}, prev_randao={:?}",
payload_attribs.timestamp, payload_attribs.prev_randao,
payload_attribs.timestamp(),
payload_attribs.prev_randao(),
);
}
}
@@ -540,7 +545,7 @@ pub async fn proposer_boost_re_org_test(
pub async fn fork_choice_before_proposal() {
// Validator count needs to be at least 32 or proposer boost gets set to 0 when computing
// `validator_count // 32`.
let validator_count = 32;
let validator_count = 64;
let all_validators = (0..validator_count).collect::<Vec<_>>();
let num_initial: u64 = 31;
+339 -83
View File
@@ -11,9 +11,11 @@ use eth2::{
types::{BlockId as CoreBlockId, StateId as CoreStateId, *},
BeaconNodeHttpClient, Error, StatusCode, Timeouts,
};
use execution_layer::test_utils::Operation;
use execution_layer::test_utils::TestingBuilder;
use execution_layer::test_utils::DEFAULT_BUILDER_THRESHOLD_WEI;
use execution_layer::test_utils::{
Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI,
};
use futures::stream::{Stream, StreamExt};
use futures::FutureExt;
use http_api::{BlockId, StateId};
@@ -22,6 +24,7 @@ use network::NetworkReceivers;
use proto_array::ExecutionStatus;
use sensitive_url::SensitiveUrl;
use slot_clock::SlotClock;
use state_processing::per_block_processing::get_expected_withdrawals;
use state_processing::per_slot_processing;
use std::convert::TryInto;
use std::sync::Arc;
@@ -72,38 +75,53 @@ struct ApiTester {
mock_builder: Option<Arc<TestingBuilder<E>>>,
}
struct ApiTesterConfig {
spec: ChainSpec,
builder_threshold: Option<u128>,
}
impl Default for ApiTesterConfig {
fn default() -> Self {
let mut spec = E::default_spec();
spec.shard_committee_period = 2;
Self {
spec,
builder_threshold: None,
}
}
}
impl ApiTester {
pub async fn new() -> Self {
// This allows for testing voluntary exits without building out a massive chain.
let mut spec = E::default_spec();
spec.shard_committee_period = 2;
Self::new_from_spec(spec).await
Self::new_from_config(ApiTesterConfig::default()).await
}
pub async fn new_with_hard_forks(altair: bool, bellatrix: bool) -> Self {
let mut spec = E::default_spec();
spec.shard_committee_period = 2;
let mut config = ApiTesterConfig::default();
// Set whether the chain has undergone each hard fork.
if altair {
spec.altair_fork_epoch = Some(Epoch::new(0));
config.spec.altair_fork_epoch = Some(Epoch::new(0));
}
if bellatrix {
spec.bellatrix_fork_epoch = Some(Epoch::new(0));
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
}
Self::new_from_spec(spec).await
Self::new_from_config(config).await
}
pub async fn new_from_spec(spec: ChainSpec) -> Self {
pub async fn new_from_config(config: ApiTesterConfig) -> Self {
// Get a random unused port
let spec = config.spec;
let port = unused_port::unused_tcp_port().unwrap();
let beacon_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
let harness = Arc::new(
BeaconChainHarness::builder(MainnetEthSpec)
.spec(spec.clone())
.logger(logging::test_logger())
.deterministic_keypairs(VALIDATOR_COUNT)
.fresh_ephemeral_store()
.mock_execution_layer_with_builder(beacon_url.clone())
.mock_execution_layer_with_builder(beacon_url.clone(), config.builder_threshold)
.build(),
);
@@ -358,6 +376,28 @@ impl ApiTester {
tester
}
pub async fn new_mev_tester_no_builder_threshold() -> Self {
let mut config = ApiTesterConfig {
builder_threshold: Some(0),
spec: E::default_spec(),
};
config.spec.altair_fork_epoch = Some(Epoch::new(0));
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
let tester = Self::new_from_config(config)
.await
.test_post_validator_register_validator()
.await;
tester
.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_BUILDER_PAYLOAD_VALUE_WEI,
)));
tester
}
fn skip_slots(self, count: u64) -> Self {
for _ in 0..count {
self.chain
@@ -1372,9 +1412,9 @@ impl ApiTester {
pub async fn test_get_config_spec(self) -> Self {
let result = self
.client
.get_config_spec::<ConfigAndPresetBellatrix>()
.get_config_spec::<ConfigAndPresetCapella>()
.await
.map(|res| ConfigAndPreset::Bellatrix(res.data))
.map(|res| ConfigAndPreset::Capella(res.data))
.unwrap();
let expected = ConfigAndPreset::from_chain_spec::<E>(&self.chain.spec, None);
@@ -2122,7 +2162,7 @@ impl ApiTester {
self
}
pub async fn test_blinded_block_production<Payload: ExecPayload<E>>(&self) {
pub async fn test_blinded_block_production<Payload: AbstractExecPayload<E>>(&self) {
let fork = self.chain.canonical_head.cached_head().head_fork();
let genesis_validators_root = self.chain.genesis_validators_root;
@@ -2182,7 +2222,7 @@ impl ApiTester {
}
}
pub async fn test_blinded_block_production_no_verify_randao<Payload: ExecPayload<E>>(
pub async fn test_blinded_block_production_no_verify_randao<Payload: AbstractExecPayload<E>>(
self,
) -> Self {
for _ in 0..E::slots_per_epoch() {
@@ -2206,7 +2246,9 @@ impl ApiTester {
self
}
pub async fn test_blinded_block_production_verify_randao_invalid<Payload: ExecPayload<E>>(
pub async fn test_blinded_block_production_verify_randao_invalid<
Payload: AbstractExecPayload<E>,
>(
self,
) -> Self {
let fork = self.chain.canonical_head.cached_head().head_fork();
@@ -2664,7 +2706,7 @@ impl ApiTester {
let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -2673,14 +2715,11 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64);
assert_eq!(
payload.execution_payload_header.fee_recipient,
expected_fee_recipient
);
assert_eq!(payload.execution_payload_header.gas_limit, 11_111_111);
assert_eq!(payload.fee_recipient(), expected_fee_recipient);
assert_eq!(payload.gas_limit(), 11_111_111);
// If this cache is empty, it indicates fallback was not used, so the payload came from the
// mock builder.
@@ -2707,7 +2746,7 @@ impl ApiTester {
let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -2716,14 +2755,11 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64);
assert_eq!(
payload.execution_payload_header.fee_recipient,
expected_fee_recipient
);
assert_eq!(payload.execution_payload_header.gas_limit, 30_000_000);
assert_eq!(payload.fee_recipient(), expected_fee_recipient);
assert_eq!(payload.gas_limit(), 30_000_000);
// This cache should not be populated because fallback should not have been used.
assert!(self
@@ -2753,7 +2789,7 @@ impl ApiTester {
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -2762,12 +2798,9 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
assert_eq!(
payload.execution_payload_header.fee_recipient,
test_fee_recipient
);
assert_eq!(payload.fee_recipient(), test_fee_recipient);
// This cache should not be populated because fallback should not have been used.
assert!(self
@@ -2801,11 +2834,11 @@ impl ApiTester {
.beacon_state
.latest_execution_payload_header()
.unwrap()
.block_hash;
.block_hash();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -2814,12 +2847,9 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
assert_eq!(
payload.execution_payload_header.parent_hash,
expected_parent_hash
);
assert_eq!(payload.parent_hash(), expected_parent_hash);
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -2856,7 +2886,7 @@ impl ApiTester {
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -2865,12 +2895,9 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
assert_eq!(
payload.execution_payload_header.prev_randao,
expected_prev_randao
);
assert_eq!(payload.prev_randao(), expected_prev_randao);
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -2901,12 +2928,12 @@ impl ApiTester {
.beacon_state
.latest_execution_payload_header()
.unwrap()
.block_number
.block_number()
+ 1;
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -2915,12 +2942,9 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
assert_eq!(
payload.execution_payload_header.block_number,
expected_block_number
);
assert_eq!(payload.block_number(), expected_block_number);
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -2951,11 +2975,11 @@ impl ApiTester {
.beacon_state
.latest_execution_payload_header()
.unwrap()
.timestamp;
.timestamp();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -2964,9 +2988,9 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
assert!(payload.execution_payload_header.timestamp > min_expected_timestamp);
assert!(payload.timestamp() > min_expected_timestamp);
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -2991,7 +3015,7 @@ impl ApiTester {
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -3000,7 +3024,7 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -3028,7 +3052,7 @@ impl ApiTester {
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -3037,7 +3061,7 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -3071,7 +3095,7 @@ impl ApiTester {
.get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch()))
.await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(next_slot, &randao_reveal, None)
.await
@@ -3080,7 +3104,7 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
// This cache should not be populated because fallback should not have been used.
assert!(self
@@ -3100,7 +3124,7 @@ impl ApiTester {
.get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch()))
.await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(next_slot, &randao_reveal, None)
.await
@@ -3109,7 +3133,7 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -3149,7 +3173,7 @@ impl ApiTester {
.get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch()))
.await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(next_slot, &randao_reveal, None)
.await
@@ -3158,7 +3182,7 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -3188,7 +3212,7 @@ impl ApiTester {
.get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch()))
.await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(next_slot, &randao_reveal, None)
.await
@@ -3197,7 +3221,7 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
// This cache should not be populated because fallback should not have been used.
assert!(self
@@ -3231,7 +3255,7 @@ impl ApiTester {
let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -3240,13 +3264,10 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64);
assert_eq!(
payload.execution_payload_header.fee_recipient,
expected_fee_recipient
);
assert_eq!(payload.fee_recipient(), expected_fee_recipient);
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -3275,7 +3296,7 @@ impl ApiTester {
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload = self
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
@@ -3284,7 +3305,7 @@ impl ApiTester {
.body()
.execution_payload()
.unwrap()
.clone();
.into();
// If this cache is populated, it indicates fallback to the local EE was correctly used.
assert!(self
@@ -3297,6 +3318,209 @@ impl ApiTester {
self
}
pub async fn test_builder_payload_chosen_when_more_profitable(self) -> Self {
// Mutate value.
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1,
)));
let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
.unwrap()
.data
.body()
.execution_payload()
.unwrap()
.into();
// The builder's payload should've been chosen, so this cache should not be populated
assert!(self
.chain
.execution_layer
.as_ref()
.unwrap()
.get_payload_by_root(&payload.tree_hash_root())
.is_none());
self
}
pub async fn test_local_payload_chosen_when_equally_profitable(self) -> Self {
// Mutate value.
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI,
)));
let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
.unwrap()
.data
.body()
.execution_payload()
.unwrap()
.into();
// The local payload should've been chosen, so this cache should be populated
assert!(self
.chain
.execution_layer
.as_ref()
.unwrap()
.get_payload_by_root(&payload.tree_hash_root())
.is_some());
self
}
pub async fn test_local_payload_chosen_when_more_profitable(self) -> Self {
// Mutate value.
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI - 1,
)));
let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
.unwrap()
.data
.body()
.execution_payload()
.unwrap()
.into();
// The local payload should've been chosen, so this cache should be populated
assert!(self
.chain
.execution_layer
.as_ref()
.unwrap()
.get_payload_by_root(&payload.tree_hash_root())
.is_some());
self
}
pub async fn test_builder_works_post_capella(self) -> Self {
// Ensure builder payload is chosen
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1,
)));
let slot = self.chain.slot().unwrap();
let propose_state = self
.harness
.chain
.state_at_slot(slot, StateSkipConfig::WithoutStateRoots)
.unwrap();
let withdrawals = get_expected_withdrawals(&propose_state, &self.chain.spec).unwrap();
let withdrawals_root = withdrawals.tree_hash_root();
// Set withdrawals root for builder
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::WithdrawalsRoot(withdrawals_root));
let epoch = self.chain.epoch().unwrap();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
.unwrap()
.data
.body()
.execution_payload()
.unwrap()
.into();
// The builder's payload should've been chosen, so this cache should not be populated
assert!(self
.chain
.execution_layer
.as_ref()
.unwrap()
.get_payload_by_root(&payload.tree_hash_root())
.is_none());
self
}
pub async fn test_lighthouse_rejects_invalid_withdrawals_root(self) -> Self {
// Ensure builder payload *would be* chosen
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1,
)));
// Set withdrawals root to something invalid
self.mock_builder
.as_ref()
.unwrap()
.builder
.add_operation(Operation::WithdrawalsRoot(Hash256::repeat_byte(0x42)));
let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap();
let (_, randao_reveal) = self.get_test_randao(slot, epoch).await;
let payload: BlindedPayload<E> = self
.client
.get_validator_blinded_blocks::<E, BlindedPayload<E>>(slot, &randao_reveal, None)
.await
.unwrap()
.data
.body()
.execution_payload()
.unwrap()
.into();
// The local payload should've been chosen because the builder's was invalid
assert!(self
.chain
.execution_layer
.as_ref()
.unwrap()
.get_payload_by_root(&payload.tree_hash_root())
.is_some());
self
}
#[cfg(target_os = "linux")]
pub async fn test_get_lighthouse_health(self) -> Self {
self.client.get_lighthouse_health().await.unwrap();
@@ -3766,9 +3990,9 @@ async fn get_events() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_events_altair() {
let mut spec = E::default_spec();
spec.altair_fork_epoch = Some(Epoch::new(0));
ApiTester::new_from_spec(spec)
let mut config = ApiTesterConfig::default();
config.spec.altair_fork_epoch = Some(Epoch::new(0));
ApiTester::new_from_config(config)
.await
.test_get_events_altair()
.await;
@@ -4281,6 +4505,38 @@ async fn builder_inadequate_builder_threshold() {
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn builder_payload_chosen_by_profit() {
ApiTester::new_mev_tester_no_builder_threshold()
.await
.test_builder_payload_chosen_when_more_profitable()
.await
.test_local_payload_chosen_when_equally_profitable()
.await
.test_local_payload_chosen_when_more_profitable()
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn builder_works_post_capella() {
let mut config = ApiTesterConfig {
builder_threshold: Some(0),
spec: E::default_spec(),
};
config.spec.altair_fork_epoch = Some(Epoch::new(0));
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
config.spec.capella_fork_epoch = Some(Epoch::new(0));
ApiTester::new_from_config(config)
.await
.test_post_validator_register_validator()
.await
.test_builder_works_post_capella()
.await
.test_lighthouse_rejects_invalid_withdrawals_root()
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lighthouse_endpoints() {
ApiTester::new()