Prevent port re-use in HTTP API tests (#4745)

## Issue Addressed

CI is plagued by `AddrAlreadyInUse` failures, which are caused by race conditions in allocating free ports.

This PR removes all usages of the `unused_port` crate for Lighthouse's HTTP API, in favour of passing `:0` as the listen address. As a result, the listen address isn't known ahead of time and must be read from the listening socket after it binds. This requires tying some self-referential knots, which is a little disruptive, but hopefully doesn't clash too much with Deneb 🤞

There are still a few usages of `unused_tcp4_port` left in cases where we start external processes, like the `watch` Postgres DB, Anvil, Geth, Nethermind, etc. Removing these usages is non-trivial because it's hard to read the port back from an external process after starting it with `--port 0`. We might be able to do something on Linux where we read from `/proc/`, but I'll leave that for future work.
This commit is contained in:
Michael Sproul 2023-09-20 01:19:03 +00:00
parent d386a07b0c
commit 4b6cb3db2c
12 changed files with 172 additions and 184 deletions

5
Cargo.lock generated
View File

@ -601,7 +601,6 @@ dependencies = [
"tree_hash", "tree_hash",
"tree_hash_derive", "tree_hash_derive",
"types", "types",
"unused_port",
] ]
[[package]] [[package]]
@ -2583,7 +2582,9 @@ dependencies = [
name = "execution_layer" name = "execution_layer"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"arc-swap",
"async-trait", "async-trait",
"axum",
"builder_client", "builder_client",
"bytes", "bytes",
"environment", "environment",
@ -2598,6 +2599,7 @@ dependencies = [
"hash-db", "hash-db",
"hash256-std-hasher", "hash256-std-hasher",
"hex", "hex",
"hyper",
"jsonwebtoken", "jsonwebtoken",
"keccak-hash", "keccak-hash",
"lazy_static", "lazy_static",
@ -3381,7 +3383,6 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tree_hash", "tree_hash",
"types", "types",
"unused_port",
"warp", "warp",
"warp_utils", "warp_utils",
] ]

View File

@ -65,7 +65,6 @@ sensitive_url = { path = "../../common/sensitive_url" }
superstruct = "0.5.0" superstruct = "0.5.0"
hex = "0.4.2" hex = "0.4.2"
exit-future = "0.2.0" exit-future = "0.2.0"
unused_port = {path = "../../common/unused_port"}
oneshot_broadcast = { path = "../../common/oneshot_broadcast" } oneshot_broadcast = { path = "../../common/oneshot_broadcast" }
[[test]] [[test]]

View File

@ -17,8 +17,8 @@ use bls::get_withdrawal_credentials;
use execution_layer::{ use execution_layer::{
auth::JwtKey, auth::JwtKey,
test_utils::{ test_utils::{
ExecutionBlockGenerator, MockExecutionLayer, TestingBuilder, DEFAULT_JWT_SECRET, ExecutionBlockGenerator, MockBuilder, MockBuilderServer, MockExecutionLayer,
DEFAULT_TERMINAL_BLOCK, DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK,
}, },
ExecutionLayer, ExecutionLayer,
}; };
@ -167,7 +167,6 @@ pub struct Builder<T: BeaconChainTypes> {
store_mutator: Option<BoxedMutator<T::EthSpec, T::HotStore, T::ColdStore>>, store_mutator: Option<BoxedMutator<T::EthSpec, T::HotStore, T::ColdStore>>,
execution_layer: Option<ExecutionLayer<T::EthSpec>>, execution_layer: Option<ExecutionLayer<T::EthSpec>>,
mock_execution_layer: Option<MockExecutionLayer<T::EthSpec>>, mock_execution_layer: Option<MockExecutionLayer<T::EthSpec>>,
mock_builder: Option<TestingBuilder<T::EthSpec>>,
testing_slot_clock: Option<TestingSlotClock>, testing_slot_clock: Option<TestingSlotClock>,
runtime: TestRuntime, runtime: TestRuntime,
log: Logger, log: Logger,
@ -301,7 +300,6 @@ where
store_mutator: None, store_mutator: None,
execution_layer: None, execution_layer: None,
mock_execution_layer: None, mock_execution_layer: None,
mock_builder: None,
testing_slot_clock: None, testing_slot_clock: None,
runtime, runtime,
log, log,
@ -433,7 +431,11 @@ where
self self
} }
pub fn mock_execution_layer(mut self) -> Self { pub fn mock_execution_layer(self) -> Self {
self.mock_execution_layer_with_config(None)
}
pub fn mock_execution_layer_with_config(mut self, builder_threshold: Option<u128>) -> Self {
let spec = self.spec.clone().expect("cannot build without spec"); let spec = self.spec.clone().expect("cannot build without spec");
let shanghai_time = spec.capella_fork_epoch.map(|epoch| { let shanghai_time = spec.capella_fork_epoch.map(|epoch| {
HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64()
@ -442,55 +444,15 @@ where
self.runtime.task_executor.clone(), self.runtime.task_executor.clone(),
DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_BLOCK,
shanghai_time, shanghai_time,
None, builder_threshold,
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
spec, spec,
None,
); );
self.execution_layer = Some(mock.el.clone()); self.execution_layer = Some(mock.el.clone());
self.mock_execution_layer = Some(mock); self.mock_execution_layer = Some(mock);
self self
} }
pub fn mock_execution_layer_with_builder(
mut self,
beacon_url: SensitiveUrl,
builder_threshold: Option<u128>,
) -> Self {
// Get a random unused port
let port = unused_port::unused_tcp4_port().unwrap();
let builder_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
let spec = self.spec.clone().expect("cannot build without spec");
let shanghai_time = spec.capella_fork_epoch.map(|epoch| {
HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64()
});
let mock_el = MockExecutionLayer::new(
self.runtime.task_executor.clone(),
DEFAULT_TERMINAL_BLOCK,
shanghai_time,
builder_threshold,
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
spec.clone(),
Some(builder_url.clone()),
)
.move_to_terminal_block();
let mock_el_url = SensitiveUrl::parse(mock_el.server.url().as_str()).unwrap();
self.mock_builder = Some(TestingBuilder::new(
mock_el_url,
builder_url,
beacon_url,
spec,
self.runtime.task_executor.clone(),
));
self.execution_layer = Some(mock_el.el.clone());
self.mock_execution_layer = Some(mock_el);
self
}
/// Instruct the mock execution engine to always return a "valid" response to any payload it is /// Instruct the mock execution engine to always return a "valid" response to any payload it is
/// asked to execute. /// asked to execute.
pub fn mock_execution_layer_all_payloads_valid(self) -> Self { pub fn mock_execution_layer_all_payloads_valid(self) -> Self {
@ -572,7 +534,7 @@ where
shutdown_receiver: Arc::new(Mutex::new(shutdown_receiver)), shutdown_receiver: Arc::new(Mutex::new(shutdown_receiver)),
runtime: self.runtime, runtime: self.runtime,
mock_execution_layer: self.mock_execution_layer, mock_execution_layer: self.mock_execution_layer,
mock_builder: self.mock_builder.map(Arc::new), mock_builder: None,
rng: make_rng(), rng: make_rng(),
} }
} }
@ -597,7 +559,7 @@ pub struct BeaconChainHarness<T: BeaconChainTypes> {
pub runtime: TestRuntime, pub runtime: TestRuntime,
pub mock_execution_layer: Option<MockExecutionLayer<T::EthSpec>>, pub mock_execution_layer: Option<MockExecutionLayer<T::EthSpec>>,
pub mock_builder: Option<Arc<TestingBuilder<T::EthSpec>>>, pub mock_builder: Option<Arc<MockBuilder<T::EthSpec>>>,
pub rng: Mutex<StdRng>, pub rng: Mutex<StdRng>,
} }
@ -633,6 +595,49 @@ where
.execution_block_generator() .execution_block_generator()
} }
pub fn set_mock_builder(&mut self, beacon_url: SensitiveUrl) -> MockBuilderServer {
let mock_el = self
.mock_execution_layer
.as_ref()
.expect("harness was not built with mock execution layer");
let mock_el_url = SensitiveUrl::parse(mock_el.server.url().as_str()).unwrap();
// Create the builder, listening on a free port.
let (mock_builder, mock_builder_server) = MockBuilder::new_for_testing(
mock_el_url,
beacon_url,
self.spec.clone(),
self.runtime.task_executor.clone(),
);
// Set the builder URL in the execution layer now that its port is known.
let builder_listen_addr = mock_builder_server.local_addr();
let port = builder_listen_addr.port();
mock_el
.el
.set_builder_url(
SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(),
None,
)
.unwrap();
self.mock_builder = Some(Arc::new(mock_builder));
// Sanity check.
let el_builder = self
.chain
.execution_layer
.as_ref()
.unwrap()
.builder()
.unwrap();
let mock_el_builder = mock_el.el.builder().unwrap();
assert!(Arc::ptr_eq(&el_builder, &mock_el_builder));
mock_builder_server
}
pub fn get_all_validators(&self) -> Vec<usize> { pub fn get_all_validators(&self) -> Vec<usize> {
(0..self.validator_keypairs.len()).collect() (0..self.validator_keypairs.len()).collect()
} }

View File

@ -42,6 +42,8 @@ ethers-core = "1.0.2"
builder_client = { path = "../builder_client" } builder_client = { path = "../builder_client" }
fork_choice = { path = "../../consensus/fork_choice" } fork_choice = { path = "../../consensus/fork_choice" }
mev-rs = { git = "https://github.com/ralexstokes/mev-rs", rev = "216657016d5c0889b505857c89ae42c7aa2764af" } mev-rs = { git = "https://github.com/ralexstokes/mev-rs", rev = "216657016d5c0889b505857c89ae42c7aa2764af" }
axum = "0.6"
hyper = "0.14"
ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "e380108" } ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "e380108" }
ssz_rs = "0.9.0" ssz_rs = "0.9.0"
tokio-stream = { version = "0.1.9", features = [ "sync" ] } tokio-stream = { version = "0.1.9", features = [ "sync" ] }
@ -51,3 +53,4 @@ hash256-std-hasher = "0.15.2"
triehash = "0.8.4" triehash = "0.8.4"
hash-db = "0.15.2" hash-db = "0.15.2"
pretty_reqwest_error = { path = "../../common/pretty_reqwest_error" } pretty_reqwest_error = { path = "../../common/pretty_reqwest_error" }
arc-swap = "1.6.0"

View File

@ -5,6 +5,7 @@
//! deposit-contract functionality that the `beacon_node/eth1` crate already provides. //! deposit-contract functionality that the `beacon_node/eth1` crate already provides.
use crate::payload_cache::PayloadCache; use crate::payload_cache::PayloadCache;
use arc_swap::ArcSwapOption;
use auth::{strip_prefix, Auth, JwtKey}; use auth::{strip_prefix, Auth, JwtKey};
use builder_client::BuilderHttpClient; use builder_client::BuilderHttpClient;
pub use engine_api::EngineCapabilities; pub use engine_api::EngineCapabilities;
@ -209,7 +210,7 @@ pub enum FailedCondition {
struct Inner<E: EthSpec> { struct Inner<E: EthSpec> {
engine: Arc<Engine>, engine: Arc<Engine>,
builder: Option<BuilderHttpClient>, builder: ArcSwapOption<BuilderHttpClient>,
execution_engine_forkchoice_lock: Mutex<()>, execution_engine_forkchoice_lock: Mutex<()>,
suggested_fee_recipient: Option<Address>, suggested_fee_recipient: Option<Address>,
proposer_preparation_data: Mutex<HashMap<u64, ProposerPreparationDataEntry>>, proposer_preparation_data: Mutex<HashMap<u64, ProposerPreparationDataEntry>>,
@ -324,25 +325,9 @@ impl<T: EthSpec> ExecutionLayer<T> {
Engine::new(api, executor.clone(), &log) Engine::new(api, executor.clone(), &log)
}; };
let builder = builder_url
.map(|url| {
let builder_client = BuilderHttpClient::new(url.clone(), builder_user_agent)
.map_err(Error::Builder)?;
info!(
log,
"Using external block builder";
"builder_url" => ?url,
"builder_profit_threshold" => builder_profit_threshold,
"local_user_agent" => builder_client.get_user_agent(),
);
Ok::<_, Error>(builder_client)
})
.transpose()?;
let inner = Inner { let inner = Inner {
engine: Arc::new(engine), engine: Arc::new(engine),
builder, builder: ArcSwapOption::empty(),
execution_engine_forkchoice_lock: <_>::default(), execution_engine_forkchoice_lock: <_>::default(),
suggested_fee_recipient, suggested_fee_recipient,
proposer_preparation_data: Mutex::new(HashMap::new()), proposer_preparation_data: Mutex::new(HashMap::new()),
@ -356,19 +341,45 @@ impl<T: EthSpec> ExecutionLayer<T> {
last_new_payload_errored: RwLock::new(false), last_new_payload_errored: RwLock::new(false),
}; };
Ok(Self { let el = Self {
inner: Arc::new(inner), inner: Arc::new(inner),
}) };
}
if let Some(builder_url) = builder_url {
el.set_builder_url(builder_url, builder_user_agent)?;
}
Ok(el)
} }
impl<T: EthSpec> ExecutionLayer<T> {
fn engine(&self) -> &Arc<Engine> { fn engine(&self) -> &Arc<Engine> {
&self.inner.engine &self.inner.engine
} }
pub fn builder(&self) -> &Option<BuilderHttpClient> { pub fn builder(&self) -> Option<Arc<BuilderHttpClient>> {
&self.inner.builder self.inner.builder.load_full()
}
/// Set the builder URL after initialization.
///
/// This is useful for breaking circular dependencies between mock ELs and mock builders in
/// tests.
pub fn set_builder_url(
&self,
builder_url: SensitiveUrl,
builder_user_agent: Option<String>,
) -> Result<(), Error> {
let builder_client = BuilderHttpClient::new(builder_url.clone(), builder_user_agent)
.map_err(Error::Builder)?;
info!(
self.log(),
"Using external block builder";
"builder_url" => ?builder_url,
"builder_profit_threshold" => self.inner.builder_profit_threshold.as_u128(),
"local_user_agent" => builder_client.get_user_agent(),
);
self.inner.builder.swap(Some(Arc::new(builder_client)));
Ok(())
} }
/// Cache a full payload, keyed on the `tree_hash_root` of the payload /// Cache a full payload, keyed on the `tree_hash_root` of the payload

View File

@ -40,6 +40,11 @@ use types::{
Uint256, Uint256,
}; };
pub type MockBuilderServer = axum::Server<
hyper::server::conn::AddrIncoming,
axum::routing::IntoMakeService<axum::routing::Router>,
>;
#[derive(Clone)] #[derive(Clone)]
pub enum Operation { pub enum Operation {
FeeRecipient(Address), FeeRecipient(Address),
@ -170,19 +175,25 @@ impl BidStuff for BuilderBid {
} }
} }
pub struct TestingBuilder<E: EthSpec> { #[derive(Clone)]
server: BlindedBlockProviderServer<MockBuilder<E>>, pub struct MockBuilder<E: EthSpec> {
pub builder: MockBuilder<E>, el: ExecutionLayer<E>,
beacon_client: BeaconNodeHttpClient,
spec: ChainSpec,
context: Arc<Context>,
val_registration_cache: Arc<RwLock<HashMap<BlsPublicKey, SignedValidatorRegistration>>>,
builder_sk: SecretKey,
operations: Arc<RwLock<Vec<Operation>>>,
invalidate_signatures: Arc<RwLock<bool>>,
} }
impl<E: EthSpec> TestingBuilder<E> { impl<E: EthSpec> MockBuilder<E> {
pub fn new( pub fn new_for_testing(
mock_el_url: SensitiveUrl, mock_el_url: SensitiveUrl,
builder_url: SensitiveUrl,
beacon_url: SensitiveUrl, beacon_url: SensitiveUrl,
spec: ChainSpec, spec: ChainSpec,
executor: TaskExecutor, executor: TaskExecutor,
) -> Self { ) -> (Self, MockBuilderServer) {
let file = NamedTempFile::new().unwrap(); let file = NamedTempFile::new().unwrap();
let path = file.path().into(); let path = file.path().into();
std::fs::write(&path, hex::encode(DEFAULT_JWT_SECRET)).unwrap(); std::fs::write(&path, hex::encode(DEFAULT_JWT_SECRET)).unwrap();
@ -211,39 +222,13 @@ impl<E: EthSpec> TestingBuilder<E> {
spec, spec,
context, context,
); );
let port = builder_url.full.port().unwrap(); let host: Ipv4Addr = Ipv4Addr::LOCALHOST;
let host: Ipv4Addr = builder_url let port = 0;
.full let provider = BlindedBlockProviderServer::new(host, port, builder.clone());
.host_str() let server = provider.serve();
.unwrap() (builder, server)
.to_string()
.parse()
.unwrap();
let server = BlindedBlockProviderServer::new(host, port, builder.clone());
Self { server, builder }
} }
pub async fn run(&self) {
let server = self.server.serve();
if let Err(err) = server.await {
println!("error while listening for incoming: {err}")
}
}
}
#[derive(Clone)]
pub struct MockBuilder<E: EthSpec> {
el: ExecutionLayer<E>,
beacon_client: BeaconNodeHttpClient,
spec: ChainSpec,
context: Arc<Context>,
val_registration_cache: Arc<RwLock<HashMap<BlsPublicKey, SignedValidatorRegistration>>>,
builder_sk: SecretKey,
operations: Arc<RwLock<Vec<Operation>>>,
invalidate_signatures: Arc<RwLock<bool>>,
}
impl<E: EthSpec> MockBuilder<E> {
pub fn new( pub fn new(
el: ExecutionLayer<E>, el: ExecutionLayer<E>,
beacon_client: BeaconNodeHttpClient, beacon_client: BeaconNodeHttpClient,

View File

@ -31,7 +31,6 @@ impl<T: EthSpec> MockExecutionLayer<T> {
None, None,
Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()),
spec, spec,
None,
) )
} }
@ -43,7 +42,6 @@ impl<T: EthSpec> MockExecutionLayer<T> {
builder_threshold: Option<u128>, builder_threshold: Option<u128>,
jwt_key: Option<JwtKey>, jwt_key: Option<JwtKey>,
spec: ChainSpec, spec: ChainSpec,
builder_url: Option<SensitiveUrl>,
) -> Self { ) -> Self {
let handle = executor.handle().unwrap(); let handle = executor.handle().unwrap();
@ -65,7 +63,6 @@ impl<T: EthSpec> MockExecutionLayer<T> {
let config = Config { let config = Config {
execution_endpoints: vec![url], execution_endpoints: vec![url],
builder_url,
secret_files: vec![path], secret_files: vec![path],
suggested_fee_recipient: Some(Address::repeat_byte(42)), suggested_fee_recipient: Some(Address::repeat_byte(42)),
builder_profit_threshold: builder_threshold.unwrap_or(DEFAULT_BUILDER_THRESHOLD_WEI), builder_profit_threshold: builder_threshold.unwrap_or(DEFAULT_BUILDER_THRESHOLD_WEI),

View File

@ -25,7 +25,7 @@ use warp::{http::StatusCode, Filter, Rejection};
use crate::EngineCapabilities; use crate::EngineCapabilities;
pub use execution_block_generator::{generate_pow_block, Block, ExecutionBlockGenerator}; pub use execution_block_generator::{generate_pow_block, Block, ExecutionBlockGenerator};
pub use hook::Hook; pub use hook::Hook;
pub use mock_builder::{Context as MockBuilderContext, MockBuilder, Operation, TestingBuilder}; pub use mock_builder::{Context as MockBuilderContext, MockBuilder, MockBuilderServer, Operation};
pub use mock_execution_layer::MockExecutionLayer; pub use mock_execution_layer::MockExecutionLayer;
pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400; pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400;

View File

@ -40,7 +40,6 @@ logging = { path = "../../common/logging" }
ethereum_serde_utils = "0.5.0" ethereum_serde_utils = "0.5.0"
operation_pool = { path = "../operation_pool" } operation_pool = { path = "../operation_pool" }
sensitive_url = { path = "../../common/sensitive_url" } sensitive_url = { path = "../../common/sensitive_url" }
unused_port = { path = "../../common/unused_port" }
store = { path = "../store" } store = { path = "../store" }
bytes = "1.1.0" bytes = "1.1.0"
beacon_processor = { path = "../beacon_processor" } beacon_processor = { path = "../beacon_processor" }

View File

@ -3635,12 +3635,13 @@ pub fn serve<T: BeaconChainTypes>(
// send the response back to our original HTTP request // send the response back to our original HTTP request
// task via a channel. // task via a channel.
let builder_future = async move { let builder_future = async move {
let builder = chain let arc_builder = chain
.execution_layer .execution_layer
.as_ref() .as_ref()
.ok_or(BeaconChainError::ExecutionLayerMissing) .ok_or(BeaconChainError::ExecutionLayerMissing)
.map_err(warp_utils::reject::beacon_chain_error)? .map_err(warp_utils::reject::beacon_chain_error)?
.builder() .builder();
let builder = arc_builder
.as_ref() .as_ref()
.ok_or(BeaconChainError::BuilderMissing) .ok_or(BeaconChainError::BuilderMissing)
.map_err(warp_utils::reject::beacon_chain_error)?; .map_err(warp_utils::reject::beacon_chain_error)?;

View File

@ -129,17 +129,9 @@ pub async fn create_api_server<T: BeaconChainTypes>(
test_runtime: &TestRuntime, test_runtime: &TestRuntime,
log: Logger, log: Logger,
) -> ApiServer<T::EthSpec, impl Future<Output = ()>> { ) -> ApiServer<T::EthSpec, impl Future<Output = ()>> {
// Get a random unused port. // Use port 0 to allocate a new unused port.
let port = unused_port::unused_tcp4_port().unwrap(); let port = 0;
create_api_server_on_port(chain, test_runtime, log, port).await
}
pub async fn create_api_server_on_port<T: BeaconChainTypes>(
chain: Arc<BeaconChain<T>>,
test_runtime: &TestRuntime,
log: Logger,
port: u16,
) -> ApiServer<T::EthSpec, impl Future<Output = ()>> {
let (network_senders, network_receivers) = NetworkSenders::new(); let (network_senders, network_receivers) = NetworkSenders::new();
// Default metadata // Default metadata

View File

@ -10,15 +10,14 @@ use eth2::{
types::{BlockId as CoreBlockId, ForkChoiceNode, StateId as CoreStateId, *}, types::{BlockId as CoreBlockId, ForkChoiceNode, StateId as CoreStateId, *},
BeaconNodeHttpClient, Error, StatusCode, Timeouts, BeaconNodeHttpClient, Error, StatusCode, Timeouts,
}; };
use execution_layer::test_utils::TestingBuilder;
use execution_layer::test_utils::DEFAULT_BUILDER_THRESHOLD_WEI;
use execution_layer::test_utils::{ use execution_layer::test_utils::{
Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, MockBuilder, Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_BUILDER_THRESHOLD_WEI,
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI,
}; };
use futures::stream::{Stream, StreamExt}; use futures::stream::{Stream, StreamExt};
use futures::FutureExt; use futures::FutureExt;
use http_api::{ use http_api::{
test_utils::{create_api_server, create_api_server_on_port, ApiServer}, test_utils::{create_api_server, ApiServer},
BlockId, StateId, BlockId, StateId,
}; };
use lighthouse_network::{Enr, EnrExt, PeerId}; use lighthouse_network::{Enr, EnrExt, PeerId};
@ -73,7 +72,7 @@ struct ApiTester {
network_rx: NetworkReceivers<E>, network_rx: NetworkReceivers<E>,
local_enr: Enr, local_enr: Enr,
external_peer_id: PeerId, external_peer_id: PeerId,
mock_builder: Option<Arc<TestingBuilder<E>>>, mock_builder: Option<Arc<MockBuilder<E>>>,
} }
struct ApiTesterConfig { struct ApiTesterConfig {
@ -120,13 +119,9 @@ impl ApiTester {
} }
pub async fn new_from_config(config: ApiTesterConfig) -> Self { pub async fn new_from_config(config: ApiTesterConfig) -> Self {
// Get a random unused port
let spec = config.spec; let spec = config.spec;
let port = unused_port::unused_tcp4_port().unwrap();
let beacon_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap();
let harness = Arc::new( let mut harness = BeaconChainHarness::builder(MainnetEthSpec)
BeaconChainHarness::builder(MainnetEthSpec)
.spec(spec.clone()) .spec(spec.clone())
.chain_config(ChainConfig { .chain_config(ChainConfig {
reconstruct_historic_states: config.retain_historic_states, reconstruct_historic_states: config.retain_historic_states,
@ -135,9 +130,17 @@ impl ApiTester {
.logger(logging::test_logger()) .logger(logging::test_logger())
.deterministic_keypairs(VALIDATOR_COUNT) .deterministic_keypairs(VALIDATOR_COUNT)
.fresh_ephemeral_store() .fresh_ephemeral_store()
.mock_execution_layer_with_builder(beacon_url.clone(), config.builder_threshold) .mock_execution_layer_with_config(config.builder_threshold)
.build(), .build();
);
harness
.mock_execution_layer
.as_ref()
.unwrap()
.server
.execution_block_generator()
.move_to_terminal_block()
.unwrap();
harness.advance_slot(); harness.advance_slot();
@ -245,29 +248,40 @@ impl ApiTester {
let ApiServer { let ApiServer {
server, server,
listening_socket: _, listening_socket,
network_rx, network_rx,
local_enr, local_enr,
external_peer_id, external_peer_id,
} = create_api_server_on_port(chain.clone(), &harness.runtime, log, port).await; } = create_api_server(chain.clone(), &harness.runtime, log).await;
harness.runtime.task_executor.spawn(server, "api_server"); harness.runtime.task_executor.spawn(server, "api_server");
// Late-initalize the mock builder now that the mock execution node and beacon API ports
// have been allocated.
let beacon_api_port = listening_socket.port();
let beacon_url =
SensitiveUrl::parse(format!("http://127.0.0.1:{beacon_api_port}").as_str()).unwrap();
let mock_builder_server = harness.set_mock_builder(beacon_url.clone());
// Start the mock builder service prior to building the chain out.
harness.runtime.task_executor.spawn(
async move {
if let Err(e) = mock_builder_server.await {
panic!("error in mock builder server: {e:?}");
}
},
"mock_builder_server",
);
let mock_builder = harness.mock_builder.clone();
let client = BeaconNodeHttpClient::new( let client = BeaconNodeHttpClient::new(
beacon_url, beacon_url,
Timeouts::set_all(Duration::from_secs(SECONDS_PER_SLOT)), Timeouts::set_all(Duration::from_secs(SECONDS_PER_SLOT)),
); );
let builder_ref = harness.mock_builder.as_ref().unwrap().clone();
harness.runtime.task_executor.spawn(
async move { builder_ref.run().await },
"mock_builder_server",
);
let mock_builder = harness.mock_builder.clone();
Self { Self {
harness, harness: Arc::new(harness),
chain, chain,
client, client,
next_block, next_block,
@ -379,7 +393,6 @@ impl ApiTester {
.mock_builder .mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::Value(Uint256::from( .add_operation(Operation::Value(Uint256::from(
DEFAULT_BUILDER_THRESHOLD_WEI, DEFAULT_BUILDER_THRESHOLD_WEI,
))); )));
@ -402,7 +415,6 @@ impl ApiTester {
.mock_builder .mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::Value(Uint256::from( .add_operation(Operation::Value(Uint256::from(
DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI,
))); )));
@ -3275,6 +3287,7 @@ impl ApiTester {
.unwrap() .unwrap()
.get_payload_by_root(&payload.tree_hash_root()) .get_payload_by_root(&payload.tree_hash_root())
.is_none()); .is_none());
self self
} }
@ -3283,7 +3296,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::GasLimit(30_000_000)); .add_operation(Operation::GasLimit(30_000_000));
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();
@ -3326,7 +3338,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::FeeRecipient(test_fee_recipient)); .add_operation(Operation::FeeRecipient(test_fee_recipient));
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();
@ -3368,7 +3379,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::ParentHash(invalid_parent_hash)); .add_operation(Operation::ParentHash(invalid_parent_hash));
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();
@ -3417,7 +3427,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::PrevRandao(invalid_prev_randao)); .add_operation(Operation::PrevRandao(invalid_prev_randao));
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();
@ -3462,7 +3471,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::BlockNumber(invalid_block_number)); .add_operation(Operation::BlockNumber(invalid_block_number));
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();
@ -3509,7 +3517,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::Timestamp(invalid_timestamp)); .add_operation(Operation::Timestamp(invalid_timestamp));
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();
@ -3549,11 +3556,7 @@ impl ApiTester {
} }
pub async fn test_payload_rejects_invalid_signature(self) -> Self { pub async fn test_payload_rejects_invalid_signature(self) -> Self {
self.mock_builder self.mock_builder.as_ref().unwrap().invalid_signatures();
.as_ref()
.unwrap()
.builder
.invalid_signatures();
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();
let epoch = self.chain.epoch().unwrap(); let epoch = self.chain.epoch().unwrap();
@ -3831,7 +3834,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::Value(Uint256::from( .add_operation(Operation::Value(Uint256::from(
DEFAULT_BUILDER_THRESHOLD_WEI - 1, DEFAULT_BUILDER_THRESHOLD_WEI - 1,
))); )));
@ -3868,7 +3870,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::Value(Uint256::from( .add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1,
))); )));
@ -3905,7 +3906,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::Value(Uint256::from( .add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI,
))); )));
@ -3942,7 +3942,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::Value(Uint256::from( .add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI - 1, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI - 1,
))); )));
@ -3979,7 +3978,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::Value(Uint256::from( .add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1,
))); )));
@ -3996,7 +3994,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::WithdrawalsRoot(withdrawals_root)); .add_operation(Operation::WithdrawalsRoot(withdrawals_root));
let epoch = self.chain.epoch().unwrap(); let epoch = self.chain.epoch().unwrap();
@ -4029,7 +4026,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::Value(Uint256::from( .add_operation(Operation::Value(Uint256::from(
DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1,
))); )));
@ -4037,7 +4033,6 @@ impl ApiTester {
self.mock_builder self.mock_builder
.as_ref() .as_ref()
.unwrap() .unwrap()
.builder
.add_operation(Operation::WithdrawalsRoot(Hash256::repeat_byte(0x42))); .add_operation(Operation::WithdrawalsRoot(Hash256::repeat_byte(0x42)));
let slot = self.chain.slot().unwrap(); let slot = self.chain.slot().unwrap();