From 1d6e8979d23245c44559c6d927dfb0ff6d84aa20 Mon Sep 17 00:00:00 2001 From: Sagar Rana Date: Sat, 18 Jul 2026 19:07:33 +0530 Subject: [PATCH 1/5] reqresp suite passing --- lean_client/networking/src/network/service.rs | 16 +++++++++++----- lean_client/networking/src/req_resp.rs | 4 ++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index a0a188d..f37a4bb 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -41,9 +41,11 @@ use crate::{ discovery::{DiscoveryConfig, DiscoveryService}, enr_ext::EnrExt, gossipsub::{self, config::GossipsubConfig, message::GossipsubMessage, topic::GossipsubKind}, - network::behaviour::{LeanNetworkBehaviour, LeanNetworkBehaviourEvent}, - network::range_sync::{MAX_SYNC_RANGE, RangeSyncState}, - req_resp::{self, LeanRequest, ReqRespMessage}, + network::{ + behaviour::{LeanNetworkBehaviour, LeanNetworkBehaviourEvent}, + range_sync::{MAX_SYNC_RANGE, RangeSyncState}, + }, + req_resp::{self, LeanRequest, RESPONSE_INVALID_REQUEST, ReqRespMessage}, types::{ CanonicalBlocksProvider, ChainMessage, ChainMessageSink, ConnectionState, MAX_BLOCK_CACHE_SIZE, NetworkFinalizedSlot, OutboundP2pRequest, P2pRequestSource, @@ -59,6 +61,7 @@ const MAX_BLOCKS_PER_REQUEST: usize = 10; /// Set comfortably above libp2p's default protocol timeout (10s) so the app-layer /// gives the underlying stream room to complete under host CPU contention. const BLOCKS_BY_ROOT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); +const RESPONSE_CODE_INVALID_REQUEST: u8 = 1; struct PendingBlocksRequest { roots: Vec, @@ -1676,8 +1679,11 @@ where if count == 0 || count > req_resp::MAX_REQUEST_BLOCKS as u64 { info!(peer = %peer, start_slot, count, "Rejecting BlocksByRange: invalid count"); - // Send an empty response — peer will treat as no data. - let response = LeanResponse::BlocksByRange(Vec::new()); + + let response = LeanResponse::Error { + code: RESPONSE_INVALID_REQUEST, + message: "Invalid Block Request Count".to_string(), + }; if let Err(e) = self .swarm .behaviour_mut() diff --git a/lean_client/networking/src/req_resp.rs b/lean_client/networking/src/req_resp.rs index 8e359a7..fb6026c 100644 --- a/lean_client/networking/src/req_resp.rs +++ b/lean_client/networking/src/req_resp.rs @@ -62,6 +62,7 @@ pub enum LeanResponse { Status(Status), BlocksByRoot(Vec), BlocksByRange(Vec), + Error { code: u8, message: String }, Empty, } @@ -297,6 +298,9 @@ impl LeanCodec { } Ok(result) } + LeanResponse::Error { code, message } => { + Self::encode_response_chunk(*code, message.as_bytes()) + } LeanResponse::Empty => Ok(Vec::new()), } } From 104d57a109dd668591e2f1f02eae1a88a45eb90b Mon Sep 17 00:00:00 2001 From: Sagar Rana Date: Sat, 18 Jul 2026 19:10:21 +0530 Subject: [PATCH 2/5] test for error path --- lean_client/networking/src/req_resp.rs | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/lean_client/networking/src/req_resp.rs b/lean_client/networking/src/req_resp.rs index fb6026c..5bcb430 100644 --- a/lean_client/networking/src/req_resp.rs +++ b/lean_client/networking/src/req_resp.rs @@ -734,3 +734,56 @@ pub fn build_blocks_by_root() -> ReqResp { pub fn build_blocks_by_range() -> ReqResp { build(vec![BLOCKS_BY_RANGE_PROTOCOL_V1.to_string()]) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Encoding an error response must produce a single, well-formed ssz-snappy + /// chunk whose response-code byte is the requested error code. This mirrors + /// what the hive `reqresp/blocks_by_range/zero_count` scenario asserts on the + /// wire: `!chunks.is_empty() && chunks[0].0 == RESPONSE_CODE_INVALID_REQUEST`. + #[test] + fn encode_error_response_sets_invalid_request_code() { + let message = "count must be greater than zero"; + let response = LeanResponse::Error { + code: RESPONSE_INVALID_REQUEST, + message: message.to_string(), + }; + + let encoded = LeanCodec::encode_response(&response).expect("error response should encode"); + + // The mock rejects an empty response, so there must be at least the code byte. + assert!(!encoded.is_empty(), "encoded error response must not be empty"); + // First byte is the response code the peer reads. + assert_eq!( + encoded[0], RESPONSE_INVALID_REQUEST, + "first byte must be the INVALID_REQUEST code" + ); + + // The chunk must be parseable back with the same framing the peer uses, + // recovering the code and the original message payload, consuming all bytes. + let (code, payload, consumed) = + LeanCodec::decode_response_chunk(&encoded).expect("error chunk should decode"); + assert_eq!(code, RESPONSE_INVALID_REQUEST); + assert_eq!(payload, message.as_bytes()); + assert_eq!(consumed, encoded.len(), "error response must be a single chunk"); + } + + /// The error code carried in the variant is what ends up on the wire, so a + /// different code round-trips independently of the payload. + #[test] + fn encode_error_response_preserves_code_and_message() { + let response = LeanResponse::Error { + code: RESPONSE_SERVER_ERROR, + message: "boom".to_string(), + }; + + let encoded = LeanCodec::encode_response(&response).expect("error response should encode"); + let (code, payload, _) = + LeanCodec::decode_response_chunk(&encoded).expect("error chunk should decode"); + + assert_eq!(code, RESPONSE_SERVER_ERROR); + assert_eq!(payload, b"boom"); + } +} From 439354bae6877ec7ea468810e5eb391cfd462dff Mon Sep 17 00:00:00 2001 From: Sagar Rana Date: Sat, 18 Jul 2026 20:31:06 +0530 Subject: [PATCH 3/5] remove test --- lean_client/networking/src/req_resp.rs | 55 +------------------------- 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/lean_client/networking/src/req_resp.rs b/lean_client/networking/src/req_resp.rs index 5bcb430..8608ced 100644 --- a/lean_client/networking/src/req_resp.rs +++ b/lean_client/networking/src/req_resp.rs @@ -733,57 +733,4 @@ pub fn build_blocks_by_root() -> ReqResp { /// Build a RequestResponse behavior for BlocksByRange protocol only pub fn build_blocks_by_range() -> ReqResp { build(vec![BLOCKS_BY_RANGE_PROTOCOL_V1.to_string()]) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Encoding an error response must produce a single, well-formed ssz-snappy - /// chunk whose response-code byte is the requested error code. This mirrors - /// what the hive `reqresp/blocks_by_range/zero_count` scenario asserts on the - /// wire: `!chunks.is_empty() && chunks[0].0 == RESPONSE_CODE_INVALID_REQUEST`. - #[test] - fn encode_error_response_sets_invalid_request_code() { - let message = "count must be greater than zero"; - let response = LeanResponse::Error { - code: RESPONSE_INVALID_REQUEST, - message: message.to_string(), - }; - - let encoded = LeanCodec::encode_response(&response).expect("error response should encode"); - - // The mock rejects an empty response, so there must be at least the code byte. - assert!(!encoded.is_empty(), "encoded error response must not be empty"); - // First byte is the response code the peer reads. - assert_eq!( - encoded[0], RESPONSE_INVALID_REQUEST, - "first byte must be the INVALID_REQUEST code" - ); - - // The chunk must be parseable back with the same framing the peer uses, - // recovering the code and the original message payload, consuming all bytes. - let (code, payload, consumed) = - LeanCodec::decode_response_chunk(&encoded).expect("error chunk should decode"); - assert_eq!(code, RESPONSE_INVALID_REQUEST); - assert_eq!(payload, message.as_bytes()); - assert_eq!(consumed, encoded.len(), "error response must be a single chunk"); - } - - /// The error code carried in the variant is what ends up on the wire, so a - /// different code round-trips independently of the payload. - #[test] - fn encode_error_response_preserves_code_and_message() { - let response = LeanResponse::Error { - code: RESPONSE_SERVER_ERROR, - message: "boom".to_string(), - }; - - let encoded = LeanCodec::encode_response(&response).expect("error response should encode"); - let (code, payload, _) = - LeanCodec::decode_response_chunk(&encoded).expect("error chunk should decode"); - - assert_eq!(code, RESPONSE_SERVER_ERROR); - assert_eq!(payload, b"boom"); - } -} +} \ No newline at end of file From cec525db0f2c2907e383e30703ac67380cb2d345 Mon Sep 17 00:00:00 2001 From: Sagar Rana Date: Sat, 18 Jul 2026 20:32:47 +0530 Subject: [PATCH 4/5] remove unused const --- lean_client/networking/src/network/service.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index f37a4bb..662ccae 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -61,7 +61,6 @@ const MAX_BLOCKS_PER_REQUEST: usize = 10; /// Set comfortably above libp2p's default protocol timeout (10s) so the app-layer /// gives the underlying stream room to complete under host CPU contention. const BLOCKS_BY_ROOT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); -const RESPONSE_CODE_INVALID_REQUEST: u8 = 1; struct PendingBlocksRequest { roots: Vec, From 9fc82bdd3feb7fde2a4d8338755ff5b477381acb Mon Sep 17 00:00:00 2001 From: Sagar Rana Date: Sat, 18 Jul 2026 20:36:06 +0530 Subject: [PATCH 5/5] fmt --- lean_client/networking/src/network/service.rs | 1 - lean_client/networking/src/req_resp.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index 662ccae..5b469cf 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -1678,7 +1678,6 @@ where if count == 0 || count > req_resp::MAX_REQUEST_BLOCKS as u64 { info!(peer = %peer, start_slot, count, "Rejecting BlocksByRange: invalid count"); - let response = LeanResponse::Error { code: RESPONSE_INVALID_REQUEST, message: "Invalid Block Request Count".to_string(), diff --git a/lean_client/networking/src/req_resp.rs b/lean_client/networking/src/req_resp.rs index 8608ced..fb6026c 100644 --- a/lean_client/networking/src/req_resp.rs +++ b/lean_client/networking/src/req_resp.rs @@ -733,4 +733,4 @@ pub fn build_blocks_by_root() -> ReqResp { /// Build a RequestResponse behavior for BlocksByRange protocol only pub fn build_blocks_by_range() -> ReqResp { build(vec![BLOCKS_BY_RANGE_PROTOCOL_V1.to_string()]) -} \ No newline at end of file +}