Skip to content

Commit 6007fe5

Browse files
committed
fix warnings + add more integration tests
1 parent 0fcf9bc commit 6007fe5

5 files changed

Lines changed: 168 additions & 9 deletions

File tree

tests/src/mock_relay.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ pub struct MockRelayState {
6969
/// the short-circuit so tests can observe the attempt. Used to drive C3
7070
/// (retry-as-JSON) tests.
7171
submit_block_ssz_status_override: Option<StatusCode>,
72+
/// If set, this literal string is sent as the outgoing `Content-Type`
73+
/// header on `handle_get_header` and `handle_submit_block_v1` responses
74+
/// instead of the canonical `application/json` / `application/octet-stream`
75+
/// value. The body is still serialized according to the encoding that was
76+
/// negotiated via `Accept`. Used to exercise PBS tolerance of
77+
/// MIME-parameter suffixes like `application/octet-stream; charset=binary`.
78+
response_content_type_override: Option<String>,
7279
received_get_header: Arc<AtomicU64>,
7380
received_get_status: Arc<AtomicU64>,
7481
received_register_validator: Arc<AtomicU64>,
@@ -102,6 +109,9 @@ impl MockRelayState {
102109
pub fn submit_block_ssz_status_override(&self) -> Option<StatusCode> {
103110
self.submit_block_ssz_status_override
104111
}
112+
pub fn response_content_type_override(&self) -> Option<&str> {
113+
self.response_content_type_override.as_deref()
114+
}
105115
pub fn set_response_override(&self, status: StatusCode) {
106116
*self.response_override.write().unwrap() = Some(status);
107117
}
@@ -116,6 +126,7 @@ impl MockRelayState {
116126
supports_submit_block_v2: true,
117127
use_not_found_for_submit_block: false,
118128
submit_block_ssz_status_override: None,
129+
response_content_type_override: None,
119130
received_get_header: Default::default(),
120131
received_get_status: Default::default(),
121132
received_register_validator: Default::default(),
@@ -154,6 +165,15 @@ impl MockRelayState {
154165
pub fn with_submit_block_ssz_status(self, status: StatusCode) -> Self {
155166
Self { submit_block_ssz_status_override: Some(status), ..self }
156167
}
168+
169+
/// Make the relay advertise `raw_content_type` as the `Content-Type`
170+
/// header on `get_header` and `submit_block_v1` responses. The body is
171+
/// still encoded via the negotiated [`EncodingType`] — only the header
172+
/// string changes. Use this to drive PBS tolerance of MIME-parameter
173+
/// suffixes (e.g. `application/octet-stream; charset=binary`).
174+
pub fn with_response_content_type(self, raw_content_type: impl Into<String>) -> Self {
175+
Self { response_content_type_override: Some(raw_content_type.into()), ..self }
176+
}
157177
}
158178

159179
pub fn mock_relay_app_router(state: Arc<MockRelayState>) -> Router {
@@ -277,7 +297,11 @@ async fn handle_get_header(
277297
let mut response = (StatusCode::OK, data).into_response();
278298
let consensus_version_header =
279299
HeaderValue::from_str(&consensus_version_header.to_string()).unwrap();
280-
let content_type_header = HeaderValue::from_str(&content_type.to_string()).unwrap();
300+
let content_type_str = state
301+
.response_content_type_override()
302+
.map(|s| s.to_string())
303+
.unwrap_or_else(|| content_type.to_string());
304+
let content_type_header = HeaderValue::from_str(&content_type_str).unwrap();
281305
response.headers_mut().insert(CONSENSUS_VERSION_HEADER, consensus_version_header);
282306
response.headers_mut().insert(CONTENT_TYPE, content_type_header);
283307
response
@@ -404,7 +428,11 @@ async fn handle_submit_block_v1(
404428
HeaderValue::from_str(&consensus_version_header.to_string()).unwrap();
405429
response.headers_mut().insert(CONSENSUS_VERSION_HEADER, consensus_version_header);
406430
}
407-
let content_type_header = HeaderValue::from_str(&response_content_type.to_string()).unwrap();
431+
let content_type_str = state
432+
.response_content_type_override()
433+
.map(|s| s.to_string())
434+
.unwrap_or_else(|| response_content_type.to_string());
435+
let content_type_header = HeaderValue::from_str(&content_type_str).unwrap();
408436
response.headers_mut().insert(CONTENT_TYPE, content_type_header);
409437
response
410438
}

tests/tests/pbs_get_header.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,89 @@ async fn test_get_header_bid_validation_matrix() -> Result<()> {
644644
Ok(())
645645
}
646646

647+
/// PBS must accept relay `Content-Type` values that include MIME parameters
648+
/// (e.g. `application/octet-stream; charset=binary`). The audit fix for C2
649+
/// switched `EncodingType::from_str` to parse via the `mediatype` crate;
650+
/// this test exercises the full relay→PBS→BN path to guard against
651+
/// regressions at the wire boundary.
652+
#[tokio::test]
653+
async fn test_get_header_tolerates_mime_params_in_content_type() -> Result<()> {
654+
setup_test_env();
655+
let signer = random_secret();
656+
let pubkey = signer.public_key();
657+
let chain = Chain::Holesky;
658+
let pbs_listener = get_free_listener().await;
659+
let relay_listener = get_free_listener().await;
660+
let pbs_port = pbs_listener.local_addr().unwrap().port();
661+
let relay_port = relay_listener.local_addr().unwrap().port();
662+
663+
let mut mock_state = MockRelayState::new(chain, signer)
664+
.with_response_content_type("application/octet-stream; charset=binary");
665+
mock_state.supported_content_types = Arc::new(HashSet::from([EncodingType::Ssz]));
666+
let mock_state = Arc::new(mock_state);
667+
let mock_relay = generate_mock_relay(relay_port, pubkey)?;
668+
tokio::spawn(start_mock_relay_service_with_listener(mock_state.clone(), relay_listener));
669+
670+
let pbs_config = get_pbs_config(pbs_port);
671+
let config = to_pbs_config(chain, pbs_config, vec![mock_relay]);
672+
let state = PbsState::new(config, PathBuf::new());
673+
drop(pbs_listener);
674+
tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state));
675+
676+
tokio::time::sleep(Duration::from_millis(100)).await;
677+
678+
let mock_validator = MockValidator::new(pbs_port)?;
679+
let res =
680+
mock_validator.do_get_header(None, vec![EncodingType::Ssz], ForkName::Electra).await?;
681+
assert_eq!(res.status(), StatusCode::OK, "PBS should tolerate `; charset=binary` MIME param");
682+
assert_eq!(mock_state.received_get_header(), 1);
683+
684+
let fork = get_consensus_version_header(res.headers()).expect("missing fork version header");
685+
let bytes = res.bytes().await?;
686+
let data = SignedBuilderBid::from_ssz_bytes_by_fork(&bytes, fork).unwrap();
687+
assert_eq!(data.message.header().block_hash().0[0], 1);
688+
Ok(())
689+
}
690+
691+
/// Same guarantee on the JSON path: `application/json; charset=utf-8` (the
692+
/// value some production relays actually emit) must be accepted as JSON.
693+
#[tokio::test]
694+
async fn test_get_header_tolerates_json_charset_param() -> Result<()> {
695+
setup_test_env();
696+
let signer = random_secret();
697+
let pubkey = signer.public_key();
698+
let chain = Chain::Holesky;
699+
let pbs_listener = get_free_listener().await;
700+
let relay_listener = get_free_listener().await;
701+
let pbs_port = pbs_listener.local_addr().unwrap().port();
702+
let relay_port = relay_listener.local_addr().unwrap().port();
703+
704+
let mut mock_state = MockRelayState::new(chain, signer)
705+
.with_response_content_type("application/json; charset=utf-8");
706+
mock_state.supported_content_types = Arc::new(HashSet::from([EncodingType::Json]));
707+
let mock_state = Arc::new(mock_state);
708+
let mock_relay = generate_mock_relay(relay_port, pubkey)?;
709+
tokio::spawn(start_mock_relay_service_with_listener(mock_state.clone(), relay_listener));
710+
711+
let pbs_config = get_pbs_config(pbs_port);
712+
let config = to_pbs_config(chain, pbs_config, vec![mock_relay]);
713+
let state = PbsState::new(config, PathBuf::new());
714+
drop(pbs_listener);
715+
tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state));
716+
717+
tokio::time::sleep(Duration::from_millis(100)).await;
718+
719+
let mock_validator = MockValidator::new(pbs_port)?;
720+
let res =
721+
mock_validator.do_get_header(None, vec![EncodingType::Json], ForkName::Electra).await?;
722+
assert_eq!(res.status(), StatusCode::OK, "PBS should tolerate `; charset=utf-8` MIME param");
723+
assert_eq!(mock_state.received_get_header(), 1);
724+
725+
let body: GetHeaderResponse = serde_json::from_slice(&res.bytes().await?)?;
726+
assert_eq!(body.data.message.header().block_hash().0[0], 1);
727+
Ok(())
728+
}
729+
647730
/// Standard mode rejects a bid whose embedded pubkey does not match the relay's
648731
/// configured pubkey; None mode forwards it unchecked, proving the bypass works
649732
/// for the signature/pubkey validation check.

tests/tests/pbs_mux.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
use std::{
2-
collections::{HashMap, HashSet},
3-
path::PathBuf,
4-
sync::Arc,
5-
time::Duration,
6-
};
1+
use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration};
72

83
use alloy::primitives::U256;
94
use cb_common::{

tests/tests/pbs_mux_refresh.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration};
1+
use std::{path::PathBuf, sync::Arc, time::Duration};
22

33
use cb_common::{
44
config::{MuxConfig, MuxKeysLoader, PbsMuxes},

tests/tests/pbs_post_blinded_blocks.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,3 +717,56 @@ async fn test_submit_block_v2_ssz_does_not_retry_on_400() -> Result<()> {
717717
assert_eq!(attempts, 1);
718718
Ok(())
719719
}
720+
721+
/// PBS must accept relay `Content-Type: application/octet-stream;
722+
/// charset=binary` on `submit_block` responses. The audit fix for C2 switched
723+
/// `EncodingType::from_str` to parse via the `mediatype` crate; this test
724+
/// exercises the full relay→PBS→BN path to guard against regressions on the
725+
/// v1 submit path.
726+
#[tokio::test]
727+
async fn test_submit_block_tolerates_mime_params_in_content_type() -> Result<()> {
728+
setup_test_env();
729+
let signer = random_secret();
730+
let pubkey = signer.public_key();
731+
let chain = Chain::Holesky;
732+
let pbs_listener = get_free_listener().await;
733+
let relay_listener = get_free_listener().await;
734+
let pbs_port = pbs_listener.local_addr().unwrap().port();
735+
let relay_port = relay_listener.local_addr().unwrap().port();
736+
737+
let mock_relay = generate_mock_relay(relay_port, pubkey)?;
738+
let mut mock_relay_state = MockRelayState::new(chain, signer)
739+
.with_response_content_type("application/octet-stream; charset=binary");
740+
mock_relay_state.supported_content_types = Arc::new(HashSet::from([EncodingType::Ssz]));
741+
let mock_state = Arc::new(mock_relay_state);
742+
tokio::spawn(start_mock_relay_service_with_listener(mock_state.clone(), relay_listener));
743+
744+
let pbs_config = get_pbs_config(pbs_port);
745+
let config = to_pbs_config(chain, pbs_config, vec![mock_relay]);
746+
let state = PbsState::new(config, PathBuf::new());
747+
drop(pbs_listener);
748+
tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state));
749+
750+
tokio::time::sleep(Duration::from_millis(100)).await;
751+
752+
let signed_blinded_block = load_test_signed_blinded_block();
753+
let mock_validator = MockValidator::new(pbs_port)?;
754+
let res = mock_validator
755+
.do_submit_block_v1(
756+
Some(signed_blinded_block.clone()),
757+
vec![EncodingType::Ssz],
758+
EncodingType::Ssz,
759+
ForkName::Electra,
760+
)
761+
.await?;
762+
assert_eq!(res.status(), StatusCode::OK, "PBS should tolerate `; charset=binary` MIME param");
763+
assert_eq!(mock_state.received_submit_block(), 1);
764+
765+
let bytes = res.bytes().await?;
766+
let response_body = PayloadAndBlobs::from_ssz_bytes_by_fork(&bytes, ForkName::Electra).unwrap();
767+
assert_eq!(
768+
response_body.execution_payload.block_hash(),
769+
signed_blinded_block.block_hash().into()
770+
);
771+
Ok(())
772+
}

0 commit comments

Comments
 (0)