diff --git a/Cargo.lock b/Cargo.lock index 798bb936..21275ff3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1412,6 +1412,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tree_hash 0.8.0", + "url", ] [[package]] diff --git a/cb-config.toml b/cb-config.toml new file mode 100644 index 00000000..c55b9c18 --- /dev/null +++ b/cb-config.toml @@ -0,0 +1,7 @@ +chain = "Holesky" + +[pbs] +port = 18550 + +[[relays]] +url = "https://0xaa58208899c6105603b74396734a6263cc7d947f444f396a90f7b7d3e65d102aec7e5e5291b27e08d02c50a050825c2f@holesky.titanrelay.xyz" \ No newline at end of file diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 04b66330..19175ebf 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -23,3 +23,5 @@ tracing-subscriber.workspace = true tree_hash.workspace = true eyre.workspace = true + +url.workspace = true \ No newline at end of file diff --git a/tests/data/configs/pbs.happy.toml b/tests/data/configs/pbs.happy.toml new file mode 100644 index 00000000..67f282b2 --- /dev/null +++ b/tests/data/configs/pbs.happy.toml @@ -0,0 +1,26 @@ +chain = "Holesky" + +[pbs] +docker_image = "ghcr.io/commit-boost/pbs:latest" +with_signer = false +host = "127.0.0.1" +port = 18550 +relay_check = true +wait_all_registrations = true +timeout_get_header_ms = 950 +timeout_get_payload_ms = 4000 +timeout_register_validator_ms = 3000 +skip_sigverify = false +min_bid_eth = 0.5 +relay_monitors = [] +late_in_slot_time_ms = 2000 +extra_validation_enabled = false +rpc_url = "https://ethereum-holesky-rpc.publicnode.com" + +[[relays]] +id = "example-relay" +url = "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz" +headers = { X-MyCustomHeader = "MyCustomHeader" } +enable_timing_games = false +target_first_request_ms = 200 +frequency_get_header_ms = 300 \ No newline at end of file diff --git a/tests/src/mock_relay.rs b/tests/src/mock_relay.rs index 672ca806..f2c87598 100644 --- a/tests/src/mock_relay.rs +++ b/tests/src/mock_relay.rs @@ -62,6 +62,9 @@ impl MockRelayState { pub fn received_submit_block(&self) -> u64 { self.received_submit_block.load(Ordering::Relaxed) } + pub fn large_body(&self) -> bool { + self.large_body + } } impl MockRelayState { @@ -108,7 +111,7 @@ async fn handle_get_header( let object_root = response.data.message.tree_hash_root().0; response.data.signature = sign_builder_root(state.chain, &state.signer, object_root); - (StatusCode::OK, axum::Json(response)).into_response() + (StatusCode::OK, Json(response)).into_response() } async fn handle_get_status(State(state): State>) -> impl IntoResponse { @@ -125,14 +128,12 @@ async fn handle_register_validator( StatusCode::OK } -async fn handle_submit_block(State(state): State>) -> impl IntoResponse { +async fn handle_submit_block(State(state): State>) -> Response { state.received_submit_block.fetch_add(1, Ordering::Relaxed); - - let response = if state.large_body { - vec![1u8; 1 + MAX_SIZE_SUBMIT_BLOCK] + if state.large_body() { + (StatusCode::OK, Json(vec![1u8; 1 + MAX_SIZE_SUBMIT_BLOCK])).into_response() } else { - serde_json::to_vec(&SubmitBlindedBlockResponse::default()).unwrap() - }; - - (StatusCode::OK, Json(response)).into_response() + let response = SubmitBlindedBlockResponse::default(); + (StatusCode::OK, Json(response)).into_response() + } } diff --git a/tests/src/mock_validator.rs b/tests/src/mock_validator.rs index 9b7ff2c6..7d31f770 100644 --- a/tests/src/mock_validator.rs +++ b/tests/src/mock_validator.rs @@ -2,13 +2,13 @@ use alloy::{ primitives::B256, rpc::types::beacon::{relay::ValidatorRegistration, BlsPublicKey}, }; -use cb_common::pbs::{GetHeaderResponse, RelayClient, SignedBlindedBeaconBlock}; -use reqwest::Error; +use cb_common::pbs::{RelayClient, SignedBlindedBeaconBlock}; +use reqwest::Response; use crate::utils::generate_mock_relay; pub struct MockValidator { - comm_boost: RelayClient, + pub comm_boost: RelayClient, } impl MockValidator { @@ -16,53 +16,41 @@ impl MockValidator { Ok(Self { comm_boost: generate_mock_relay(port, BlsPublicKey::default())? }) } - pub async fn do_get_header(&self, pubkey: Option) -> Result<(), Error> { - let url = self - .comm_boost - .get_header_url(0, B256::ZERO, pubkey.unwrap_or(BlsPublicKey::ZERO)) - .unwrap(); - let res = self.comm_boost.client.get(url).send().await?.bytes().await?; - assert!(serde_json::from_slice::(&res).is_ok()); - - Ok(()) + pub async fn do_get_header(&self, pubkey: Option) -> eyre::Result { + let url = self.comm_boost.get_header_url(0, B256::ZERO, pubkey.unwrap_or_default())?; + Ok(self.comm_boost.client.get(url).send().await?) } - pub async fn do_get_status(&self) -> Result<(), Error> { - let url = self.comm_boost.get_status_url().unwrap(); - let _res = self.comm_boost.client.get(url).send().await?; - // assert!(res.status().is_success()); - - Ok(()) + pub async fn do_get_status(&self) -> eyre::Result { + let url = self.comm_boost.get_status_url()?; + Ok(self.comm_boost.client.get(url).send().await?) } - pub async fn do_register_validator(&self) -> Result<(), Error> { + pub async fn do_register_validator(&self) -> eyre::Result { self.do_register_custom_validators(vec![]).await } pub async fn do_register_custom_validators( &self, registrations: Vec, - ) -> Result<(), Error> { + ) -> eyre::Result { let url = self.comm_boost.register_validator_url().unwrap(); - self.comm_boost.client.post(url).json(®istrations).send().await?.error_for_status()?; - - Ok(()) + Ok(self.comm_boost.client.post(url).json(®istrations).send().await?) } - pub async fn do_submit_block(&self) -> Result<(), Error> { + pub async fn do_submit_block( + &self, + signed_blinded_block: Option, + ) -> eyre::Result { let url = self.comm_boost.submit_block_url().unwrap(); - let signed_blinded_block = SignedBlindedBeaconBlock::default(); - - self.comm_boost + Ok(self + .comm_boost .client .post(url) - .json(&signed_blinded_block) + .json(&signed_blinded_block.unwrap_or_default()) .send() - .await? - .error_for_status()?; - - Ok(()) + .await?) } } diff --git a/tests/src/utils.rs b/tests/src/utils.rs index f6716a97..fcc01194 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -1,9 +1,13 @@ -use std::sync::Once; +use std::{ + net::{Ipv4Addr, SocketAddr}, + sync::{Arc, Once}, +}; -use alloy::rpc::types::beacon::BlsPublicKey; +use alloy::{primitives::U256, rpc::types::beacon::BlsPublicKey}; use cb_common::{ - config::RelayConfig, + config::{PbsConfig, PbsModuleConfig, RelayConfig}, pbs::{RelayClient, RelayEntry}, + types::Chain, }; use eyre::Result; @@ -51,3 +55,38 @@ pub fn generate_mock_relay_with_batch_size( }; RelayClient::new(config) } + +pub fn get_pbs_static_config(port: u16) -> PbsConfig { + PbsConfig { + host: Ipv4Addr::UNSPECIFIED, + port, + wait_all_registrations: true, + relay_check: true, + timeout_get_header_ms: u64::MAX, + timeout_get_payload_ms: u64::MAX, + timeout_register_validator_ms: u64::MAX, + skip_sigverify: false, + min_bid_wei: U256::ZERO, + late_in_slot_time_ms: u64::MAX, + relay_monitors: vec![], + extra_validation_enabled: false, + rpc_url: None, + } +} + +pub fn to_pbs_config( + chain: Chain, + pbs_config: PbsConfig, + relays: Vec, +) -> PbsModuleConfig { + PbsModuleConfig { + chain, + endpoint: SocketAddr::new(pbs_config.host.into(), pbs_config.port), + pbs_config: Arc::new(pbs_config), + signer_client: None, + event_publisher: None, + all_relays: relays.clone(), + relays, + muxes: None, + } +} diff --git a/tests/tests/config.rs b/tests/tests/config.rs index 044a62c8..3d6ffd70 100644 --- a/tests/tests/config.rs +++ b/tests/tests/config.rs @@ -1,8 +1,12 @@ -use cb_common::{config::CommitBoostConfig, types::Chain}; +use std::net::Ipv4Addr; + +use alloy::primitives::U256; +use cb_common::{config::CommitBoostConfig, types::Chain, utils::WEI_PER_ETH}; use eyre::Result; +use url::Url; #[tokio::test] -async fn test_load_config() -> Result<()> { +async fn test_load_example_config() -> Result<()> { let config = CommitBoostConfig::from_file("../config.example.toml")?; config.validate().await?; assert_eq!(config.chain, Chain::Holesky); @@ -10,3 +14,152 @@ async fn test_load_config() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_load_pbs_happy() -> Result<()> { + let config = CommitBoostConfig::from_file("./data/configs/pbs.happy.toml")?; + config.validate().await?; + + // Chain and existing header check + assert_eq!(config.chain, Chain::Holesky); + assert_eq!( + config.relays[0].headers.as_ref().unwrap().get("X-MyCustomHeader").unwrap(), + "MyCustomHeader" + ); + + // Docker and general settings + assert_eq!(config.pbs.docker_image, "ghcr.io/commit-boost/pbs:latest"); + assert_eq!(config.pbs.with_signer, false); + assert_eq!(config.pbs.pbs_config.host, "127.0.0.1".parse::().unwrap()); + assert_eq!(config.pbs.pbs_config.port, 18550); + assert_eq!(config.pbs.pbs_config.relay_check, true); + assert_eq!(config.pbs.pbs_config.wait_all_registrations, true); + + // Timeouts + assert_eq!(config.pbs.pbs_config.timeout_get_header_ms, 950); + assert_eq!(config.pbs.pbs_config.timeout_get_payload_ms, 4000); + assert_eq!(config.pbs.pbs_config.timeout_register_validator_ms, 3000); + + // Bid settings and validation + assert_eq!(config.pbs.pbs_config.skip_sigverify, false); + dbg!(&config.pbs.pbs_config.min_bid_wei); + dbg!(&U256::from(0.5)); + assert_eq!(config.pbs.pbs_config.min_bid_wei, U256::from((0.5 * WEI_PER_ETH as f64) as u64)); + assert!(config.pbs.pbs_config.relay_monitors.is_empty()); + assert_eq!(config.pbs.pbs_config.late_in_slot_time_ms, 2000); + assert_eq!(config.pbs.pbs_config.extra_validation_enabled, false); + assert_eq!( + config.pbs.pbs_config.rpc_url, + Some("https://ethereum-holesky-rpc.publicnode.com".parse::().unwrap()) + ); + + // Relay specific settings + let relay = &config.relays[0]; + assert_eq!(relay.id, Some("example-relay".to_string())); + assert_eq!(relay.entry.url, "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz".parse::().unwrap()); + assert_eq!(relay.enable_timing_games, false); + assert_eq!(relay.target_first_request_ms, Some(200)); + assert_eq!(relay.frequency_get_header_ms, Some(300)); + + Ok(()) +} + +#[tokio::test] +async fn test_validate_bad_timeout_get_header_ms() -> Result<()> { + let mut config = CommitBoostConfig::from_file("./data/configs/pbs.happy.toml")?; + + // Set invalid timeout + config.pbs.pbs_config.timeout_get_header_ms = 0; + + let result = config.validate().await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("timeout_get_header_ms must be greater than 0")); + + Ok(()) +} + +#[tokio::test] +async fn test_validate_bad_timeout_get_payload_ms() -> Result<()> { + let mut config = CommitBoostConfig::from_file("./data/configs/pbs.happy.toml")?; + config.pbs.pbs_config.timeout_get_payload_ms = 0; + + let result = config.validate().await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("timeout_get_payload_ms must be greater than 0")); + Ok(()) +} + +#[tokio::test] +async fn test_validate_bad_timeout_register_validator_ms() -> Result<()> { + let mut config = CommitBoostConfig::from_file("./data/configs/pbs.happy.toml")?; + config.pbs.pbs_config.timeout_register_validator_ms = 0; + + let result = config.validate().await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("timeout_register_validator_ms must be greater than 0")); + Ok(()) +} + +#[tokio::test] +async fn test_validate_bad_late_in_slot_time_ms() -> Result<()> { + let mut config = CommitBoostConfig::from_file("./data/configs/pbs.happy.toml")?; + config.pbs.pbs_config.late_in_slot_time_ms = 0; + + let result = config.validate().await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("late_in_slot_time_ms must be greater than 0")); + Ok(()) +} + +#[tokio::test] +async fn test_validate_bad_timeout_header_vs_late() -> Result<()> { + let mut config = CommitBoostConfig::from_file("./data/configs/pbs.happy.toml")?; + config.pbs.pbs_config.timeout_get_header_ms = 3000; + config.pbs.pbs_config.late_in_slot_time_ms = 2000; + + let result = config.validate().await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("timeout_get_header_ms must be less than late_in_slot_time_ms")); + Ok(()) +} + +#[tokio::test] +async fn test_validate_bad_min_bid() -> Result<()> { + let mut config = CommitBoostConfig::from_file("./data/configs/pbs.happy.toml")?; + config.pbs.pbs_config.min_bid_wei = U256::from(2 * WEI_PER_ETH); + + let result = config.validate().await; + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("min bid is too high")); + Ok(()) +} + +#[tokio::test] +async fn test_validate_missing_rpc_url() -> Result<()> { + let mut config = CommitBoostConfig::from_file("./data/configs/pbs.happy.toml")?; + config.pbs.pbs_config.extra_validation_enabled = true; + config.pbs.pbs_config.rpc_url = None; + + let result = config.validate().await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("rpc_url is required if extra_validation_enabled is true")); + Ok(()) +} diff --git a/tests/tests/payloads.rs b/tests/tests/payloads.rs index 8a2c1a68..60bb9ce8 100644 --- a/tests/tests/payloads.rs +++ b/tests/tests/payloads.rs @@ -3,6 +3,9 @@ use cb_common::{ pbs::{SignedBlindedBeaconBlock, SubmitBlindedBlockResponse}, utils::test_encode_decode, }; +use serde_json::Value; + +// Happy path tests #[test] fn test_registrations() { let data = include_str!("../data/registration_holesky.json"); @@ -20,3 +23,102 @@ fn test_submit_block_response() { let data = include_str!("../data/submit_block_response_holesky.json"); test_encode_decode::(&data); } + +// Unhappy path tests +fn test_missing_registration_field(field_name: &str) -> String { + let data = include_str!("../data/registration_holesky.json"); + let mut values: Value = serde_json::from_str(data).unwrap(); + + // Remove specified field from the first validator's message + if let Value::Array(arr) = &mut values { + if let Some(first_validator) = arr.get_mut(0) { + if let Some(message) = first_validator.get_mut("message") { + if let Value::Object(msg_obj) = message { + msg_obj.remove(field_name); + } + } + } + } + + // This should fail since the field is required + let result = serde_json::from_value::>(values); + assert!(result.is_err()); + result.unwrap_err().to_string() +} + +#[test] +fn test_registration_missing_fields() { + let fields = ["fee_recipient", "gas_limit", "timestamp", "pubkey"]; + + for field in fields { + let error = test_missing_registration_field(field); + assert!( + error.contains(&format!("missing field `{}`", field)), + "Expected error about missing {}, got: {}", + field, + error + ); + } +} + +fn test_missing_signed_blinded_block_field(field_name: &str) -> String { + let data = include_str!("../data/signed_blinded_block_holesky.json"); + let mut values: Value = serde_json::from_str(data).unwrap(); + + // Remove specified field from the message + if let Some(message) = values.get_mut("message") { + if let Value::Object(msg_obj) = message { + msg_obj.remove(field_name); + } + } + + // This should fail since the field is required + let result = serde_json::from_value::(values); + assert!(result.is_err()); + result.unwrap_err().to_string() +} + +#[test] +fn test_signed_blinded_block_missing_fields() { + let fields = ["slot", "proposer_index", "parent_root", "state_root", "body"]; + + for field in fields { + let error = test_missing_signed_blinded_block_field(field); + assert!( + error.contains(&format!("missing field `{}`", field)), + "Expected error about missing {}, got: {}", + field, + error + ); + } +} + +fn test_missing_submit_block_response_field(field_name: &str) -> String { + let data = include_str!("../data/submit_block_response_holesky.json"); + let mut values: Value = serde_json::from_str(data).unwrap(); + + // Remove specified field + if let Value::Object(obj) = &mut values { + obj.remove(field_name); + } + + // This should fail since the field is required + let result = serde_json::from_value::(values); + assert!(result.is_err()); + result.unwrap_err().to_string() +} + +#[test] +fn test_submit_block_response_missing_fields() { + let fields = ["version", "data"]; + + for field in fields { + let error = test_missing_submit_block_response_field(field); + assert!( + error.contains(&format!("missing field `{}`", field)), + "Expected error about missing {}, got: {}", + field, + error + ); + } +} diff --git a/tests/tests/pbs_get_header.rs b/tests/tests/pbs_get_header.rs new file mode 100644 index 00000000..898ea964 --- /dev/null +++ b/tests/tests/pbs_get_header.rs @@ -0,0 +1,141 @@ +use std::{sync::Arc, time::Duration}; + +use alloy::primitives::{B256, U256}; +use cb_common::{ + pbs::GetHeaderResponse, + signature::sign_builder_root, + signer::{random_secret, BlsPublicKey}, + types::Chain, + utils::{blst_pubkey_to_alloy, timestamp_of_slot_start_sec}, +}; +use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; +use cb_tests::{ + mock_relay::{start_mock_relay_service, MockRelayState}, + mock_validator::MockValidator, + utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, +}; +use eyre::Result; +use reqwest::StatusCode; +use tracing::info; +use tree_hash::TreeHash; + +#[tokio::test] +async fn test_get_header() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 3200; + let relay_port = pbs_port + 1; + + // Run a mock relay + let mock_state = Arc::new(MockRelayState::new(chain, signer)); + let mock_relay = generate_mock_relay(relay_port, *pubkey)?; + tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); + + // Run the PBS service + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![mock_relay.clone()]); + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending get header"); + let res = mock_validator.do_get_header(None).await?; + assert_eq!(res.status(), StatusCode::OK); + + let res = serde_json::from_slice::(&res.bytes().await?)?; + + assert_eq!(mock_state.received_get_header(), 1); + assert_eq!(res.data.message.header.block_hash.0[0], 1); + assert_eq!(res.data.message.header.parent_hash, B256::ZERO); + assert_eq!(res.data.message.value, U256::from(10)); + assert_eq!(res.data.message.pubkey, blst_pubkey_to_alloy(&mock_state.signer.sk_to_pk())); + assert_eq!(res.data.message.header.timestamp, timestamp_of_slot_start_sec(0, chain)); + assert_eq!( + res.data.signature, + sign_builder_root(chain, &mock_state.signer, res.data.message.tree_hash_root().0) + ); + Ok(()) +} + +#[tokio::test] +async fn test_get_header_returns_204_if_relay_down() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 3300; + let relay_port = pbs_port + 1; + + // Create a mock relay client + let mock_state = Arc::new(MockRelayState::new(chain, signer)); + let mock_relay = generate_mock_relay(relay_port, *pubkey)?; + + // Don't start the relay + // tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); + + // Run the PBS service + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![mock_relay.clone()]); + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending get header"); + let res = mock_validator.do_get_header(None).await?; + + assert_eq!(res.status(), StatusCode::NO_CONTENT); // 204 error + assert_eq!(mock_state.received_get_header(), 0); // no header received + Ok(()) +} + +#[tokio::test] +async fn test_get_header_returns_400_if_request_is_invalid() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 3400; + let relay_port = pbs_port + 1; + + // Run a mock relay + let mock_state = Arc::new(MockRelayState::new(chain, signer)); + let mock_relay = generate_mock_relay(relay_port, *pubkey)?; + tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); + + // Run the PBS service + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![mock_relay.clone()]); + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + // Create an invalid URL by truncating the pubkey + let mut bad_url = mock_relay.get_header_url(0, B256::ZERO, *pubkey).unwrap(); + bad_url.set_path(&bad_url.path().replace(&pubkey.to_string(), &pubkey.to_string()[..10])); + + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending get header with invalid pubkey URL"); + // Use the bad_url in the request instead of the default + let res = mock_validator.comm_boost.client.get(bad_url).send().await?; + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + + // Attempt again by truncating the parent hash + let mut bad_url = mock_relay.get_header_url(0, B256::ZERO, *pubkey).unwrap(); + bad_url + .set_path(&bad_url.path().replace(&B256::ZERO.to_string(), &B256::ZERO.to_string()[..10])); + let res = mock_validator.comm_boost.client.get(bad_url).send().await?; + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + + assert_eq!(mock_state.received_get_header(), 0); // no header received + Ok(()) +} diff --git a/tests/tests/pbs_get_status.rs b/tests/tests/pbs_get_status.rs new file mode 100644 index 00000000..2dd06fd9 --- /dev/null +++ b/tests/tests/pbs_get_status.rs @@ -0,0 +1,85 @@ +use std::{sync::Arc, time::Duration}; + +use cb_common::{ + signer::{random_secret, BlsPublicKey}, + types::Chain, + utils::blst_pubkey_to_alloy, +}; +use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; +use cb_tests::{ + mock_relay::{start_mock_relay_service, MockRelayState}, + mock_validator::MockValidator, + utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, +}; +use eyre::Result; +use reqwest::StatusCode; +use tracing::info; + +#[tokio::test] +async fn test_get_status() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 3500; + let relay_0_port = pbs_port + 1; + let relay_1_port = pbs_port + 2; + + let relays = vec![ + generate_mock_relay(relay_0_port, *pubkey)?, + generate_mock_relay(relay_1_port, *pubkey)?, + ]; + let mock_state = Arc::new(MockRelayState::new(chain, signer)); + tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_0_port)); + tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_1_port)); + + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays.clone()); + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending get status"); + let res = mock_validator.do_get_status().await.expect("failed to get status"); + assert_eq!(res.status(), StatusCode::OK); + + // Expect two statuses since two relays in config + assert_eq!(mock_state.received_get_status(), 2); + Ok(()) +} + +#[tokio::test] +async fn test_get_status_returns_502_if_relay_down() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 3600; + let relay_port = pbs_port + 1; + + let relays = vec![generate_mock_relay(relay_port, *pubkey)?]; + let mock_state = Arc::new(MockRelayState::new(chain, signer)); + + // Don't start the relay + // tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); + + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays.clone()); + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending get status"); + let res = mock_validator.do_get_status().await.expect("failed to get status"); + assert_eq!(res.status(), StatusCode::BAD_GATEWAY); // 502 error + + // Expect no statuses since relay is down + assert_eq!(mock_state.received_get_status(), 0); + Ok(()) +} diff --git a/tests/tests/pbs_integration.rs b/tests/tests/pbs_integration.rs deleted file mode 100644 index 2e7f95e8..00000000 --- a/tests/tests/pbs_integration.rs +++ /dev/null @@ -1,313 +0,0 @@ -use std::{ - collections::HashMap, - net::{Ipv4Addr, SocketAddr}, - sync::Arc, - time::Duration, - u64, -}; - -use alloy::{primitives::U256, rpc::types::beacon::relay::ValidatorRegistration}; -use cb_common::{ - config::{PbsConfig, PbsModuleConfig, RuntimeMuxConfig}, - pbs::RelayClient, - signer::{random_secret, BlsPublicKey}, - types::Chain, - utils::blst_pubkey_to_alloy, -}; -use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; -use cb_tests::{ - mock_relay::{start_mock_relay_service, MockRelayState}, - mock_validator::MockValidator, - utils::{generate_mock_relay, generate_mock_relay_with_batch_size, setup_test_env}, -}; -use eyre::Result; -use tracing::info; - -fn get_pbs_static_config(port: u16) -> PbsConfig { - PbsConfig { - host: Ipv4Addr::UNSPECIFIED, - port, - wait_all_registrations: true, - relay_check: true, - timeout_get_header_ms: u64::MAX, - timeout_get_payload_ms: u64::MAX, - timeout_register_validator_ms: u64::MAX, - skip_sigverify: false, - min_bid_wei: U256::ZERO, - late_in_slot_time_ms: u64::MAX, - relay_monitors: vec![], - extra_validation_enabled: false, - rpc_url: None, - } -} - -fn to_pbs_config(chain: Chain, pbs_config: PbsConfig, relays: Vec) -> PbsModuleConfig { - PbsModuleConfig { - chain, - endpoint: SocketAddr::new(pbs_config.host.into(), pbs_config.port), - pbs_config: Arc::new(pbs_config), - signer_client: None, - event_publisher: None, - all_relays: relays.clone(), - relays, - muxes: None, - } -} - -#[tokio::test] -async fn test_get_header() -> Result<()> { - setup_test_env(); - let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); - - let chain = Chain::Holesky; - let port = 3000; - - let mock_state = Arc::new(MockRelayState::new(chain, signer)); - let mock_relay = generate_mock_relay(port + 1, *pubkey)?; - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); - - let config = to_pbs_config(chain, get_pbs_static_config(port), vec![mock_relay]); - let state = PbsState::new(config); - tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); - - // leave some time to start servers - tokio::time::sleep(Duration::from_millis(100)).await; - - let mock_validator = MockValidator::new(port)?; - info!("Sending get header"); - let res = mock_validator.do_get_header(None).await; - - assert!(res.is_ok()); - assert_eq!(mock_state.received_get_header(), 1); - Ok(()) -} - -#[tokio::test] -async fn test_get_status() -> Result<()> { - setup_test_env(); - let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); - - let chain = Chain::Holesky; - let port = 3100; - - let relays = - vec![generate_mock_relay(port + 1, *pubkey)?, generate_mock_relay(port + 2, *pubkey)?]; - let mock_state = Arc::new(MockRelayState::new(chain, signer)); - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 2)); - - let config = to_pbs_config(chain, get_pbs_static_config(port), relays); - let state = PbsState::new(config); - tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); - - // leave some time to start servers - tokio::time::sleep(Duration::from_millis(100)).await; - - let mock_validator = MockValidator::new(port)?; - info!("Sending get status"); - let res = mock_validator.do_get_status().await; - - assert!(res.is_ok()); - assert_eq!(mock_state.received_get_status(), 2); - Ok(()) -} - -#[tokio::test] -async fn test_register_validators() -> Result<()> { - setup_test_env(); - let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); - - let chain = Chain::Holesky; - let port = 3300; - - let relays = vec![generate_mock_relay(port + 1, *pubkey)?]; - let mock_state = Arc::new(MockRelayState::new(chain, signer)); - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); - - let config = to_pbs_config(chain, get_pbs_static_config(port), relays); - let state = PbsState::new(config); - tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); - - // leave some time to start servers - tokio::time::sleep(Duration::from_millis(100)).await; - - let mock_validator = MockValidator::new(port)?; - info!("Sending register validator"); - let res = mock_validator.do_register_validator().await; - - assert!(res.is_ok()); - assert_eq!(mock_state.received_register_validator(), 1); - Ok(()) -} - -#[tokio::test] -async fn test_batch_register_validators() -> Result<()> { - setup_test_env(); - let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); - - let chain = Chain::Holesky; - let port = 3310; - - let relays = vec![generate_mock_relay_with_batch_size(port + 1, *pubkey, 5)?]; - let mock_state = Arc::new(MockRelayState::new(chain, signer)); - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); - - let config = to_pbs_config(chain, get_pbs_static_config(port), relays); - let state = PbsState::new(config); - tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); - - // leave some time to start servers - tokio::time::sleep(Duration::from_millis(100)).await; - - let data = include_str!("../data/registration_holesky.json"); - let registrations: Vec = serde_json::from_str(data)?; - - let mock_validator = MockValidator::new(port)?; - info!("Sending register validator"); - let res = mock_validator.do_register_custom_validators(registrations.clone()).await; - - // registrations.len() == 17. 5 per batch, 4 batches - assert!(res.is_ok()); - assert_eq!(mock_state.received_register_validator(), 4); - - let mock_validator = MockValidator::new(port)?; - info!("Sending register validator"); - let res = mock_validator.do_register_custom_validators(registrations[..2].to_vec()).await; - - // Expected one more registration request - assert!(res.is_ok()); - assert_eq!(mock_state.received_register_validator(), 5); - - Ok(()) -} - -#[tokio::test] -async fn test_submit_block() -> Result<()> { - setup_test_env(); - let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); - - let chain = Chain::Holesky; - let port = 3400; - - let relays = vec![generate_mock_relay(port + 1, *pubkey)?]; - let mock_state = Arc::new(MockRelayState::new(chain, signer)); - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); - - let config = to_pbs_config(chain, get_pbs_static_config(port), relays); - let state = PbsState::new(config); - tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); - - // leave some time to start servers - tokio::time::sleep(Duration::from_millis(100)).await; - - let mock_validator = MockValidator::new(port)?; - info!("Sending submit block"); - let res = mock_validator.do_submit_block().await; - - assert!(res.is_err()); - assert_eq!(mock_state.received_submit_block(), 1); - Ok(()) -} - -#[tokio::test] -async fn test_submit_block_too_large() -> Result<()> { - setup_test_env(); - let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); - - let chain = Chain::Holesky; - let port = 3500; - - let relays = vec![generate_mock_relay(port + 1, *pubkey)?]; - let mock_state = Arc::new(MockRelayState::new(chain, signer).with_large_body()); - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); - - let config = to_pbs_config(chain, get_pbs_static_config(port), relays); - let state = PbsState::new(config); - tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); - - // leave some time to start servers - tokio::time::sleep(Duration::from_millis(100)).await; - - let mock_validator = MockValidator::new(port)?; - info!("Sending submit block"); - let res = mock_validator.do_submit_block().await; - - assert!(res.is_err()); - assert_eq!(mock_state.received_submit_block(), 1); - Ok(()) -} - -#[tokio::test] -async fn test_mux() -> Result<()> { - setup_test_env(); - let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); - - let chain = Chain::Holesky; - let port = 3600; - - let mux_relay_1 = generate_mock_relay(port + 1, *pubkey)?; - let mux_relay_2 = generate_mock_relay(port + 2, *pubkey)?; - let default_relay = generate_mock_relay(port + 3, *pubkey)?; - - let mock_state = Arc::new(MockRelayState::new(chain, signer)); - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 1)); - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 2)); - tokio::spawn(start_mock_relay_service(mock_state.clone(), port + 3)); - - let relays = vec![default_relay.clone()]; - let mut config = to_pbs_config(chain, get_pbs_static_config(port), relays); - config.all_relays = vec![mux_relay_1.clone(), mux_relay_2.clone(), default_relay.clone()]; - - let mux = RuntimeMuxConfig { - id: String::from("test"), - config: config.pbs_config.clone(), - relays: vec![mux_relay_1, mux_relay_2], - }; - - let validator_pubkey = blst_pubkey_to_alloy(&random_secret().sk_to_pk()); - - config.muxes = Some(HashMap::from([(validator_pubkey, mux)])); - - let state = PbsState::new(config); - tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); - - // leave some time to start servers - tokio::time::sleep(Duration::from_millis(100)).await; - - let mock_validator = MockValidator::new(port)?; - info!("Sending get header with default"); - let res = mock_validator.do_get_header(None).await; - - assert!(res.is_ok()); - assert_eq!(mock_state.received_get_header(), 1); // only default relay was used - - info!("Sending get header with mux"); - let res = mock_validator.do_get_header(Some(validator_pubkey)).await; - - assert!(res.is_ok()); - assert_eq!(mock_state.received_get_header(), 3); // two mux relays were used - - let res = mock_validator.do_get_status().await; - - assert!(res.is_ok()); - assert_eq!(mock_state.received_get_status(), 3); // default + 2 mux relays were used - - let res = mock_validator.do_register_validator().await; - - assert!(res.is_ok()); - assert_eq!(mock_state.received_register_validator(), 3); // default + 2 mux relays were used - - let res = mock_validator.do_submit_block().await; - - assert!(res.is_err()); - assert_eq!(mock_state.received_submit_block(), 3); // default + 2 mux relays were used - - Ok(()) -} diff --git a/tests/tests/pbs_mux.rs b/tests/tests/pbs_mux.rs new file mode 100644 index 00000000..111fe27e --- /dev/null +++ b/tests/tests/pbs_mux.rs @@ -0,0 +1,91 @@ +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use cb_common::{ + config::RuntimeMuxConfig, + signer::{random_secret, BlsPublicKey}, + types::Chain, + utils::blst_pubkey_to_alloy, +}; +use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; +use cb_tests::{ + mock_relay::{start_mock_relay_service, MockRelayState}, + mock_validator::MockValidator, + utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, +}; +use eyre::Result; +use reqwest::StatusCode; +use tracing::info; + +#[tokio::test] +async fn test_mux() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 3700; + + let mux_relay_1 = generate_mock_relay(pbs_port + 1, *pubkey)?; + let mux_relay_2 = generate_mock_relay(pbs_port + 2, *pubkey)?; + let default_relay = generate_mock_relay(pbs_port + 3, *pubkey)?; + + // Run 3 mock relays + let mock_state = Arc::new(MockRelayState::new(chain, signer)); + tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); + tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 2)); + tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 3)); + + // Register all relays in PBS config + let relays = vec![default_relay.clone()]; + let mut config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + config.all_relays = vec![mux_relay_1.clone(), mux_relay_2.clone(), default_relay.clone()]; + + // Configure mux for two relays + let mux = RuntimeMuxConfig { + id: String::from("test"), + config: config.pbs_config.clone(), + relays: vec![mux_relay_1, mux_relay_2], + }; + + // Bind mux to a specific validator key + let validator_pubkey = blst_pubkey_to_alloy(&random_secret().sk_to_pk()); + config.muxes = Some(HashMap::from([(validator_pubkey, mux)])); + + // Run PBS service + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + // Send default request without specifying a validator key + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending get header with default"); + assert_eq!(mock_validator.do_get_header(None).await?.status(), StatusCode::OK); + assert_eq!(mock_state.received_get_header(), 1); // only default relay was used + + // Send request specifying a validator key to use mux + info!("Sending get header with mux"); + assert_eq!( + mock_validator.do_get_header(Some(validator_pubkey)).await?.status(), + StatusCode::OK + ); + assert_eq!(mock_state.received_get_header(), 3); // two mux relays were used + + // Status requests should go to all relays + info!("Sending get status"); + assert_eq!(mock_validator.do_get_status().await?.status(), StatusCode::OK); + assert_eq!(mock_state.received_get_status(), 3); // default + 2 mux relays were used + + // Register requests should go to all relays + info!("Sending register validator"); + assert_eq!(mock_validator.do_register_validator().await?.status(), StatusCode::OK); + assert_eq!(mock_state.received_register_validator(), 3); // default + 2 mux relays were used + + // Submit block requests should go to all relays + info!("Sending submit block"); + assert_eq!(mock_validator.do_submit_block(None).await?.status(), StatusCode::OK); + assert_eq!(mock_state.received_submit_block(), 3); // default + 2 mux relays were used + + Ok(()) +} diff --git a/tests/tests/pbs_post_blinded_blocks.rs b/tests/tests/pbs_post_blinded_blocks.rs new file mode 100644 index 00000000..64255195 --- /dev/null +++ b/tests/tests/pbs_post_blinded_blocks.rs @@ -0,0 +1,81 @@ +use std::{sync::Arc, time::Duration}; + +use cb_common::{ + pbs::{SignedBlindedBeaconBlock, SubmitBlindedBlockResponse}, + signer::{random_secret, BlsPublicKey}, + types::Chain, + utils::blst_pubkey_to_alloy, +}; +use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; +use cb_tests::{ + mock_relay::{start_mock_relay_service, MockRelayState}, + mock_validator::MockValidator, + utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, +}; +use eyre::Result; +use reqwest::StatusCode; +use tracing::info; + +#[tokio::test] +async fn test_submit_block() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 3800; + + // Run a mock relay + let relays = vec![generate_mock_relay(pbs_port + 1, *pubkey)?]; + let mock_state = Arc::new(MockRelayState::new(chain, signer)); + tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); + + // Run the PBS service + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending submit block"); + let res = mock_validator.do_submit_block(Some(SignedBlindedBeaconBlock::default())).await?; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(mock_state.received_submit_block(), 1); + + let response_body = serde_json::from_slice::(&res.bytes().await?)?; + assert_eq!(response_body.block_hash(), SubmitBlindedBlockResponse::default().block_hash()); + Ok(()) +} + +#[tokio::test] +async fn test_submit_block_too_large() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 3900; + + let relays = vec![generate_mock_relay(pbs_port + 1, *pubkey)?]; + let mock_state = Arc::new(MockRelayState::new(chain, signer).with_large_body()); + tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); + + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending submit block"); + let res = mock_validator.do_submit_block(None).await; + + // response size exceeds max size: max: 20971520 + assert_eq!(res.unwrap().status(), StatusCode::BAD_GATEWAY); + assert_eq!(mock_state.received_submit_block(), 1); + Ok(()) +} diff --git a/tests/tests/pbs_post_validators.rs b/tests/tests/pbs_post_validators.rs new file mode 100644 index 00000000..6db80d3d --- /dev/null +++ b/tests/tests/pbs_post_validators.rs @@ -0,0 +1,203 @@ +use std::{sync::Arc, time::Duration}; + +use alloy::rpc::types::beacon::relay::ValidatorRegistration; +use cb_common::{ + signer::{random_secret, BlsPublicKey}, + types::Chain, + utils::blst_pubkey_to_alloy, +}; +use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; +use cb_tests::{ + mock_relay::{start_mock_relay_service, MockRelayState}, + mock_validator::MockValidator, + utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, +}; +use eyre::Result; +use reqwest::StatusCode; +use tracing::info; + +#[tokio::test] +async fn test_register_validators() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 4000; + + // Run a mock relay + let relays = vec![generate_mock_relay(pbs_port + 1, *pubkey)?]; + let mock_state = Arc::new(MockRelayState::new(chain, signer)); + tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); + + // Run the PBS service + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + let mock_validator = MockValidator::new(pbs_port)?; + info!("Sending register validator"); + + let registration: ValidatorRegistration = serde_json::from_str( + r#"{ + "message": { + "fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "gas_limit": "100000", + "timestamp": "1000000", + "pubkey": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "signature": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }"#, + )?; + + let registrations = vec![registration]; + let res = mock_validator.do_register_custom_validators(registrations).await?; + + assert_eq!(mock_state.received_register_validator(), 1); + assert_eq!(res.status(), StatusCode::OK); + + Ok(()) +} + +#[tokio::test] +async fn test_register_validators_returns_422_if_request_is_malformed() -> Result<()> { + setup_test_env(); + let signer = random_secret(); + let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()).into(); + + let chain = Chain::Holesky; + let pbs_port = 4100; + + // Run a mock relay + let relays = vec![generate_mock_relay(pbs_port + 1, *pubkey)?]; + let mock_state = Arc::new(MockRelayState::new(chain, signer)); + tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); + + // Run the PBS service + let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let state = PbsState::new(config); + tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); + + // leave some time to start servers + tokio::time::sleep(Duration::from_millis(100)).await; + + let mock_validator = MockValidator::new(pbs_port)?; + let url = mock_validator.comm_boost.register_validator_url().unwrap(); + info!("Sending register validator"); + + // Bad fee recipient + let bad_json = r#"[{ + "message": { + "fee_recipient": "0xaa", + "gas_limit": "100000", + "timestamp": "1000000", + "pubkey": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "signature": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }]"#; + + let res = mock_validator + .comm_boost + .client + .post(url.clone()) + .header("Content-Type", "application/json") + .body(bad_json) + .send() + .await?; + + assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); + + // Bad pubkey + let bad_json = r#"[{ + "message": { + "fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "gas_limit": "100000", + "timestamp": "1000000", + "pubkey": "0xbbb" + }, + "signature": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }]"#; + + let res = mock_validator + .comm_boost + .client + .post(url.clone()) + .header("Content-Type", "application/json") + .body(bad_json) + .send() + .await?; + + assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); + + // Bad signature + let bad_json = r#"[{ + "message": { + "fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "gas_limit": "100000", + "timestamp": "1000000", + "pubkey": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "signature": "0xcccc" + }]"#; + + let res = mock_validator + .comm_boost + .client + .post(url.clone()) + .header("Content-Type", "application/json") + .body(bad_json) + .send() + .await?; + + assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); + + // gas limit too high + let bad_json = r#"[{ + "message": { + "fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "gas_limit": "10000000000000000000000000000000000000000000000000000000", + "timestamp": "1000000", + "pubkey": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "signature": "0xcccc" + }]"#; + + let res = mock_validator + .comm_boost + .client + .post(url.clone()) + .header("Content-Type", "application/json") + .body(bad_json) + .send() + .await?; + + assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); + + // timestamp too high + let bad_json = r#"[{ + "message": { + "fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "gas_limit": "1000000", + "timestamp": "10000000000000000000000000000000000000000000000000000000", + "pubkey": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "signature": "0xcccc" + }]"#; + + let res = mock_validator + .comm_boost + .client + .post(url.clone()) + .header("Content-Type", "application/json") + .body(bad_json) + .send() + .await?; + + assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); + + assert_eq!(mock_state.received_register_validator(), 0); + Ok(()) +}