From d8ae35d64259e1833d421dc487fe059240cb14da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:04:49 -0300 Subject: [PATCH 01/25] feat: add BlocksByRootCodec --- crates/net/p2p/src/lib.rs | 89 ++++++++++++-- crates/net/p2p/src/messages/blocks_by_root.rs | 116 ++++++++++++++++++ crates/net/p2p/src/messages/mod.rs | 96 +++++++++++++++ crates/net/p2p/src/messages/status.rs | 91 +------------- 4 files changed, 293 insertions(+), 99 deletions(-) create mode 100644 crates/net/p2p/src/messages/blocks_by_root.rs diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index ecaab66..1f05c8d 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -27,7 +27,11 @@ use crate::{ gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, messages::{ MAX_COMPRESSED_PAYLOAD_SIZE, - status::{STATUS_PROTOCOL_V1, Status}, + blocks_by_root::{ + BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootCodec, BlocksByRootRequest, + BlocksByRootResponse, + }, + status::{STATUS_PROTOCOL_V1, Status, StatusCodec}, }, }; @@ -75,7 +79,7 @@ pub async fn start_p2p( let gossipsub = libp2p::gossipsub::Behaviour::new(MessageAuthenticity::Anonymous, config) .expect("failed to initiate behaviour"); - let req_resp = request_response::Behaviour::new( + let status = request_response::Behaviour::new( vec![( StreamProtocol::new(STATUS_PROTOCOL_V1), request_response::ProtocolSupport::Full, @@ -83,9 +87,18 @@ pub async fn start_p2p( Default::default(), ); + let blocks_by_root = request_response::Behaviour::new( + vec![( + StreamProtocol::new(BLOCKS_BY_ROOT_PROTOCOL_V1), + request_response::ProtocolSupport::Full, + )], + Default::default(), + ); + let behavior = Behaviour { gossipsub, - req_resp, + status, + blocks_by_root, }; // TODO: set peer scoring params @@ -160,7 +173,8 @@ pub async fn start_p2p( #[derive(NetworkBehaviour)] struct Behaviour { gossipsub: libp2p::gossipsub::Behaviour, - req_resp: request_response::Behaviour, + status: request_response::Behaviour, + blocks_by_root: request_response::Behaviour, } /// Event loop for the P2P crate. @@ -188,10 +202,29 @@ async fn event_loop( break; }; match event { - SwarmEvent::Behaviour(BehaviourEvent::ReqResp( + SwarmEvent::Behaviour(BehaviourEvent::Status( message @ request_response::Event::Message { .. }, )) => { - handle_req_resp_message(&mut swarm, message, &store).await; + handle_status_message(&mut swarm, message, &store).await; + } + SwarmEvent::Behaviour(BehaviourEvent::BlocksByRoot( + request_response::Event::Message { peer, message, .. }, + )) => { + match message { + request_response::Message::Request { + request, + channel, + .. + } => { + handle_blocks_by_root_request(&mut swarm, request, channel, peer).await; + } + request_response::Message::Response { + response, + .. + } => { + handle_blocks_by_root_response(response, &mut blockchain, peer).await; + } + } } SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( message @ libp2p::gossipsub::Event::Message { .. }, @@ -210,7 +243,7 @@ async fn event_loop( // Send status request on first connection to this peer let our_status = build_status(&store); info!(%peer_id, %direction, finalized_slot=%our_status.finalized.slot, head_slot=%our_status.head.slot, "Added connection to new peer, sending status request"); - swarm.behaviour_mut().req_resp.send_request(&peer_id, our_status); + swarm.behaviour_mut().status.send_request(&peer_id, our_status); } else { info!(%peer_id, %direction, "Added peer connection"); } @@ -310,7 +343,7 @@ async fn handle_outgoing_gossip( } } -async fn handle_req_resp_message( +async fn handle_status_message( swarm: &mut libp2p::Swarm, event: request_response::Event, store: &Store, @@ -333,7 +366,7 @@ async fn handle_req_resp_message( let our_status = build_status(store); swarm .behaviour_mut() - .req_resp + .status .send_response(channel, our_status) .unwrap(); } @@ -394,6 +427,44 @@ fn connection_direction(endpoint: &libp2p::core::ConnectedPoint) -> &'static str } } +async fn handle_blocks_by_root_request( + swarm: &mut libp2p::Swarm, + request: BlocksByRootRequest, + channel: request_response::ResponseChannel, + peer: PeerId, +) { + let num_roots = request.len(); + info!(%peer, num_roots, "Received BlocksByRoot request"); + + // TODO: Implement signed block storage to serve BlocksByRoot requests + // For now, return empty response + let blocks: Vec<_> = vec![]; + let num_blocks = blocks.len(); + let response = BlocksByRootResponse::new(blocks).expect("within limit"); + + info!(%peer, num_roots, num_blocks, "Sending BlocksByRoot response (no signed blocks available)"); + let _ = swarm + .behaviour_mut() + .blocks_by_root + .send_response(channel, response) + .inspect_err(|_| warn!(%peer, "Failed to send BlocksByRoot response")); +} + +async fn handle_blocks_by_root_response( + response: BlocksByRootResponse, + blockchain: &mut BlockChain, + peer: PeerId, +) { + let num_blocks = response.len(); + info!(%peer, num_blocks, "Received BlocksByRoot response"); + + for signed_block in response.iter() { + let slot = signed_block.message.block.slot; + trace!(%peer, %slot, "Processing block from BlocksByRoot response"); + blockchain.notify_new_block(signed_block.clone()).await; + } +} + /// Build a Status message from the current Store state. fn build_status(store: &Store) -> Status { let finalized = store.latest_finalized(); diff --git a/crates/net/p2p/src/messages/blocks_by_root.rs b/crates/net/p2p/src/messages/blocks_by_root.rs new file mode 100644 index 0000000..7bbadfc --- /dev/null +++ b/crates/net/p2p/src/messages/blocks_by_root.rs @@ -0,0 +1,116 @@ +use std::io; + +use ethlambda_types::{block::SignedBlockWithAttestation, primitives::H256}; +use libp2p::futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use snap::read::FrameEncoder; +use ssz::{Decode, Encode}; +use ssz_types::typenum; +use tracing::trace; + +use crate::messages::{decode_payload, encode_varint}; + +pub const BLOCKS_BY_ROOT_PROTOCOL_V1: &str = "/leanconsensus/req/blocks_by_root/1/ssz_snappy"; + +#[allow(dead_code)] +const MAX_REQUEST_BLOCKS: usize = 1024; +type MaxRequestBlocks = typenum::U1024; + +pub type BlocksByRootRequest = ssz_types::VariableList; +pub type BlocksByRootResponse = + ssz_types::VariableList; + +#[derive(Debug, Clone, Default)] +pub struct BlocksByRootCodec; + +#[async_trait::async_trait] +impl libp2p::request_response::Codec for BlocksByRootCodec { + type Protocol = libp2p::StreamProtocol; + type Request = BlocksByRootRequest; + type Response = BlocksByRootResponse; + + async fn read_request(&mut self, _: &Self::Protocol, io: &mut T) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + let payload = decode_payload(io).await?; + let request = BlocksByRootRequest::from_ssz_bytes(&payload) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")))?; + Ok(request) + } + + async fn read_response( + &mut self, + _: &Self::Protocol, + io: &mut T, + ) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + let mut result = 0_u8; + io.read_exact(std::slice::from_mut(&mut result)).await?; + + // TODO: send errors to event loop? + if result != 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "non-zero result in response", + )); + } + + let payload = decode_payload(io).await?; + let response = BlocksByRootResponse::from_ssz_bytes(&payload) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")))?; + Ok(response) + } + + async fn write_request( + &mut self, + _: &Self::Protocol, + io: &mut T, + req: Self::Request, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + trace!(?req, "Writing BlocksByRoot request"); + + let encoded = req.as_ssz_bytes(); + let mut compressor = FrameEncoder::new(&encoded[..]); + + let mut buf = Vec::new(); + io::Read::read_to_end(&mut compressor, &mut buf)?; + + let mut size_buf = [0; 5]; + let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); + io.write_all(varint_buf).await?; + io.write_all(&buf).await?; + + Ok(()) + } + + async fn write_response( + &mut self, + _: &Self::Protocol, + io: &mut T, + resp: Self::Response, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + // Send result byte + io.write_all(&[0]).await?; + + let encoded = resp.as_ssz_bytes(); + let mut compressor = FrameEncoder::new(&encoded[..]); + + let mut buf = Vec::new(); + io::Read::read_to_end(&mut compressor, &mut buf)?; + + let mut size_buf = [0; 5]; + let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); + io.write_all(varint_buf).await?; + io.write_all(&buf).await?; + + Ok(()) + } +} diff --git a/crates/net/p2p/src/messages/mod.rs b/crates/net/p2p/src/messages/mod.rs index 4591439..693ff2a 100644 --- a/crates/net/p2p/src/messages/mod.rs +++ b/crates/net/p2p/src/messages/mod.rs @@ -1,6 +1,102 @@ +use std::io; + +use libp2p::futures::{AsyncRead, AsyncReadExt}; + +pub mod blocks_by_root; pub mod status; pub const MAX_PAYLOAD_SIZE: usize = 10 * 1024 * 1024; // 10 MB // https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/p2p-interface.md#max_message_size pub const MAX_COMPRESSED_PAYLOAD_SIZE: usize = 32 + MAX_PAYLOAD_SIZE + MAX_PAYLOAD_SIZE / 6 + 1024; // ~12 MB + +/// Decode a varint-prefixed, snappy-compressed SSZ payload from an async reader. +pub async fn decode_payload(io: &mut T) -> io::Result> +where + T: AsyncRead + Unpin + Send, +{ + // TODO: limit bytes received + let mut varint_buf = [0; 5]; + + let read = io + .take(varint_buf.len() as u64) + .read(&mut varint_buf) + .await?; + let (size, rest) = decode_varint(&varint_buf[..read])?; + + if (size as usize) < rest.len() || size as usize > MAX_COMPRESSED_PAYLOAD_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid message size", + )); + } + + let mut message = vec![0; size as usize]; + if rest.is_empty() { + io.read_exact(&mut message).await?; + } else { + message[..rest.len()].copy_from_slice(rest); + io.read_exact(&mut message[rest.len()..]).await?; + } + + let mut decoder = snap::read::FrameDecoder::new(&message[..]); + let mut uncompressed = Vec::new(); + io::Read::read_to_end(&mut decoder, &mut uncompressed)?; + + Ok(uncompressed) +} + +/// Encodes a u32 as a varint into the provided buffer, returning a slice of the buffer +/// containing the encoded bytes. +pub fn encode_varint(mut value: u32, dst: &mut [u8; 5]) -> &[u8] { + for i in 0..5 { + let mut byte = (value & 0x7F) as u8; + value >>= 7; + if value != 0 { + byte |= 0x80; + } + dst[i] = byte; + if value == 0 { + return &dst[..=i]; + } + } + &dst[..] +} + +/// Decode a varint from a byte buffer, returning the value and remaining bytes. +pub fn decode_varint(buf: &[u8]) -> io::Result<(u32, &[u8])> { + let mut result = 0_u32; + let mut read_size = 0; + + for (i, byte) in buf.iter().enumerate() { + let value = (byte & 0x7F) as u32; + result |= value << (7 * i); + if byte & 0x80 == 0 { + read_size = i + 1; + break; + } + } + if read_size == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "message size is bigger than 28 bits", + )); + } + Ok((result, &buf[read_size..])) +} + +#[cfg(test)] +mod tests { + use super::decode_varint; + + #[test] + fn test_decode_varint() { + // Example from https://protobuf.dev/programming-guides/encoding/ + let buf = [0b10010110, 0b00000001]; + let (value, rest) = decode_varint(&buf).unwrap(); + assert_eq!(value, 150); + + let expected: &[u8] = &[]; + assert_eq!(rest, expected); + } +} diff --git a/crates/net/p2p/src/messages/status.rs b/crates/net/p2p/src/messages/status.rs index f30a249..b8e38e3 100644 --- a/crates/net/p2p/src/messages/status.rs +++ b/crates/net/p2p/src/messages/status.rs @@ -7,7 +7,7 @@ use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use tracing::trace; -use crate::messages::MAX_COMPRESSED_PAYLOAD_SIZE; +use crate::messages::{decode_payload, encode_varint}; pub const STATUS_PROTOCOL_V1: &str = "/leanconsensus/req/status/1/ssz_snappy"; @@ -105,41 +105,6 @@ impl libp2p::request_response::Codec for StatusCodec { } } -async fn decode_payload(io: &mut T) -> io::Result> -where - T: AsyncRead + Unpin + Send, -{ - // TODO: limit bytes received - let mut varint_buf = [0; 5]; - - let read = io - .take(varint_buf.len() as u64) - .read(&mut varint_buf) - .await?; - let (size, rest) = decode_varint(&varint_buf[..read])?; - - if (size as usize) < rest.len() || size as usize > MAX_COMPRESSED_PAYLOAD_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid message size", - )); - } - - let mut message = vec![0; size as usize]; - if rest.is_empty() { - io.read_exact(&mut message).await?; - } else { - message[..rest.len()].copy_from_slice(rest); - io.read_exact(&mut message[rest.len()..]).await?; - } - - let mut decoder = snap::read::FrameDecoder::new(&message[..]); - let mut uncompressed = Vec::new(); - io::Read::read_to_end(&mut decoder, &mut uncompressed)?; - - Ok(uncompressed) -} - fn deserialize_payload(payload: Vec) -> io::Result { let status = Status::from_ssz_bytes(&payload) // We turn to string since DecodeError does not implement std::error::Error @@ -147,62 +112,8 @@ fn deserialize_payload(payload: Vec) -> io::Result { Ok(status) } -/// Encodes a u32 as a varint into the provided buffer, returning a slice of the buffer -/// containing the encoded bytes. -fn encode_varint(mut value: u32, dst: &mut [u8; 5]) -> &[u8] { - for i in 0..5 { - let mut byte = (value & 0x7F) as u8; - value >>= 7; - if value != 0 { - byte |= 0x80; - } - dst[i] = byte; - if value == 0 { - return &dst[..=i]; - } - } - &dst[..] -} - -fn decode_varint(buf: &[u8]) -> io::Result<(u32, &[u8])> { - let mut result = 0_u32; - let mut read_size = 0; - - for (i, byte) in buf.iter().enumerate() { - let value = (byte & 0x7F) as u32; - result |= value << (7 * i); - if byte & 0x80 == 0 { - read_size = i + 1; - break; - } - } - if read_size == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "message size is bigger than 28 bits", - )); - } - Ok((result, &buf[read_size..])) -} - #[derive(Debug, Clone, Encode, Decode)] pub struct Status { pub finalized: Checkpoint, pub head: Checkpoint, } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_decode_varint() { - // Example from https://protobuf.dev/programming-guides/encoding/ - let buf = [0b10010110, 0b00000001]; - let (value, rest) = decode_varint(&buf).unwrap(); - assert_eq!(value, 150); - - let expected: &[u8] = &[]; - assert_eq!(rest, expected); - } -} From 12e00eae787a746730d7c1a49c7c84905743c6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:32:14 -0300 Subject: [PATCH 02/25] refactor: unify both Codecs --- crates/net/p2p/src/lib.rs | 282 +++++++++--------- crates/net/p2p/src/messages/blocks_by_root.rs | 104 ------- crates/net/p2p/src/messages/status.rs | 118 -------- crates/net/p2p/src/req_resp.rs | 169 +++++++++++ 4 files changed, 310 insertions(+), 363 deletions(-) create mode 100644 crates/net/p2p/src/req_resp.rs diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 1f05c8d..7b1f00e 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -27,17 +27,16 @@ use crate::{ gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, messages::{ MAX_COMPRESSED_PAYLOAD_SIZE, - blocks_by_root::{ - BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootCodec, BlocksByRootRequest, - BlocksByRootResponse, - }, - status::{STATUS_PROTOCOL_V1, Status, StatusCodec}, + blocks_by_root::{BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse}, + status::STATUS_PROTOCOL_V1, }, + req_resp::{Codec, Request, Response, Status}, }; mod gossipsub; mod messages; pub mod metrics; +mod req_resp; pub use metrics::populate_name_registry; @@ -79,26 +78,23 @@ pub async fn start_p2p( let gossipsub = libp2p::gossipsub::Behaviour::new(MessageAuthenticity::Anonymous, config) .expect("failed to initiate behaviour"); - let status = request_response::Behaviour::new( - vec![( - StreamProtocol::new(STATUS_PROTOCOL_V1), - request_response::ProtocolSupport::Full, - )], - Default::default(), - ); - - let blocks_by_root = request_response::Behaviour::new( - vec![( - StreamProtocol::new(BLOCKS_BY_ROOT_PROTOCOL_V1), - request_response::ProtocolSupport::Full, - )], + let req_resp = request_response::Behaviour::new( + vec![ + ( + StreamProtocol::new(STATUS_PROTOCOL_V1), + request_response::ProtocolSupport::Full, + ), + ( + StreamProtocol::new(BLOCKS_BY_ROOT_PROTOCOL_V1), + request_response::ProtocolSupport::Full, + ), + ], Default::default(), ); let behavior = Behaviour { gossipsub, - status, - blocks_by_root, + req_resp, }; // TODO: set peer scoring params @@ -173,8 +169,7 @@ pub async fn start_p2p( #[derive(NetworkBehaviour)] struct Behaviour { gossipsub: libp2p::gossipsub::Behaviour, - status: request_response::Behaviour, - blocks_by_root: request_response::Behaviour, + req_resp: request_response::Behaviour, } /// Event loop for the P2P crate. @@ -201,98 +196,119 @@ async fn event_loop( let Some(event) = event else { break; }; - match event { - SwarmEvent::Behaviour(BehaviourEvent::Status( - message @ request_response::Event::Message { .. }, - )) => { - handle_status_message(&mut swarm, message, &store).await; - } - SwarmEvent::Behaviour(BehaviourEvent::BlocksByRoot( - request_response::Event::Message { peer, message, .. }, - )) => { - match message { - request_response::Message::Request { - request, - channel, - .. - } => { - handle_blocks_by_root_request(&mut swarm, request, channel, peer).await; - } - request_response::Message::Response { - response, - .. - } => { - handle_blocks_by_root_response(response, &mut blockchain, peer).await; - } + handle_swarm_event(event, &mut swarm, &mut blockchain, &store).await; + } + } + } +} + +async fn handle_swarm_event( + event: SwarmEvent, + swarm: &mut libp2p::Swarm, + blockchain: &mut BlockChain, + store: &Store, +) { + match event { + SwarmEvent::Behaviour(BehaviourEvent::ReqResp( + request_response::Event::Message { peer, message, .. }, + )) => { + match message { + request_response::Message::Request { + request, + channel, + .. + } => { + match request { + Request::Status(status) => { + handle_status_request(swarm, status, channel, peer, store).await; } - } - SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( - message @ libp2p::gossipsub::Event::Message { .. }, - )) => { - gossipsub::handle_gossipsub_message(&mut blockchain, message).await; - } - SwarmEvent::ConnectionEstablished { - peer_id, - endpoint, - num_established, - .. - } => { - let direction = connection_direction(&endpoint); - if num_established.get() == 1 { - metrics::notify_peer_connected(&Some(peer_id), direction, "success"); - // Send status request on first connection to this peer - let our_status = build_status(&store); - info!(%peer_id, %direction, finalized_slot=%our_status.finalized.slot, head_slot=%our_status.head.slot, "Added connection to new peer, sending status request"); - swarm.behaviour_mut().status.send_request(&peer_id, our_status); - } else { - info!(%peer_id, %direction, "Added peer connection"); + Request::BlocksByRoot(request) => { + handle_blocks_by_root_request(swarm, request, channel, peer).await; } } - SwarmEvent::ConnectionClosed { - peer_id, - endpoint, - num_established, - cause, - .. - } => { - let direction = connection_direction(&endpoint); - let reason = match cause { - None => "remote_close", - Some(err) => { - // Categorize disconnection reasons - let err_str = err.to_string().to_lowercase(); - if err_str.contains("timeout") || err_str.contains("timedout") || err_str.contains("keepalive") { - "timeout" - } else if err_str.contains("reset") || err_str.contains("connectionreset") { - "remote_close" - } else { - "error" - } - } - }; - if num_established == 0 { - metrics::notify_peer_disconnected(&Some(peer_id), direction, reason); + } + request_response::Message::Response { response, .. } => { + match response { + Response::Status(status) => { + handle_status_response(status, peer).await; + } + Response::BlocksByRoot(response) => { + handle_blocks_by_root_response(response, blockchain, peer).await; } - info!(%peer_id, %direction, %reason, "Peer disconnected"); - } - SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { - let result = if error.to_string().to_lowercase().contains("timed out") { - "timeout" - } else { - "error" - }; - metrics::notify_peer_connected(&peer_id, "outbound", result); - warn!(?peer_id, %error, "Outgoing connection error"); - } - SwarmEvent::IncomingConnectionError { peer_id, error, .. } => { - metrics::notify_peer_connected(&peer_id, "inbound", "error"); - warn!(%error, "Incoming connection error"); } - _ => { - trace!(?event, "Ignored swarm event"); + } + } + } + SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( + message @ libp2p::gossipsub::Event::Message { .. }, + )) => { + gossipsub::handle_gossipsub_message(blockchain, message).await; + } + SwarmEvent::ConnectionEstablished { + peer_id, + endpoint, + num_established, + .. + } => { + let direction = connection_direction(&endpoint); + if num_established.get() == 1 { + metrics::notify_peer_connected(&Some(peer_id), direction, "success"); + // Send status request on first connection to this peer + let our_status = build_status(store); + info!(%peer_id, %direction, finalized_slot=%our_status.finalized.slot, head_slot=%our_status.head.slot, "Added connection to new peer, sending status request"); + swarm + .behaviour_mut() + .req_resp + .send_request(&peer_id, Request::Status(our_status)); + } else { + info!(%peer_id, %direction, "Added peer connection"); + } + } + SwarmEvent::ConnectionClosed { + peer_id, + endpoint, + num_established, + cause, + .. + } => { + let direction = connection_direction(&endpoint); + let reason = match cause { + None => "remote_close", + Some(err) => { + // Categorize disconnection reasons + let err_str = err.to_string().to_lowercase(); + if err_str.contains("timeout") + || err_str.contains("timedout") + || err_str.contains("keepalive") + { + "timeout" + } else if err_str.contains("reset") || err_str.contains("connectionreset") { + "remote_close" + } else { + "error" } } + }; + if num_established == 0 { + metrics::notify_peer_disconnected(&Some(peer_id), direction, reason); } + info!(%peer_id, %direction, %reason, "Peer disconnected"); + } + SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { + let result = if error.to_string().to_lowercase().contains("timed out") { + "timeout" + } else { + "error" + }; + metrics::notify_peer_connected(&peer_id, "outbound", result); + warn!(?peer_id, %error, "Outgoing connection error"); + } + SwarmEvent::IncomingConnectionError { peer_id, error, .. } => { + metrics::notify_peer_connected(&peer_id, "inbound", "error"); + warn!(%error, "Incoming connection error"); + } + _ => { + trace!(?event, "Ignored swarm event"); } } } @@ -343,40 +359,24 @@ async fn handle_outgoing_gossip( } } -async fn handle_status_message( +async fn handle_status_request( swarm: &mut libp2p::Swarm, - event: request_response::Event, + request: Status, + channel: request_response::ResponseChannel, + peer: PeerId, store: &Store, ) { - let request_response::Event::Message { - peer, - connection_id: _, - message, - } = event - else { - unreachable!("we already matched on event_loop"); - }; - match message { - request_response::Message::Request { - request_id: _, - request, - channel, - } => { - info!(finalized_slot=%request.finalized.slot, head_slot=%request.head.slot, "Received status request from peer {peer}"); - let our_status = build_status(store); - swarm - .behaviour_mut() - .status - .send_response(channel, our_status) - .unwrap(); - } - request_response::Message::Response { - request_id: _, - response, - } => { - info!(finalized_slot=%response.finalized.slot, head_slot=%response.head.slot, "Received status response from peer {peer}"); - } - } + info!(finalized_slot=%request.finalized.slot, head_slot=%request.head.slot, "Received status request from peer {peer}"); + let our_status = build_status(store); + swarm + .behaviour_mut() + .req_resp + .send_response(channel, Response::Status(our_status)) + .unwrap(); +} + +async fn handle_status_response(status: Status, peer: PeerId) { + info!(finalized_slot=%status.finalized.slot, head_slot=%status.head.slot, "Received status response from peer {peer}"); } pub struct Bootnode { @@ -430,13 +430,13 @@ fn connection_direction(endpoint: &libp2p::core::ConnectedPoint) -> &'static str async fn handle_blocks_by_root_request( swarm: &mut libp2p::Swarm, request: BlocksByRootRequest, - channel: request_response::ResponseChannel, + channel: request_response::ResponseChannel, peer: PeerId, ) { let num_roots = request.len(); info!(%peer, num_roots, "Received BlocksByRoot request"); - // TODO: Implement signed block storage to serve BlocksByRoot requests + // TODO: Implement signature storage to serve BlocksByRoot requests // For now, return empty response let blocks: Vec<_> = vec![]; let num_blocks = blocks.len(); @@ -445,8 +445,8 @@ async fn handle_blocks_by_root_request( info!(%peer, num_roots, num_blocks, "Sending BlocksByRoot response (no signed blocks available)"); let _ = swarm .behaviour_mut() - .blocks_by_root - .send_response(channel, response) + .req_resp + .send_response(channel, Response::BlocksByRoot(response)) .inspect_err(|_| warn!(%peer, "Failed to send BlocksByRoot response")); } diff --git a/crates/net/p2p/src/messages/blocks_by_root.rs b/crates/net/p2p/src/messages/blocks_by_root.rs index 7bbadfc..9ecb426 100644 --- a/crates/net/p2p/src/messages/blocks_by_root.rs +++ b/crates/net/p2p/src/messages/blocks_by_root.rs @@ -1,13 +1,5 @@ -use std::io; - use ethlambda_types::{block::SignedBlockWithAttestation, primitives::H256}; -use libp2p::futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use snap::read::FrameEncoder; -use ssz::{Decode, Encode}; use ssz_types::typenum; -use tracing::trace; - -use crate::messages::{decode_payload, encode_varint}; pub const BLOCKS_BY_ROOT_PROTOCOL_V1: &str = "/leanconsensus/req/blocks_by_root/1/ssz_snappy"; @@ -18,99 +10,3 @@ type MaxRequestBlocks = typenum::U1024; pub type BlocksByRootRequest = ssz_types::VariableList; pub type BlocksByRootResponse = ssz_types::VariableList; - -#[derive(Debug, Clone, Default)] -pub struct BlocksByRootCodec; - -#[async_trait::async_trait] -impl libp2p::request_response::Codec for BlocksByRootCodec { - type Protocol = libp2p::StreamProtocol; - type Request = BlocksByRootRequest; - type Response = BlocksByRootResponse; - - async fn read_request(&mut self, _: &Self::Protocol, io: &mut T) -> io::Result - where - T: AsyncRead + Unpin + Send, - { - let payload = decode_payload(io).await?; - let request = BlocksByRootRequest::from_ssz_bytes(&payload) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")))?; - Ok(request) - } - - async fn read_response( - &mut self, - _: &Self::Protocol, - io: &mut T, - ) -> io::Result - where - T: AsyncRead + Unpin + Send, - { - let mut result = 0_u8; - io.read_exact(std::slice::from_mut(&mut result)).await?; - - // TODO: send errors to event loop? - if result != 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "non-zero result in response", - )); - } - - let payload = decode_payload(io).await?; - let response = BlocksByRootResponse::from_ssz_bytes(&payload) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")))?; - Ok(response) - } - - async fn write_request( - &mut self, - _: &Self::Protocol, - io: &mut T, - req: Self::Request, - ) -> io::Result<()> - where - T: AsyncWrite + Unpin + Send, - { - trace!(?req, "Writing BlocksByRoot request"); - - let encoded = req.as_ssz_bytes(); - let mut compressor = FrameEncoder::new(&encoded[..]); - - let mut buf = Vec::new(); - io::Read::read_to_end(&mut compressor, &mut buf)?; - - let mut size_buf = [0; 5]; - let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); - io.write_all(varint_buf).await?; - io.write_all(&buf).await?; - - Ok(()) - } - - async fn write_response( - &mut self, - _: &Self::Protocol, - io: &mut T, - resp: Self::Response, - ) -> io::Result<()> - where - T: AsyncWrite + Unpin + Send, - { - // Send result byte - io.write_all(&[0]).await?; - - let encoded = resp.as_ssz_bytes(); - let mut compressor = FrameEncoder::new(&encoded[..]); - - let mut buf = Vec::new(); - io::Read::read_to_end(&mut compressor, &mut buf)?; - - let mut size_buf = [0; 5]; - let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); - io.write_all(varint_buf).await?; - io.write_all(&buf).await?; - - Ok(()) - } -} diff --git a/crates/net/p2p/src/messages/status.rs b/crates/net/p2p/src/messages/status.rs index b8e38e3..3575e9d 100644 --- a/crates/net/p2p/src/messages/status.rs +++ b/crates/net/p2p/src/messages/status.rs @@ -1,119 +1 @@ -use std::io; - -use ethlambda_types::state::Checkpoint; -use libp2p::futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use snap::read::FrameEncoder; -use ssz::{Decode, Encode}; -use ssz_derive::{Decode, Encode}; -use tracing::trace; - -use crate::messages::{decode_payload, encode_varint}; - pub const STATUS_PROTOCOL_V1: &str = "/leanconsensus/req/status/1/ssz_snappy"; - -#[derive(Debug, Clone, Default)] -pub struct StatusCodec; - -#[async_trait::async_trait] -impl libp2p::request_response::Codec for StatusCodec { - type Protocol = libp2p::StreamProtocol; - type Request = Status; - type Response = Status; - - async fn read_request(&mut self, _: &Self::Protocol, io: &mut T) -> io::Result - where - T: AsyncRead + Unpin + Send, - { - let payload = decode_payload(io).await?; - let status = deserialize_payload(payload)?; - Ok(status) - } - - async fn read_response( - &mut self, - _: &Self::Protocol, - io: &mut T, - ) -> io::Result - where - T: AsyncRead + Unpin + Send, - { - let mut result = 0_u8; - io.read_exact(std::slice::from_mut(&mut result)).await?; - - // TODO: send errors to event loop? - if result != 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "non-zero result in response", - )); - } - - let payload = decode_payload(io).await?; - let status = deserialize_payload(payload)?; - Ok(status) - } - - async fn write_request( - &mut self, - _: &Self::Protocol, - io: &mut T, - req: Self::Request, - ) -> io::Result<()> - where - T: AsyncWrite + Unpin + Send, - { - trace!(?req, "Writing status request"); - - let encoded = req.as_ssz_bytes(); - let mut compressor = FrameEncoder::new(&encoded[..]); - - let mut buf = Vec::new(); - io::Read::read_to_end(&mut compressor, &mut buf)?; - - let mut size_buf = [0; 5]; - let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); - io.write_all(varint_buf).await?; - io.write_all(&buf).await?; - - Ok(()) - } - - async fn write_response( - &mut self, - _: &Self::Protocol, - io: &mut T, - resp: Self::Response, - ) -> io::Result<()> - where - T: AsyncWrite + Unpin + Send, - { - // Send result byte - io.write_all(&[0]).await?; - - let encoded = resp.as_ssz_bytes(); - let mut compressor = FrameEncoder::new(&encoded[..]); - - let mut buf = Vec::new(); - io::Read::read_to_end(&mut compressor, &mut buf)?; - - let mut size_buf = [0; 5]; - let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); - io.write_all(varint_buf).await?; - io.write_all(&buf).await?; - - Ok(()) - } -} - -fn deserialize_payload(payload: Vec) -> io::Result { - let status = Status::from_ssz_bytes(&payload) - // We turn to string since DecodeError does not implement std::error::Error - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")))?; - Ok(status) -} - -#[derive(Debug, Clone, Encode, Decode)] -pub struct Status { - pub finalized: Checkpoint, - pub head: Checkpoint, -} diff --git a/crates/net/p2p/src/req_resp.rs b/crates/net/p2p/src/req_resp.rs new file mode 100644 index 0000000..909fb41 --- /dev/null +++ b/crates/net/p2p/src/req_resp.rs @@ -0,0 +1,169 @@ +use std::io; + +use ethlambda_types::state::Checkpoint; +use libp2p::futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use snap::read::FrameEncoder; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use tracing::trace; + +use crate::messages::{ + blocks_by_root::{BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse}, + decode_payload, encode_varint, + status::STATUS_PROTOCOL_V1, +}; + +#[derive(Debug, Clone)] +pub enum Request { + Status(Status), + BlocksByRoot(BlocksByRootRequest), +} + +#[derive(Debug, Clone)] +pub enum Response { + Status(Status), + BlocksByRoot(BlocksByRootResponse), +} + +#[derive(Debug, Clone, Default)] +pub struct Codec; + +#[async_trait::async_trait] +impl libp2p::request_response::Codec for Codec { + type Protocol = libp2p::StreamProtocol; + type Request = Request; + type Response = Response; + + async fn read_request( + &mut self, + protocol: &Self::Protocol, + io: &mut T, + ) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + let payload = decode_payload(io).await?; + + match protocol.as_ref() { + STATUS_PROTOCOL_V1 => { + let status = Status::from_ssz_bytes(&payload).map_err(|err| { + io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")) + })?; + Ok(Request::Status(status)) + } + BLOCKS_BY_ROOT_PROTOCOL_V1 => { + let request = BlocksByRootRequest::from_ssz_bytes(&payload).map_err(|err| { + io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")) + })?; + Ok(Request::BlocksByRoot(request)) + } + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("unknown protocol: {}", protocol.as_ref()), + )), + } + } + + async fn read_response( + &mut self, + protocol: &Self::Protocol, + io: &mut T, + ) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + let mut result = 0_u8; + io.read_exact(std::slice::from_mut(&mut result)).await?; + + // TODO: send errors to event loop? + if result != 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "non-zero result in response", + )); + } + + let payload = decode_payload(io).await?; + + match protocol.as_ref() { + STATUS_PROTOCOL_V1 => { + let status = Status::from_ssz_bytes(&payload).map_err(|err| { + io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")) + })?; + Ok(Response::Status(status)) + } + BLOCKS_BY_ROOT_PROTOCOL_V1 => { + let response = BlocksByRootResponse::from_ssz_bytes(&payload).map_err(|err| { + io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")) + })?; + Ok(Response::BlocksByRoot(response)) + } + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("unknown protocol: {}", protocol.as_ref()), + )), + } + } + + async fn write_request( + &mut self, + _: &Self::Protocol, + io: &mut T, + req: Self::Request, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + trace!(?req, "Writing request"); + + let encoded = match req { + Request::Status(status) => status.as_ssz_bytes(), + Request::BlocksByRoot(request) => request.as_ssz_bytes(), + }; + + write_payload(io, &encoded).await + } + + async fn write_response( + &mut self, + _: &Self::Protocol, + io: &mut T, + resp: Self::Response, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + // Send result byte + io.write_all(&[0]).await?; + + let encoded = match resp { + Response::Status(status) => status.as_ssz_bytes(), + Response::BlocksByRoot(response) => response.as_ssz_bytes(), + }; + + write_payload(io, &encoded).await + } +} + +async fn write_payload(io: &mut T, encoded: &[u8]) -> io::Result<()> +where + T: AsyncWrite + Unpin, +{ + let mut compressor = FrameEncoder::new(encoded); + + let mut buf = Vec::new(); + io::Read::read_to_end(&mut compressor, &mut buf)?; + + let mut size_buf = [0; 5]; + let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); + io.write_all(varint_buf).await?; + io.write_all(&buf).await?; + + Ok(()) +} + +#[derive(Debug, Clone, Encode, Decode)] +pub struct Status { + pub finalized: Checkpoint, + pub head: Checkpoint, +} From ca4dea6c8bfbb7586cc128dd2febc3accc6c0574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:40:48 -0300 Subject: [PATCH 03/25] refactor: restructure code in gossipsub and req_resp modules --- crates/net/p2p/src/gossipsub/encoding.rs | 32 ++++++++++ .../{gossipsub.rs => gossipsub/handler.rs} | 40 ++---------- crates/net/p2p/src/gossipsub/messages.rs | 4 ++ crates/net/p2p/src/gossipsub/mod.rs | 7 ++ crates/net/p2p/src/lib.rs | 64 ++++++++++--------- .../src/{req_resp.rs => req_resp/codec.rs} | 43 ++----------- crates/net/p2p/src/req_resp/encoding.rs | 23 +++++++ crates/net/p2p/src/req_resp/messages.rs | 22 +++++++ crates/net/p2p/src/req_resp/mod.rs | 6 ++ 9 files changed, 138 insertions(+), 103 deletions(-) create mode 100644 crates/net/p2p/src/gossipsub/encoding.rs rename crates/net/p2p/src/{gossipsub.rs => gossipsub/handler.rs} (58%) create mode 100644 crates/net/p2p/src/gossipsub/messages.rs create mode 100644 crates/net/p2p/src/gossipsub/mod.rs rename crates/net/p2p/src/{req_resp.rs => req_resp/codec.rs} (81%) create mode 100644 crates/net/p2p/src/req_resp/encoding.rs create mode 100644 crates/net/p2p/src/req_resp/messages.rs create mode 100644 crates/net/p2p/src/req_resp/mod.rs diff --git a/crates/net/p2p/src/gossipsub/encoding.rs b/crates/net/p2p/src/gossipsub/encoding.rs new file mode 100644 index 0000000..07d20a4 --- /dev/null +++ b/crates/net/p2p/src/gossipsub/encoding.rs @@ -0,0 +1,32 @@ +/// Decompress data using raw snappy format (for gossipsub messages). +pub fn decompress_message(data: &[u8]) -> snap::Result> { + let uncompressed_size = snap::raw::decompress_len(data)?; + let mut uncompressed_data = vec![0u8; uncompressed_size]; + snap::raw::Decoder::new().decompress(data, &mut uncompressed_data)?; + Ok(uncompressed_data) +} + +/// Compress data using raw snappy format (for gossipsub messages). +pub fn compress_message(data: &[u8]) -> Vec { + let max_compressed_len = snap::raw::max_compress_len(data.len()); + let mut compressed = vec![0u8; max_compressed_len]; + let compressed_len = snap::raw::Encoder::new() + .compress(data, &mut compressed) + .expect("snappy compression should not fail"); + compressed.truncate(compressed_len); + compressed +} + +#[cfg(test)] +mod tests { + use ethlambda_types::block::SignedBlockWithAttestation; + use ssz::Decode; + + #[test] + #[ignore = "Test data uses old BlockSignatures field order (proposer_signature, attestation_signatures). Needs regeneration with correct order (attestation_signatures, proposer_signature)."] + fn test_decode_block() { + // Sample uncompressed block sent by Zeam (commit b153373806aa49f65aadc47c41b68ead4fab7d6e) + let block_bytes = include_bytes!("../../test_data/signed_block_with_attestation.ssz"); + let _block = SignedBlockWithAttestation::from_ssz_bytes(block_bytes).unwrap(); + } +} diff --git a/crates/net/p2p/src/gossipsub.rs b/crates/net/p2p/src/gossipsub/handler.rs similarity index 58% rename from crates/net/p2p/src/gossipsub.rs rename to crates/net/p2p/src/gossipsub/handler.rs index aa2651e..dd29a71 100644 --- a/crates/net/p2p/src/gossipsub.rs +++ b/crates/net/p2p/src/gossipsub/handler.rs @@ -4,10 +4,10 @@ use libp2p::gossipsub::Event; use ssz::Decode; use tracing::{error, info, trace}; -/// Topic kind for block gossip -pub const BLOCK_TOPIC_KIND: &str = "block"; -/// Topic kind for attestation gossip -pub const ATTESTATION_TOPIC_KIND: &str = "attestation"; +use super::{ + encoding::decompress_message, + messages::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, +}; pub async fn handle_gossipsub_message(blockchain: &mut BlockChain, event: Event) { let Event::Message { @@ -57,35 +57,3 @@ pub async fn handle_gossipsub_message(blockchain: &mut BlockChain, event: Event) } } } - -fn decompress_message(data: &[u8]) -> snap::Result> { - let uncompressed_size = snap::raw::decompress_len(data)?; - let mut uncompressed_data = vec![0u8; uncompressed_size]; - snap::raw::Decoder::new().decompress(data, &mut uncompressed_data)?; - Ok(uncompressed_data) -} - -/// Compress data using raw snappy format (for gossipsub messages). -pub fn compress_message(data: &[u8]) -> Vec { - let max_compressed_len = snap::raw::max_compress_len(data.len()); - let mut compressed = vec![0u8; max_compressed_len]; - let compressed_len = snap::raw::Encoder::new() - .compress(data, &mut compressed) - .expect("snappy compression should not fail"); - compressed.truncate(compressed_len); - compressed -} - -#[cfg(test)] -mod tests { - use ethlambda_types::block::SignedBlockWithAttestation; - use ssz::Decode; - - #[test] - #[ignore = "Test data uses old BlockSignatures field order (proposer_signature, attestation_signatures). Needs regeneration with correct order (attestation_signatures, proposer_signature)."] - fn test_decode_block() { - // Sample uncompressed block sent by Zeam (commit b153373806aa49f65aadc47c41b68ead4fab7d6e) - let block_bytes = include_bytes!("../test_data/signed_block_with_attestation.ssz"); - let _block = SignedBlockWithAttestation::from_ssz_bytes(block_bytes).unwrap(); - } -} diff --git a/crates/net/p2p/src/gossipsub/messages.rs b/crates/net/p2p/src/gossipsub/messages.rs new file mode 100644 index 0000000..7df4db1 --- /dev/null +++ b/crates/net/p2p/src/gossipsub/messages.rs @@ -0,0 +1,4 @@ +/// Topic kind for block gossip +pub const BLOCK_TOPIC_KIND: &str = "block"; +/// Topic kind for attestation gossip +pub const ATTESTATION_TOPIC_KIND: &str = "attestation"; diff --git a/crates/net/p2p/src/gossipsub/mod.rs b/crates/net/p2p/src/gossipsub/mod.rs new file mode 100644 index 0000000..49f6bd7 --- /dev/null +++ b/crates/net/p2p/src/gossipsub/mod.rs @@ -0,0 +1,7 @@ +mod encoding; +mod handler; +mod messages; + +pub use encoding::compress_message; +pub use handler::handle_gossipsub_message; +pub use messages::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}; diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 7b1f00e..5a2c551 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -202,6 +202,35 @@ async fn event_loop( } } +async fn handle_req_resp_message( + message: request_response::Message, + peer: PeerId, + swarm: &mut libp2p::Swarm, + blockchain: &mut BlockChain, + store: &Store, +) { + match message { + request_response::Message::Request { + request, channel, .. + } => match request { + Request::Status(status) => { + handle_status_request(swarm, status, channel, peer, store).await; + } + Request::BlocksByRoot(request) => { + handle_blocks_by_root_request(swarm, request, channel, peer).await; + } + }, + request_response::Message::Response { response, .. } => match response { + Response::Status(status) => { + handle_status_response(status, peer).await; + } + Response::BlocksByRoot(response) => { + handle_blocks_by_root_response(response, blockchain, peer).await; + } + }, + } +} + async fn handle_swarm_event( event: SwarmEvent, swarm: &mut libp2p::Swarm, @@ -209,35 +238,12 @@ async fn handle_swarm_event( store: &Store, ) { match event { - SwarmEvent::Behaviour(BehaviourEvent::ReqResp( - request_response::Event::Message { peer, message, .. }, - )) => { - match message { - request_response::Message::Request { - request, - channel, - .. - } => { - match request { - Request::Status(status) => { - handle_status_request(swarm, status, channel, peer, store).await; - } - Request::BlocksByRoot(request) => { - handle_blocks_by_root_request(swarm, request, channel, peer).await; - } - } - } - request_response::Message::Response { response, .. } => { - match response { - Response::Status(status) => { - handle_status_response(status, peer).await; - } - Response::BlocksByRoot(response) => { - handle_blocks_by_root_response(response, blockchain, peer).await; - } - } - } - } + SwarmEvent::Behaviour(BehaviourEvent::ReqResp(request_response::Event::Message { + peer, + message, + .. + })) => { + handle_req_resp_message(message, peer, swarm, blockchain, store).await; } SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( message @ libp2p::gossipsub::Event::Message { .. }, diff --git a/crates/net/p2p/src/req_resp.rs b/crates/net/p2p/src/req_resp/codec.rs similarity index 81% rename from crates/net/p2p/src/req_resp.rs rename to crates/net/p2p/src/req_resp/codec.rs index 909fb41..e4ecf2e 100644 --- a/crates/net/p2p/src/req_resp.rs +++ b/crates/net/p2p/src/req_resp/codec.rs @@ -1,29 +1,19 @@ use std::io; -use ethlambda_types::state::Checkpoint; use libp2p::futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use snap::read::FrameEncoder; use ssz::{Decode, Encode}; -use ssz_derive::{Decode, Encode}; use tracing::trace; use crate::messages::{ blocks_by_root::{BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse}, - decode_payload, encode_varint, + decode_payload, status::STATUS_PROTOCOL_V1, }; -#[derive(Debug, Clone)] -pub enum Request { - Status(Status), - BlocksByRoot(BlocksByRootRequest), -} - -#[derive(Debug, Clone)] -pub enum Response { - Status(Status), - BlocksByRoot(BlocksByRootResponse), -} +use super::{ + encoding::write_payload, + messages::{Request, Response, Status}, +}; #[derive(Debug, Clone, Default)] pub struct Codec; @@ -144,26 +134,3 @@ impl libp2p::request_response::Codec for Codec { write_payload(io, &encoded).await } } - -async fn write_payload(io: &mut T, encoded: &[u8]) -> io::Result<()> -where - T: AsyncWrite + Unpin, -{ - let mut compressor = FrameEncoder::new(encoded); - - let mut buf = Vec::new(); - io::Read::read_to_end(&mut compressor, &mut buf)?; - - let mut size_buf = [0; 5]; - let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); - io.write_all(varint_buf).await?; - io.write_all(&buf).await?; - - Ok(()) -} - -#[derive(Debug, Clone, Encode, Decode)] -pub struct Status { - pub finalized: Checkpoint, - pub head: Checkpoint, -} diff --git a/crates/net/p2p/src/req_resp/encoding.rs b/crates/net/p2p/src/req_resp/encoding.rs new file mode 100644 index 0000000..0f3be8c --- /dev/null +++ b/crates/net/p2p/src/req_resp/encoding.rs @@ -0,0 +1,23 @@ +use std::io; + +use libp2p::futures::{AsyncWrite, AsyncWriteExt}; +use snap::read::FrameEncoder; + +use crate::messages::encode_varint; + +pub async fn write_payload(io: &mut T, encoded: &[u8]) -> io::Result<()> +where + T: AsyncWrite + Unpin, +{ + let mut compressor = FrameEncoder::new(encoded); + + let mut buf = Vec::new(); + io::Read::read_to_end(&mut compressor, &mut buf)?; + + let mut size_buf = [0; 5]; + let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); + io.write_all(varint_buf).await?; + io.write_all(&buf).await?; + + Ok(()) +} diff --git a/crates/net/p2p/src/req_resp/messages.rs b/crates/net/p2p/src/req_resp/messages.rs new file mode 100644 index 0000000..829c57e --- /dev/null +++ b/crates/net/p2p/src/req_resp/messages.rs @@ -0,0 +1,22 @@ +use ethlambda_types::state::Checkpoint; +use ssz_derive::{Decode, Encode}; + +use crate::messages::blocks_by_root::{BlocksByRootRequest, BlocksByRootResponse}; + +#[derive(Debug, Clone)] +pub enum Request { + Status(Status), + BlocksByRoot(BlocksByRootRequest), +} + +#[derive(Debug, Clone)] +pub enum Response { + Status(Status), + BlocksByRoot(BlocksByRootResponse), +} + +#[derive(Debug, Clone, Encode, Decode)] +pub struct Status { + pub finalized: Checkpoint, + pub head: Checkpoint, +} diff --git a/crates/net/p2p/src/req_resp/mod.rs b/crates/net/p2p/src/req_resp/mod.rs new file mode 100644 index 0000000..b596039 --- /dev/null +++ b/crates/net/p2p/src/req_resp/mod.rs @@ -0,0 +1,6 @@ +mod codec; +mod encoding; +mod messages; + +pub use codec::Codec; +pub use messages::{Request, Response, Status}; From 2087ac3772098ed2edc7c30fc064dcab04e2c8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:44:45 -0300 Subject: [PATCH 04/25] refactor: remove the messages module --- crates/net/p2p/src/lib.rs | 7 +- crates/net/p2p/src/messages/blocks_by_root.rs | 12 --- crates/net/p2p/src/messages/mod.rs | 102 ------------------ crates/net/p2p/src/messages/status.rs | 1 - crates/net/p2p/src/req_resp/codec.rs | 13 +-- crates/net/p2p/src/req_resp/encoding.rs | 98 ++++++++++++++++- crates/net/p2p/src/req_resp/messages.rs | 12 ++- crates/net/p2p/src/req_resp/mod.rs | 6 +- 8 files changed, 117 insertions(+), 134 deletions(-) delete mode 100644 crates/net/p2p/src/messages/blocks_by_root.rs delete mode 100644 crates/net/p2p/src/messages/mod.rs delete mode 100644 crates/net/p2p/src/messages/status.rs diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 5a2c551..11b9702 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -25,16 +25,11 @@ use tracing::{info, trace, warn}; use crate::{ gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, - messages::{ - MAX_COMPRESSED_PAYLOAD_SIZE, - blocks_by_root::{BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse}, - status::STATUS_PROTOCOL_V1, + req_resp::{Codec, BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse, MAX_COMPRESSED_PAYLOAD_SIZE, Request, Response, STATUS_PROTOCOL_V1, Status, }, - req_resp::{Codec, Request, Response, Status}, }; mod gossipsub; -mod messages; pub mod metrics; mod req_resp; diff --git a/crates/net/p2p/src/messages/blocks_by_root.rs b/crates/net/p2p/src/messages/blocks_by_root.rs deleted file mode 100644 index 9ecb426..0000000 --- a/crates/net/p2p/src/messages/blocks_by_root.rs +++ /dev/null @@ -1,12 +0,0 @@ -use ethlambda_types::{block::SignedBlockWithAttestation, primitives::H256}; -use ssz_types::typenum; - -pub const BLOCKS_BY_ROOT_PROTOCOL_V1: &str = "/leanconsensus/req/blocks_by_root/1/ssz_snappy"; - -#[allow(dead_code)] -const MAX_REQUEST_BLOCKS: usize = 1024; -type MaxRequestBlocks = typenum::U1024; - -pub type BlocksByRootRequest = ssz_types::VariableList; -pub type BlocksByRootResponse = - ssz_types::VariableList; diff --git a/crates/net/p2p/src/messages/mod.rs b/crates/net/p2p/src/messages/mod.rs deleted file mode 100644 index 693ff2a..0000000 --- a/crates/net/p2p/src/messages/mod.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::io; - -use libp2p::futures::{AsyncRead, AsyncReadExt}; - -pub mod blocks_by_root; -pub mod status; - -pub const MAX_PAYLOAD_SIZE: usize = 10 * 1024 * 1024; // 10 MB - -// https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/p2p-interface.md#max_message_size -pub const MAX_COMPRESSED_PAYLOAD_SIZE: usize = 32 + MAX_PAYLOAD_SIZE + MAX_PAYLOAD_SIZE / 6 + 1024; // ~12 MB - -/// Decode a varint-prefixed, snappy-compressed SSZ payload from an async reader. -pub async fn decode_payload(io: &mut T) -> io::Result> -where - T: AsyncRead + Unpin + Send, -{ - // TODO: limit bytes received - let mut varint_buf = [0; 5]; - - let read = io - .take(varint_buf.len() as u64) - .read(&mut varint_buf) - .await?; - let (size, rest) = decode_varint(&varint_buf[..read])?; - - if (size as usize) < rest.len() || size as usize > MAX_COMPRESSED_PAYLOAD_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid message size", - )); - } - - let mut message = vec![0; size as usize]; - if rest.is_empty() { - io.read_exact(&mut message).await?; - } else { - message[..rest.len()].copy_from_slice(rest); - io.read_exact(&mut message[rest.len()..]).await?; - } - - let mut decoder = snap::read::FrameDecoder::new(&message[..]); - let mut uncompressed = Vec::new(); - io::Read::read_to_end(&mut decoder, &mut uncompressed)?; - - Ok(uncompressed) -} - -/// Encodes a u32 as a varint into the provided buffer, returning a slice of the buffer -/// containing the encoded bytes. -pub fn encode_varint(mut value: u32, dst: &mut [u8; 5]) -> &[u8] { - for i in 0..5 { - let mut byte = (value & 0x7F) as u8; - value >>= 7; - if value != 0 { - byte |= 0x80; - } - dst[i] = byte; - if value == 0 { - return &dst[..=i]; - } - } - &dst[..] -} - -/// Decode a varint from a byte buffer, returning the value and remaining bytes. -pub fn decode_varint(buf: &[u8]) -> io::Result<(u32, &[u8])> { - let mut result = 0_u32; - let mut read_size = 0; - - for (i, byte) in buf.iter().enumerate() { - let value = (byte & 0x7F) as u32; - result |= value << (7 * i); - if byte & 0x80 == 0 { - read_size = i + 1; - break; - } - } - if read_size == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "message size is bigger than 28 bits", - )); - } - Ok((result, &buf[read_size..])) -} - -#[cfg(test)] -mod tests { - use super::decode_varint; - - #[test] - fn test_decode_varint() { - // Example from https://protobuf.dev/programming-guides/encoding/ - let buf = [0b10010110, 0b00000001]; - let (value, rest) = decode_varint(&buf).unwrap(); - assert_eq!(value, 150); - - let expected: &[u8] = &[]; - assert_eq!(rest, expected); - } -} diff --git a/crates/net/p2p/src/messages/status.rs b/crates/net/p2p/src/messages/status.rs deleted file mode 100644 index 3575e9d..0000000 --- a/crates/net/p2p/src/messages/status.rs +++ /dev/null @@ -1 +0,0 @@ -pub const STATUS_PROTOCOL_V1: &str = "/leanconsensus/req/status/1/ssz_snappy"; diff --git a/crates/net/p2p/src/req_resp/codec.rs b/crates/net/p2p/src/req_resp/codec.rs index e4ecf2e..583521d 100644 --- a/crates/net/p2p/src/req_resp/codec.rs +++ b/crates/net/p2p/src/req_resp/codec.rs @@ -4,15 +4,12 @@ use libp2p::futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use ssz::{Decode, Encode}; use tracing::trace; -use crate::messages::{ - blocks_by_root::{BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse}, - decode_payload, - status::STATUS_PROTOCOL_V1, -}; - use super::{ - encoding::write_payload, - messages::{Request, Response, Status}, + encoding::{decode_payload, write_payload}, + messages::{ + BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse, Request, Response, + STATUS_PROTOCOL_V1, Status, + }, }; #[derive(Debug, Clone, Default)] diff --git a/crates/net/p2p/src/req_resp/encoding.rs b/crates/net/p2p/src/req_resp/encoding.rs index 0f3be8c..a0fd207 100644 --- a/crates/net/p2p/src/req_resp/encoding.rs +++ b/crates/net/p2p/src/req_resp/encoding.rs @@ -1,9 +1,48 @@ use std::io; -use libp2p::futures::{AsyncWrite, AsyncWriteExt}; +use libp2p::futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use snap::read::FrameEncoder; -use crate::messages::encode_varint; +pub const MAX_PAYLOAD_SIZE: usize = 10 * 1024 * 1024; // 10 MB + +// https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/p2p-interface.md#max_message_size +pub const MAX_COMPRESSED_PAYLOAD_SIZE: usize = 32 + MAX_PAYLOAD_SIZE + MAX_PAYLOAD_SIZE / 6 + 1024; // ~12 MB + +/// Decode a varint-prefixed, snappy-compressed SSZ payload from an async reader. +pub async fn decode_payload(io: &mut T) -> io::Result> +where + T: AsyncRead + Unpin + Send, +{ + // TODO: limit bytes received + let mut varint_buf = [0; 5]; + + let read = io + .take(varint_buf.len() as u64) + .read(&mut varint_buf) + .await?; + let (size, rest) = decode_varint(&varint_buf[..read])?; + + if (size as usize) < rest.len() || size as usize > MAX_COMPRESSED_PAYLOAD_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid message size", + )); + } + + let mut message = vec![0; size as usize]; + if rest.is_empty() { + io.read_exact(&mut message).await?; + } else { + message[..rest.len()].copy_from_slice(rest); + io.read_exact(&mut message[rest.len()..]).await?; + } + + let mut decoder = snap::read::FrameDecoder::new(&message[..]); + let mut uncompressed = Vec::new(); + io::Read::read_to_end(&mut decoder, &mut uncompressed)?; + + Ok(uncompressed) +} pub async fn write_payload(io: &mut T, encoded: &[u8]) -> io::Result<()> where @@ -21,3 +60,58 @@ where Ok(()) } + +/// Encodes a u32 as a varint into the provided buffer, returning a slice of the buffer +/// containing the encoded bytes. +pub fn encode_varint(mut value: u32, dst: &mut [u8; 5]) -> &[u8] { + for i in 0..5 { + let mut byte = (value & 0x7F) as u8; + value >>= 7; + if value != 0 { + byte |= 0x80; + } + dst[i] = byte; + if value == 0 { + return &dst[..=i]; + } + } + &dst[..] +} + +/// Decode a varint from a byte buffer, returning the value and remaining bytes. +pub fn decode_varint(buf: &[u8]) -> io::Result<(u32, &[u8])> { + let mut result = 0_u32; + let mut read_size = 0; + + for (i, byte) in buf.iter().enumerate() { + let value = (byte & 0x7F) as u32; + result |= value << (7 * i); + if byte & 0x80 == 0 { + read_size = i + 1; + break; + } + } + if read_size == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "message size is bigger than 28 bits", + )); + } + Ok((result, &buf[read_size..])) +} + +#[cfg(test)] +mod tests { + use super::decode_varint; + + #[test] + fn test_decode_varint() { + // Example from https://protobuf.dev/programming-guides/encoding/ + let buf = [0b10010110, 0b00000001]; + let (value, rest) = decode_varint(&buf).unwrap(); + assert_eq!(value, 150); + + let expected: &[u8] = &[]; + assert_eq!(rest, expected); + } +} diff --git a/crates/net/p2p/src/req_resp/messages.rs b/crates/net/p2p/src/req_resp/messages.rs index 829c57e..5ff8483 100644 --- a/crates/net/p2p/src/req_resp/messages.rs +++ b/crates/net/p2p/src/req_resp/messages.rs @@ -1,7 +1,9 @@ -use ethlambda_types::state::Checkpoint; +use ethlambda_types::{block::SignedBlockWithAttestation, primitives::H256, state::Checkpoint}; use ssz_derive::{Decode, Encode}; +use ssz_types::typenum; -use crate::messages::blocks_by_root::{BlocksByRootRequest, BlocksByRootResponse}; +pub const STATUS_PROTOCOL_V1: &str = "/leanconsensus/req/status/1/ssz_snappy"; +pub const BLOCKS_BY_ROOT_PROTOCOL_V1: &str = "/leanconsensus/req/blocks_by_root/1/ssz_snappy"; #[derive(Debug, Clone)] pub enum Request { @@ -20,3 +22,9 @@ pub struct Status { pub finalized: Checkpoint, pub head: Checkpoint, } + +type MaxRequestBlocks = typenum::U1024; + +pub type BlocksByRootRequest = ssz_types::VariableList; +pub type BlocksByRootResponse = + ssz_types::VariableList; diff --git a/crates/net/p2p/src/req_resp/mod.rs b/crates/net/p2p/src/req_resp/mod.rs index b596039..fe6964f 100644 --- a/crates/net/p2p/src/req_resp/mod.rs +++ b/crates/net/p2p/src/req_resp/mod.rs @@ -3,4 +3,8 @@ mod encoding; mod messages; pub use codec::Codec; -pub use messages::{Request, Response, Status}; +pub use encoding::MAX_COMPRESSED_PAYLOAD_SIZE; +pub use messages::{ + BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse, Request, Response, + STATUS_PROTOCOL_V1, Status, +}; From 20877f44b8882c6e8d60a622e9f023a0ec96435b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:04:43 -0300 Subject: [PATCH 05/25] refactor: handle response result explicitly --- crates/net/p2p/src/lib.rs | 26 ++++++++++++----- crates/net/p2p/src/req_resp/codec.rs | 37 ++++++++++++++++++------- crates/net/p2p/src/req_resp/messages.rs | 28 ++++++++++++++++++- crates/net/p2p/src/req_resp/mod.rs | 2 +- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 11b9702..a089b9c 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -215,12 +215,12 @@ async fn handle_req_resp_message( handle_blocks_by_root_request(swarm, request, channel, peer).await; } }, - request_response::Message::Response { response, .. } => match response { - Response::Status(status) => { - handle_status_response(status, peer).await; + request_response::Message::Response { response, .. } => match response.payload() { + req_resp::ResponsePayload::Status(status) => { + handle_status_response(status.clone(), peer).await; } - Response::BlocksByRoot(response) => { - handle_blocks_by_root_response(response, blockchain, peer).await; + req_resp::ResponsePayload::BlocksByRoot(blocks) => { + handle_blocks_by_root_response(blocks.clone(), blockchain, peer).await; } }, } @@ -372,7 +372,13 @@ async fn handle_status_request( swarm .behaviour_mut() .req_resp - .send_response(channel, Response::Status(our_status)) + .send_response( + channel, + Response::new( + req_resp::ResponseResult::Success, + req_resp::ResponsePayload::Status(our_status), + ), + ) .unwrap(); } @@ -447,7 +453,13 @@ async fn handle_blocks_by_root_request( let _ = swarm .behaviour_mut() .req_resp - .send_response(channel, Response::BlocksByRoot(response)) + .send_response( + channel, + Response::new( + req_resp::ResponseResult::Success, + req_resp::ResponsePayload::BlocksByRoot(response), + ), + ) .inspect_err(|_| warn!(%peer, "Failed to send BlocksByRoot response")); } diff --git a/crates/net/p2p/src/req_resp/codec.rs b/crates/net/p2p/src/req_resp/codec.rs index 583521d..596b496 100644 --- a/crates/net/p2p/src/req_resp/codec.rs +++ b/crates/net/p2p/src/req_resp/codec.rs @@ -62,11 +62,22 @@ impl libp2p::request_response::Codec for Codec { let mut result = 0_u8; io.read_exact(std::slice::from_mut(&mut result)).await?; - // TODO: send errors to event loop? - if result != 0 { + let result_code = match result { + 0 => super::messages::ResponseResult::Success, + 1 => super::messages::ResponseResult::InvalidRequest, + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("invalid result code: {}", result), + )); + } + }; + + // TODO: send errors to event loop when result != Success? + if result_code != super::messages::ResponseResult::Success { return Err(io::Error::new( io::ErrorKind::InvalidData, - "non-zero result in response", + "non-success result in response", )); } @@ -77,13 +88,19 @@ impl libp2p::request_response::Codec for Codec { let status = Status::from_ssz_bytes(&payload).map_err(|err| { io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")) })?; - Ok(Response::Status(status)) + Ok(Response::new( + result_code, + super::messages::ResponsePayload::Status(status), + )) } BLOCKS_BY_ROOT_PROTOCOL_V1 => { - let response = BlocksByRootResponse::from_ssz_bytes(&payload).map_err(|err| { + let blocks = BlocksByRootResponse::from_ssz_bytes(&payload).map_err(|err| { io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")) })?; - Ok(Response::BlocksByRoot(response)) + Ok(Response::new( + result_code, + super::messages::ResponsePayload::BlocksByRoot(blocks), + )) } _ => Err(io::Error::new( io::ErrorKind::InvalidData, @@ -121,11 +138,11 @@ impl libp2p::request_response::Codec for Codec { T: AsyncWrite + Unpin + Send, { // Send result byte - io.write_all(&[0]).await?; + io.write_all(&[resp.result() as u8]).await?; - let encoded = match resp { - Response::Status(status) => status.as_ssz_bytes(), - Response::BlocksByRoot(response) => response.as_ssz_bytes(), + let encoded = match resp.payload() { + super::messages::ResponsePayload::Status(status) => status.as_ssz_bytes(), + super::messages::ResponsePayload::BlocksByRoot(response) => response.as_ssz_bytes(), }; write_payload(io, &encoded).await diff --git a/crates/net/p2p/src/req_resp/messages.rs b/crates/net/p2p/src/req_resp/messages.rs index 5ff8483..61e4e6e 100644 --- a/crates/net/p2p/src/req_resp/messages.rs +++ b/crates/net/p2p/src/req_resp/messages.rs @@ -12,7 +12,33 @@ pub enum Request { } #[derive(Debug, Clone)] -pub enum Response { +pub struct Response { + result: ResponseResult, + payload: ResponsePayload, +} + +impl Response { + pub fn new(result: ResponseResult, payload: ResponsePayload) -> Self { + Self { result, payload } + } + + pub fn result(&self) -> ResponseResult { + self.result + } + + pub fn payload(&self) -> &ResponsePayload { + &self.payload + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResponseResult { + Success = 0, + InvalidRequest = 1, +} + +#[derive(Debug, Clone)] +pub enum ResponsePayload { Status(Status), BlocksByRoot(BlocksByRootResponse), } diff --git a/crates/net/p2p/src/req_resp/mod.rs b/crates/net/p2p/src/req_resp/mod.rs index fe6964f..92d56d2 100644 --- a/crates/net/p2p/src/req_resp/mod.rs +++ b/crates/net/p2p/src/req_resp/mod.rs @@ -6,5 +6,5 @@ pub use codec::Codec; pub use encoding::MAX_COMPRESSED_PAYLOAD_SIZE; pub use messages::{ BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse, Request, Response, - STATUS_PROTOCOL_V1, Status, + ResponsePayload, ResponseResult, STATUS_PROTOCOL_V1, Status, }; From 0e7695591d671c979f8a211532627323f35d5e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:37:53 -0300 Subject: [PATCH 06/25] refactor: make Response fields public --- crates/net/p2p/src/lib.rs | 6 +++--- crates/net/p2p/src/req_resp/codec.rs | 4 ++-- crates/net/p2p/src/req_resp/messages.rs | 12 ++---------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index a089b9c..d1ff86c 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -215,12 +215,12 @@ async fn handle_req_resp_message( handle_blocks_by_root_request(swarm, request, channel, peer).await; } }, - request_response::Message::Response { response, .. } => match response.payload() { + request_response::Message::Response { response, .. } => match response.payload { req_resp::ResponsePayload::Status(status) => { - handle_status_response(status.clone(), peer).await; + handle_status_response(status, peer).await; } req_resp::ResponsePayload::BlocksByRoot(blocks) => { - handle_blocks_by_root_response(blocks.clone(), blockchain, peer).await; + handle_blocks_by_root_response(blocks, blockchain, peer).await; } }, } diff --git a/crates/net/p2p/src/req_resp/codec.rs b/crates/net/p2p/src/req_resp/codec.rs index 596b496..1f8b011 100644 --- a/crates/net/p2p/src/req_resp/codec.rs +++ b/crates/net/p2p/src/req_resp/codec.rs @@ -138,9 +138,9 @@ impl libp2p::request_response::Codec for Codec { T: AsyncWrite + Unpin + Send, { // Send result byte - io.write_all(&[resp.result() as u8]).await?; + io.write_all(&[resp.result as u8]).await?; - let encoded = match resp.payload() { + let encoded = match &resp.payload { super::messages::ResponsePayload::Status(status) => status.as_ssz_bytes(), super::messages::ResponsePayload::BlocksByRoot(response) => response.as_ssz_bytes(), }; diff --git a/crates/net/p2p/src/req_resp/messages.rs b/crates/net/p2p/src/req_resp/messages.rs index 61e4e6e..561423f 100644 --- a/crates/net/p2p/src/req_resp/messages.rs +++ b/crates/net/p2p/src/req_resp/messages.rs @@ -13,22 +13,14 @@ pub enum Request { #[derive(Debug, Clone)] pub struct Response { - result: ResponseResult, - payload: ResponsePayload, + pub result: ResponseResult, + pub payload: ResponsePayload, } impl Response { pub fn new(result: ResponseResult, payload: ResponsePayload) -> Self { Self { result, payload } } - - pub fn result(&self) -> ResponseResult { - self.result - } - - pub fn payload(&self) -> &ResponsePayload { - &self.payload - } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] From ab3285f60371864bda886f79c48cacadbb39c49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:49:02 -0300 Subject: [PATCH 07/25] refactor: move req-resp handlers to new module --- crates/net/p2p/src/lib.rs | 119 +---------------------- crates/net/p2p/src/req_resp/codec.rs | 1 + crates/net/p2p/src/req_resp/handlers.rs | 120 ++++++++++++++++++++++++ crates/net/p2p/src/req_resp/mod.rs | 2 + 4 files changed, 126 insertions(+), 116 deletions(-) create mode 100644 crates/net/p2p/src/req_resp/handlers.rs diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index d1ff86c..9023ef6 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -5,7 +5,6 @@ use std::{ use ethlambda_blockchain::{BlockChain, OutboundGossip}; use ethlambda_storage::Store; -use ethlambda_types::state::Checkpoint; use ethrex_common::H264; use ethrex_p2p::types::NodeRecord; use ethrex_rlp::decode::RLPDecode; @@ -25,7 +24,8 @@ use tracing::{info, trace, warn}; use crate::{ gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, - req_resp::{Codec, BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse, MAX_COMPRESSED_PAYLOAD_SIZE, Request, Response, STATUS_PROTOCOL_V1, Status, + req_resp::{Codec, BLOCKS_BY_ROOT_PROTOCOL_V1, MAX_COMPRESSED_PAYLOAD_SIZE, Request, STATUS_PROTOCOL_V1, + build_status, handle_req_resp_message, }, }; @@ -162,7 +162,7 @@ pub async fn start_p2p( /// [libp2p Behaviour](libp2p::swarm::NetworkBehaviour) combining Gossipsub and Request-Response Behaviours #[derive(NetworkBehaviour)] -struct Behaviour { +pub(crate) struct Behaviour { gossipsub: libp2p::gossipsub::Behaviour, req_resp: request_response::Behaviour, } @@ -197,35 +197,6 @@ async fn event_loop( } } -async fn handle_req_resp_message( - message: request_response::Message, - peer: PeerId, - swarm: &mut libp2p::Swarm, - blockchain: &mut BlockChain, - store: &Store, -) { - match message { - request_response::Message::Request { - request, channel, .. - } => match request { - Request::Status(status) => { - handle_status_request(swarm, status, channel, peer, store).await; - } - Request::BlocksByRoot(request) => { - handle_blocks_by_root_request(swarm, request, channel, peer).await; - } - }, - request_response::Message::Response { response, .. } => match response.payload { - req_resp::ResponsePayload::Status(status) => { - handle_status_response(status, peer).await; - } - req_resp::ResponsePayload::BlocksByRoot(blocks) => { - handle_blocks_by_root_response(blocks, blockchain, peer).await; - } - }, - } -} - async fn handle_swarm_event( event: SwarmEvent, swarm: &mut libp2p::Swarm, @@ -360,32 +331,6 @@ async fn handle_outgoing_gossip( } } -async fn handle_status_request( - swarm: &mut libp2p::Swarm, - request: Status, - channel: request_response::ResponseChannel, - peer: PeerId, - store: &Store, -) { - info!(finalized_slot=%request.finalized.slot, head_slot=%request.head.slot, "Received status request from peer {peer}"); - let our_status = build_status(store); - swarm - .behaviour_mut() - .req_resp - .send_response( - channel, - Response::new( - req_resp::ResponseResult::Success, - req_resp::ResponsePayload::Status(our_status), - ), - ) - .unwrap(); -} - -async fn handle_status_response(status: Status, peer: PeerId) { - info!(finalized_slot=%status.finalized.slot, head_slot=%status.head.slot, "Received status response from peer {peer}"); -} - pub struct Bootnode { ip: IpAddr, quic_port: u16, @@ -434,64 +379,6 @@ fn connection_direction(endpoint: &libp2p::core::ConnectedPoint) -> &'static str } } -async fn handle_blocks_by_root_request( - swarm: &mut libp2p::Swarm, - request: BlocksByRootRequest, - channel: request_response::ResponseChannel, - peer: PeerId, -) { - let num_roots = request.len(); - info!(%peer, num_roots, "Received BlocksByRoot request"); - - // TODO: Implement signature storage to serve BlocksByRoot requests - // For now, return empty response - let blocks: Vec<_> = vec![]; - let num_blocks = blocks.len(); - let response = BlocksByRootResponse::new(blocks).expect("within limit"); - - info!(%peer, num_roots, num_blocks, "Sending BlocksByRoot response (no signed blocks available)"); - let _ = swarm - .behaviour_mut() - .req_resp - .send_response( - channel, - Response::new( - req_resp::ResponseResult::Success, - req_resp::ResponsePayload::BlocksByRoot(response), - ), - ) - .inspect_err(|_| warn!(%peer, "Failed to send BlocksByRoot response")); -} - -async fn handle_blocks_by_root_response( - response: BlocksByRootResponse, - blockchain: &mut BlockChain, - peer: PeerId, -) { - let num_blocks = response.len(); - info!(%peer, num_blocks, "Received BlocksByRoot response"); - - for signed_block in response.iter() { - let slot = signed_block.message.block.slot; - trace!(%peer, %slot, "Processing block from BlocksByRoot response"); - blockchain.notify_new_block(signed_block.clone()).await; - } -} - -/// Build a Status message from the current Store state. -fn build_status(store: &Store) -> Status { - let finalized = store.latest_finalized(); - let head_root = store.head(); - let head_slot = store.get_block(&head_root).expect("head block exists").slot; - Status { - finalized, - head: Checkpoint { - root: head_root, - slot: head_slot, - }, - } -} - fn compute_message_id(message: &libp2p::gossipsub::Message) -> libp2p::gossipsub::MessageId { const MESSAGE_DOMAIN_INVALID_SNAPPY: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; const MESSAGE_DOMAIN_VALID_SNAPPY: [u8; 4] = [0x01, 0x00, 0x00, 0x00]; diff --git a/crates/net/p2p/src/req_resp/codec.rs b/crates/net/p2p/src/req_resp/codec.rs index 1f8b011..b91c72e 100644 --- a/crates/net/p2p/src/req_resp/codec.rs +++ b/crates/net/p2p/src/req_resp/codec.rs @@ -62,6 +62,7 @@ impl libp2p::request_response::Codec for Codec { let mut result = 0_u8; io.read_exact(std::slice::from_mut(&mut result)).await?; + // TODO: move matching to ResponseResult impl let result_code = match result { 0 => super::messages::ResponseResult::Success, 1 => super::messages::ResponseResult::InvalidRequest, diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs new file mode 100644 index 0000000..cca48ea --- /dev/null +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -0,0 +1,120 @@ +use ethlambda_blockchain::BlockChain; +use ethlambda_storage::Store; +use libp2p::{PeerId, request_response}; +use tracing::{info, trace, warn}; + +use super::{ + BlocksByRootRequest, BlocksByRootResponse, Request, Response, ResponsePayload, ResponseResult, + Status, +}; +use crate::Behaviour; + +pub async fn handle_req_resp_message( + message: request_response::Message, + peer: PeerId, + swarm: &mut libp2p::Swarm, + blockchain: &mut BlockChain, + store: &Store, +) { + match message { + request_response::Message::Request { + request, channel, .. + } => match request { + Request::Status(status) => { + handle_status_request(swarm, status, channel, peer, store).await; + } + Request::BlocksByRoot(request) => { + handle_blocks_by_root_request(swarm, request, channel, peer).await; + } + }, + request_response::Message::Response { response, .. } => match response.payload { + ResponsePayload::Status(status) => { + handle_status_response(status, peer).await; + } + ResponsePayload::BlocksByRoot(blocks) => { + handle_blocks_by_root_response(blocks, blockchain, peer).await; + } + }, + } +} + +async fn handle_status_request( + swarm: &mut libp2p::Swarm, + request: Status, + channel: request_response::ResponseChannel, + peer: PeerId, + store: &Store, +) { + info!(finalized_slot=%request.finalized.slot, head_slot=%request.head.slot, "Received status request from peer {peer}"); + let our_status = build_status(store); + swarm + .behaviour_mut() + .req_resp + .send_response( + channel, + Response::new(ResponseResult::Success, ResponsePayload::Status(our_status)), + ) + .unwrap(); +} + +async fn handle_status_response(status: Status, peer: PeerId) { + info!(finalized_slot=%status.finalized.slot, head_slot=%status.head.slot, "Received status response from peer {peer}"); +} + +async fn handle_blocks_by_root_request( + swarm: &mut libp2p::Swarm, + request: BlocksByRootRequest, + channel: request_response::ResponseChannel, + peer: PeerId, +) { + let num_roots = request.len(); + info!(%peer, num_roots, "Received BlocksByRoot request"); + + // TODO: Implement signature storage to serve BlocksByRoot requests + // For now, return empty response + let blocks: Vec<_> = vec![]; + let num_blocks = blocks.len(); + let response = BlocksByRootResponse::new(blocks).expect("within limit"); + + info!(%peer, num_roots, num_blocks, "Sending BlocksByRoot response (no signed blocks available)"); + let _ = swarm + .behaviour_mut() + .req_resp + .send_response( + channel, + Response::new( + ResponseResult::Success, + ResponsePayload::BlocksByRoot(response), + ), + ) + .inspect_err(|_| warn!(%peer, "Failed to send BlocksByRoot response")); +} + +async fn handle_blocks_by_root_response( + response: BlocksByRootResponse, + blockchain: &mut BlockChain, + peer: PeerId, +) { + let num_blocks = response.len(); + info!(%peer, num_blocks, "Received BlocksByRoot response"); + + for signed_block in response.iter() { + let slot = signed_block.message.block.slot; + trace!(%peer, %slot, "Processing block from BlocksByRoot response"); + blockchain.notify_new_block(signed_block.clone()).await; + } +} + +/// Build a Status message from the current Store state. +pub fn build_status(store: &Store) -> Status { + let finalized = store.latest_finalized(); + let head_root = store.head(); + let head_slot = store.get_block(&head_root).expect("head block exists").slot; + Status { + finalized, + head: ethlambda_types::state::Checkpoint { + root: head_root, + slot: head_slot, + }, + } +} diff --git a/crates/net/p2p/src/req_resp/mod.rs b/crates/net/p2p/src/req_resp/mod.rs index 92d56d2..acc37e5 100644 --- a/crates/net/p2p/src/req_resp/mod.rs +++ b/crates/net/p2p/src/req_resp/mod.rs @@ -1,9 +1,11 @@ mod codec; mod encoding; +pub mod handlers; mod messages; pub use codec::Codec; pub use encoding::MAX_COMPRESSED_PAYLOAD_SIZE; +pub use handlers::{build_status, handle_req_resp_message}; pub use messages::{ BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse, Request, Response, ResponsePayload, ResponseResult, STATUS_PROTOCOL_V1, Status, From e83b37c406f9e05449ce5d1db93fe9d8117aa126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:54:21 -0300 Subject: [PATCH 08/25] fix: make response include only a single block --- crates/net/p2p/src/lib.rs | 4 +- crates/net/p2p/src/req_resp/codec.rs | 15 +++++--- crates/net/p2p/src/req_resp/handlers.rs | 51 +++++++++---------------- crates/net/p2p/src/req_resp/messages.rs | 7 ++-- crates/net/p2p/src/req_resp/mod.rs | 4 +- 5 files changed, 34 insertions(+), 47 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 9023ef6..723de70 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -25,7 +25,7 @@ use tracing::{info, trace, warn}; use crate::{ gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, req_resp::{Codec, BLOCKS_BY_ROOT_PROTOCOL_V1, MAX_COMPRESSED_PAYLOAD_SIZE, Request, STATUS_PROTOCOL_V1, - build_status, handle_req_resp_message, + build_status, }, }; @@ -209,7 +209,7 @@ async fn handle_swarm_event( message, .. })) => { - handle_req_resp_message(message, peer, swarm, blockchain, store).await; + req_resp::handle_req_resp_message(message, peer, swarm, blockchain, store).await; } SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( message @ libp2p::gossipsub::Event::Message { .. }, diff --git a/crates/net/p2p/src/req_resp/codec.rs b/crates/net/p2p/src/req_resp/codec.rs index b91c72e..e50775f 100644 --- a/crates/net/p2p/src/req_resp/codec.rs +++ b/crates/net/p2p/src/req_resp/codec.rs @@ -7,11 +7,13 @@ use tracing::trace; use super::{ encoding::{decode_payload, write_payload}, messages::{ - BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse, Request, Response, - STATUS_PROTOCOL_V1, Status, + BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, Request, Response, STATUS_PROTOCOL_V1, + Status, }, }; +use ethlambda_types::block::SignedBlockWithAttestation; + #[derive(Debug, Clone, Default)] pub struct Codec; @@ -95,12 +97,13 @@ impl libp2p::request_response::Codec for Codec { )) } BLOCKS_BY_ROOT_PROTOCOL_V1 => { - let blocks = BlocksByRootResponse::from_ssz_bytes(&payload).map_err(|err| { - io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")) - })?; + let block = + SignedBlockWithAttestation::from_ssz_bytes(&payload).map_err(|err| { + io::Error::new(io::ErrorKind::InvalidData, format!("{err:?}")) + })?; Ok(Response::new( result_code, - super::messages::ResponsePayload::BlocksByRoot(blocks), + super::messages::ResponsePayload::BlocksByRoot(block), )) } _ => Err(io::Error::new( diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index cca48ea..ee40532 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -1,12 +1,11 @@ use ethlambda_blockchain::BlockChain; use ethlambda_storage::Store; use libp2p::{PeerId, request_response}; -use tracing::{info, trace, warn}; +use tracing::{info, warn}; -use super::{ - BlocksByRootRequest, BlocksByRootResponse, Request, Response, ResponsePayload, ResponseResult, - Status, -}; +use ethlambda_types::block::SignedBlockWithAttestation; + +use super::{BlocksByRootRequest, Request, Response, ResponsePayload, ResponseResult, Status}; use crate::Behaviour; pub async fn handle_req_resp_message( @@ -62,47 +61,31 @@ async fn handle_status_response(status: Status, peer: PeerId) { } async fn handle_blocks_by_root_request( - swarm: &mut libp2p::Swarm, + _swarm: &mut libp2p::Swarm, request: BlocksByRootRequest, - channel: request_response::ResponseChannel, + _channel: request_response::ResponseChannel, peer: PeerId, ) { let num_roots = request.len(); info!(%peer, num_roots, "Received BlocksByRoot request"); - // TODO: Implement signature storage to serve BlocksByRoot requests - // For now, return empty response - let blocks: Vec<_> = vec![]; - let num_blocks = blocks.len(); - let response = BlocksByRootResponse::new(blocks).expect("within limit"); - - info!(%peer, num_roots, num_blocks, "Sending BlocksByRoot response (no signed blocks available)"); - let _ = swarm - .behaviour_mut() - .req_resp - .send_response( - channel, - Response::new( - ResponseResult::Success, - ResponsePayload::BlocksByRoot(response), - ), - ) - .inspect_err(|_| warn!(%peer, "Failed to send BlocksByRoot response")); + // TODO: Implement signed block storage and send response chunks + // For now, we don't send any response (drop the channel) + // In a full implementation, we would: + // 1. Look up each requested block root + // 2. Send a response chunk for each found block + // 3. Each chunk contains: result byte + encoded SignedBlockWithAttestation + warn!(%peer, num_roots, "BlocksByRoot request received but block storage not implemented"); } async fn handle_blocks_by_root_response( - response: BlocksByRootResponse, + block: SignedBlockWithAttestation, blockchain: &mut BlockChain, peer: PeerId, ) { - let num_blocks = response.len(); - info!(%peer, num_blocks, "Received BlocksByRoot response"); - - for signed_block in response.iter() { - let slot = signed_block.message.block.slot; - trace!(%peer, %slot, "Processing block from BlocksByRoot response"); - blockchain.notify_new_block(signed_block.clone()).await; - } + let slot = block.message.block.slot; + info!(%peer, %slot, "Received BlocksByRoot response chunk"); + blockchain.notify_new_block(block).await; } /// Build a Status message from the current Store state. diff --git a/crates/net/p2p/src/req_resp/messages.rs b/crates/net/p2p/src/req_resp/messages.rs index 561423f..91cc70b 100644 --- a/crates/net/p2p/src/req_resp/messages.rs +++ b/crates/net/p2p/src/req_resp/messages.rs @@ -30,9 +30,12 @@ pub enum ResponseResult { } #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] pub enum ResponsePayload { Status(Status), - BlocksByRoot(BlocksByRootResponse), + + // TODO: here we assume there's a single block per request + BlocksByRoot(SignedBlockWithAttestation), } #[derive(Debug, Clone, Encode, Decode)] @@ -44,5 +47,3 @@ pub struct Status { type MaxRequestBlocks = typenum::U1024; pub type BlocksByRootRequest = ssz_types::VariableList; -pub type BlocksByRootResponse = - ssz_types::VariableList; diff --git a/crates/net/p2p/src/req_resp/mod.rs b/crates/net/p2p/src/req_resp/mod.rs index acc37e5..00e5439 100644 --- a/crates/net/p2p/src/req_resp/mod.rs +++ b/crates/net/p2p/src/req_resp/mod.rs @@ -7,6 +7,6 @@ pub use codec::Codec; pub use encoding::MAX_COMPRESSED_PAYLOAD_SIZE; pub use handlers::{build_status, handle_req_resp_message}; pub use messages::{ - BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, BlocksByRootResponse, Request, Response, - ResponsePayload, ResponseResult, STATUS_PROTOCOL_V1, Status, + BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, Request, Response, ResponsePayload, + ResponseResult, STATUS_PROTOCOL_V1, Status, }; From 6807c0b8681edf43d29f468a7b028035a3de808c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:58:11 -0300 Subject: [PATCH 09/25] refactor: move handle_outgoing_gossip to gossipsub module --- crates/net/p2p/src/gossipsub/handler.rs | 57 +++++++++++++++++++++++-- crates/net/p2p/src/gossipsub/mod.rs | 3 +- crates/net/p2p/src/lib.rs | 49 +-------------------- 3 files changed, 56 insertions(+), 53 deletions(-) diff --git a/crates/net/p2p/src/gossipsub/handler.rs b/crates/net/p2p/src/gossipsub/handler.rs index dd29a71..34c8de3 100644 --- a/crates/net/p2p/src/gossipsub/handler.rs +++ b/crates/net/p2p/src/gossipsub/handler.rs @@ -1,13 +1,14 @@ -use ethlambda_blockchain::BlockChain; +use ethlambda_blockchain::{BlockChain, OutboundGossip}; use ethlambda_types::{attestation::SignedAttestation, block::SignedBlockWithAttestation}; use libp2p::gossipsub::Event; -use ssz::Decode; +use ssz::{Decode, Encode}; use tracing::{error, info, trace}; use super::{ - encoding::decompress_message, + encoding::{compress_message, decompress_message}, messages::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, }; +use crate::Behaviour; pub async fn handle_gossipsub_message(blockchain: &mut BlockChain, event: Event) { let Event::Message { @@ -57,3 +58,53 @@ pub async fn handle_gossipsub_message(blockchain: &mut BlockChain, event: Event) } } } + +pub async fn handle_outgoing_gossip( + swarm: &mut libp2p::Swarm, + message: OutboundGossip, + attestation_topic: &libp2p::gossipsub::IdentTopic, + block_topic: &libp2p::gossipsub::IdentTopic, +) { + match message { + OutboundGossip::PublishAttestation(attestation) => { + let slot = attestation.message.slot; + let validator = attestation.validator_id; + + // Encode to SSZ + let ssz_bytes = attestation.as_ssz_bytes(); + + // Compress with raw snappy + let compressed = compress_message(&ssz_bytes); + + // Publish to gossipsub + let _ = swarm + .behaviour_mut() + .gossipsub + .publish(attestation_topic.clone(), compressed) + .inspect(|_| trace!(%slot, %validator, "Published attestation to gossipsub")) + .inspect_err(|err| { + tracing::warn!(%slot, %validator, %err, "Failed to publish attestation to gossipsub") + }); + } + OutboundGossip::PublishBlock(signed_block) => { + let slot = signed_block.message.block.slot; + let proposer = signed_block.message.block.proposer_index; + + // Encode to SSZ + let ssz_bytes = signed_block.as_ssz_bytes(); + + // Compress with raw snappy + let compressed = compress_message(&ssz_bytes); + + // Publish to gossipsub + let _ = swarm + .behaviour_mut() + .gossipsub + .publish(block_topic.clone(), compressed) + .inspect(|_| info!(%slot, %proposer, "Published block to gossipsub")) + .inspect_err(|err| { + tracing::warn!(%slot, %proposer, %err, "Failed to publish block to gossipsub") + }); + } + } +} diff --git a/crates/net/p2p/src/gossipsub/mod.rs b/crates/net/p2p/src/gossipsub/mod.rs index 49f6bd7..0c4c29c 100644 --- a/crates/net/p2p/src/gossipsub/mod.rs +++ b/crates/net/p2p/src/gossipsub/mod.rs @@ -2,6 +2,5 @@ mod encoding; mod handler; mod messages; -pub use encoding::compress_message; -pub use handler::handle_gossipsub_message; +pub use handler::{handle_gossipsub_message, handle_outgoing_gossip}; pub use messages::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}; diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 723de70..d68df3f 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -18,12 +18,11 @@ use libp2p::{ swarm::{NetworkBehaviour, SwarmEvent}, }; use sha2::Digest; -use ssz::Encode; use tokio::sync::mpsc; use tracing::{info, trace, warn}; use crate::{ - gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, + gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND, handle_outgoing_gossip}, req_resp::{Codec, BLOCKS_BY_ROOT_PROTOCOL_V1, MAX_COMPRESSED_PAYLOAD_SIZE, Request, STATUS_PROTOCOL_V1, build_status, }, @@ -285,52 +284,6 @@ async fn handle_swarm_event( } } -async fn handle_outgoing_gossip( - swarm: &mut libp2p::Swarm, - message: OutboundGossip, - attestation_topic: &libp2p::gossipsub::IdentTopic, - block_topic: &libp2p::gossipsub::IdentTopic, -) { - match message { - OutboundGossip::PublishAttestation(attestation) => { - let slot = attestation.message.slot; - let validator = attestation.validator_id; - - // Encode to SSZ - let ssz_bytes = attestation.as_ssz_bytes(); - - // Compress with raw snappy - let compressed = gossipsub::compress_message(&ssz_bytes); - - // Publish to gossipsub - let _ = swarm - .behaviour_mut() - .gossipsub - .publish(attestation_topic.clone(), compressed) - .inspect(|_| trace!(%slot, %validator, "Published attestation to gossipsub")) - .inspect_err(|err| tracing::warn!(%slot, %validator, %err, "Failed to publish attestation to gossipsub")); - } - OutboundGossip::PublishBlock(signed_block) => { - let slot = signed_block.message.block.slot; - let proposer = signed_block.message.block.proposer_index; - - // Encode to SSZ - let ssz_bytes = signed_block.as_ssz_bytes(); - - // Compress with raw snappy - let compressed = gossipsub::compress_message(&ssz_bytes); - - // Publish to gossipsub - let _ = swarm - .behaviour_mut() - .gossipsub - .publish(block_topic.clone(), compressed) - .inspect(|_| info!(%slot, %proposer, "Published block to gossipsub")) - .inspect_err(|err| tracing::warn!(%slot, %proposer, %err, "Failed to publish block to gossipsub")); - } - } -} - pub struct Bootnode { ip: IpAddr, quic_port: u16, From f2da96e51562a82872be43c0c966e29cfd315cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:04:56 -0300 Subject: [PATCH 10/25] refactor: add new FetchBlock P2PMessage --- crates/blockchain/src/lib.rs | 14 +++-- crates/net/p2p/src/gossipsub/handler.rs | 82 ++++++++++++------------- crates/net/p2p/src/gossipsub/mod.rs | 2 +- crates/net/p2p/src/lib.rs | 29 +++++++-- 4 files changed, 75 insertions(+), 52 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 9f35821..91823e1 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -22,13 +22,15 @@ pub mod key_manager; pub mod metrics; pub mod store; -/// Messages sent from the blockchain to the P2P layer for publishing. +/// Messages sent from the blockchain to the P2P layer. #[derive(Clone, Debug)] -pub enum OutboundGossip { +pub enum P2PMessage { /// Publish an attestation to the gossip network. PublishAttestation(SignedAttestation), /// Publish a block to the gossip network. PublishBlock(SignedBlockWithAttestation), + /// Fetch a block by its root hash. + FetchBlock(ethlambda_types::primitives::H256), } pub struct BlockChain { @@ -41,7 +43,7 @@ pub const SECONDS_PER_SLOT: u64 = 4; impl BlockChain { pub fn spawn( store: Store, - p2p_tx: mpsc::UnboundedSender, + p2p_tx: mpsc::UnboundedSender, validator_keys: HashMap, ) -> BlockChain { let genesis_time = store.config().genesis_time; @@ -84,7 +86,7 @@ impl BlockChain { struct BlockChainServer { store: Store, - p2p_tx: mpsc::UnboundedSender, + p2p_tx: mpsc::UnboundedSender, key_manager: key_manager::KeyManager, } @@ -173,7 +175,7 @@ impl BlockChainServer { // Publish to gossip network let Ok(_) = self .p2p_tx - .send(OutboundGossip::PublishAttestation(signed_attestation)) + .send(P2PMessage::PublishAttestation(signed_attestation)) .inspect_err( |err| error!(%slot, %validator_id, %err, "Failed to publish attestation"), ) @@ -244,7 +246,7 @@ impl BlockChainServer { // Publish to gossip network let Ok(()) = self .p2p_tx - .send(OutboundGossip::PublishBlock(signed_block)) + .send(P2PMessage::PublishBlock(signed_block)) .inspect_err(|err| error!(%slot, %validator_id, %err, "Failed to publish block")) else { return; diff --git a/crates/net/p2p/src/gossipsub/handler.rs b/crates/net/p2p/src/gossipsub/handler.rs index 34c8de3..53b7988 100644 --- a/crates/net/p2p/src/gossipsub/handler.rs +++ b/crates/net/p2p/src/gossipsub/handler.rs @@ -1,4 +1,4 @@ -use ethlambda_blockchain::{BlockChain, OutboundGossip}; +use ethlambda_blockchain::BlockChain; use ethlambda_types::{attestation::SignedAttestation, block::SignedBlockWithAttestation}; use libp2p::gossipsub::Event; use ssz::{Decode, Encode}; @@ -59,52 +59,52 @@ pub async fn handle_gossipsub_message(blockchain: &mut BlockChain, event: Event) } } -pub async fn handle_outgoing_gossip( +pub async fn publish_attestation( swarm: &mut libp2p::Swarm, - message: OutboundGossip, - attestation_topic: &libp2p::gossipsub::IdentTopic, - block_topic: &libp2p::gossipsub::IdentTopic, + attestation: SignedAttestation, + topic: &libp2p::gossipsub::IdentTopic, ) { - match message { - OutboundGossip::PublishAttestation(attestation) => { - let slot = attestation.message.slot; - let validator = attestation.validator_id; + let slot = attestation.message.slot; + let validator = attestation.validator_id; - // Encode to SSZ - let ssz_bytes = attestation.as_ssz_bytes(); + // Encode to SSZ + let ssz_bytes = attestation.as_ssz_bytes(); - // Compress with raw snappy - let compressed = compress_message(&ssz_bytes); + // Compress with raw snappy + let compressed = compress_message(&ssz_bytes); - // Publish to gossipsub - let _ = swarm - .behaviour_mut() - .gossipsub - .publish(attestation_topic.clone(), compressed) - .inspect(|_| trace!(%slot, %validator, "Published attestation to gossipsub")) - .inspect_err(|err| { - tracing::warn!(%slot, %validator, %err, "Failed to publish attestation to gossipsub") - }); - } - OutboundGossip::PublishBlock(signed_block) => { - let slot = signed_block.message.block.slot; - let proposer = signed_block.message.block.proposer_index; + // Publish to gossipsub + let _ = swarm + .behaviour_mut() + .gossipsub + .publish(topic.clone(), compressed) + .inspect(|_| trace!(%slot, %validator, "Published attestation to gossipsub")) + .inspect_err(|err| { + tracing::warn!(%slot, %validator, %err, "Failed to publish attestation to gossipsub") + }); +} + +pub async fn publish_block( + swarm: &mut libp2p::Swarm, + signed_block: SignedBlockWithAttestation, + topic: &libp2p::gossipsub::IdentTopic, +) { + let slot = signed_block.message.block.slot; + let proposer = signed_block.message.block.proposer_index; - // Encode to SSZ - let ssz_bytes = signed_block.as_ssz_bytes(); + // Encode to SSZ + let ssz_bytes = signed_block.as_ssz_bytes(); - // Compress with raw snappy - let compressed = compress_message(&ssz_bytes); + // Compress with raw snappy + let compressed = compress_message(&ssz_bytes); - // Publish to gossipsub - let _ = swarm - .behaviour_mut() - .gossipsub - .publish(block_topic.clone(), compressed) - .inspect(|_| info!(%slot, %proposer, "Published block to gossipsub")) - .inspect_err(|err| { - tracing::warn!(%slot, %proposer, %err, "Failed to publish block to gossipsub") - }); - } - } + // Publish to gossipsub + let _ = swarm + .behaviour_mut() + .gossipsub + .publish(topic.clone(), compressed) + .inspect(|_| info!(%slot, %proposer, "Published block to gossipsub")) + .inspect_err( + |err| tracing::warn!(%slot, %proposer, %err, "Failed to publish block to gossipsub"), + ); } diff --git a/crates/net/p2p/src/gossipsub/mod.rs b/crates/net/p2p/src/gossipsub/mod.rs index 0c4c29c..a66855e 100644 --- a/crates/net/p2p/src/gossipsub/mod.rs +++ b/crates/net/p2p/src/gossipsub/mod.rs @@ -2,5 +2,5 @@ mod encoding; mod handler; mod messages; -pub use handler::{handle_gossipsub_message, handle_outgoing_gossip}; +pub use handler::{handle_gossipsub_message, publish_attestation, publish_block}; pub use messages::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}; diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index d68df3f..507c0db 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -3,7 +3,7 @@ use std::{ time::Duration, }; -use ethlambda_blockchain::{BlockChain, OutboundGossip}; +use ethlambda_blockchain::{BlockChain, P2PMessage}; use ethlambda_storage::Store; use ethrex_common::H264; use ethrex_p2p::types::NodeRecord; @@ -22,7 +22,7 @@ use tokio::sync::mpsc; use tracing::{info, trace, warn}; use crate::{ - gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND, handle_outgoing_gossip}, + gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND, publish_attestation, publish_block}, req_resp::{Codec, BLOCKS_BY_ROOT_PROTOCOL_V1, MAX_COMPRESSED_PAYLOAD_SIZE, Request, STATUS_PROTOCOL_V1, build_status, }, @@ -39,7 +39,7 @@ pub async fn start_p2p( bootnodes: Vec, listening_socket: SocketAddr, blockchain: BlockChain, - p2p_rx: mpsc::UnboundedReceiver, + p2p_rx: mpsc::UnboundedReceiver, store: Store, ) { let config = libp2p::gossipsub::ConfigBuilder::default() @@ -171,7 +171,7 @@ pub(crate) struct Behaviour { async fn event_loop( mut swarm: libp2p::Swarm, mut blockchain: BlockChain, - mut p2p_rx: mpsc::UnboundedReceiver, + mut p2p_rx: mpsc::UnboundedReceiver, attestation_topic: libp2p::gossipsub::IdentTopic, block_topic: libp2p::gossipsub::IdentTopic, store: Store, @@ -284,6 +284,27 @@ async fn handle_swarm_event( } } +async fn handle_outgoing_gossip( + swarm: &mut libp2p::Swarm, + message: ethlambda_blockchain::P2PMessage, + attestation_topic: &libp2p::gossipsub::IdentTopic, + block_topic: &libp2p::gossipsub::IdentTopic, +) { + match message { + ethlambda_blockchain::P2PMessage::PublishAttestation(attestation) => { + publish_attestation(swarm, attestation, attestation_topic).await; + } + ethlambda_blockchain::P2PMessage::PublishBlock(signed_block) => { + publish_block(swarm, signed_block, block_topic).await; + } + ethlambda_blockchain::P2PMessage::FetchBlock(_root) => { + // TODO: Implement BlocksByRoot request + // Need to send a BlocksByRoot request to peers + warn!("FetchBlock message received but not yet implemented"); + } + } +} + pub struct Bootnode { ip: IpAddr, quic_port: u16, From 0b41d04fe02a914c692f04fa1d288652d6fa7112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:05:59 -0300 Subject: [PATCH 11/25] refactor: rename to handle_p2p_message --- crates/net/p2p/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 507c0db..4c22d52 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -184,7 +184,7 @@ async fn event_loop( let Some(message) = message else { break; }; - handle_outgoing_gossip(&mut swarm, message, &attestation_topic, &block_topic).await; + handle_p2p_message(&mut swarm, message, &attestation_topic, &block_topic).await; } event = swarm.next() => { let Some(event) = event else { @@ -284,20 +284,20 @@ async fn handle_swarm_event( } } -async fn handle_outgoing_gossip( +async fn handle_p2p_message( swarm: &mut libp2p::Swarm, - message: ethlambda_blockchain::P2PMessage, + message: P2PMessage, attestation_topic: &libp2p::gossipsub::IdentTopic, block_topic: &libp2p::gossipsub::IdentTopic, ) { match message { - ethlambda_blockchain::P2PMessage::PublishAttestation(attestation) => { + P2PMessage::PublishAttestation(attestation) => { publish_attestation(swarm, attestation, attestation_topic).await; } - ethlambda_blockchain::P2PMessage::PublishBlock(signed_block) => { + P2PMessage::PublishBlock(signed_block) => { publish_block(swarm, signed_block, block_topic).await; } - ethlambda_blockchain::P2PMessage::FetchBlock(_root) => { + P2PMessage::FetchBlock(_root) => { // TODO: Implement BlocksByRoot request // Need to send a BlocksByRoot request to peers warn!("FetchBlock message received but not yet implemented"); From 4009104a429952558e26dd3027b9649731747fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:59:06 -0300 Subject: [PATCH 12/25] feat: request missing blocks --- Cargo.lock | 1 + crates/blockchain/src/lib.rs | 76 ++++++++++++++++++++++++++++++++++-- crates/net/p2p/Cargo.toml | 2 + crates/net/p2p/src/lib.rs | 56 ++++++++++++++++++++++---- 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a25374..65ffe4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2024,6 +2024,7 @@ dependencies = [ "ethrex-rlp", "hex", "libp2p", + "rand 0.8.5", "sha2", "snap", "ssz_types", diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 91823e1..dc5bf86 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::time::{Duration, SystemTime}; use ethlambda_state_transition::is_proposer; @@ -14,7 +14,7 @@ use spawned_concurrency::tasks::{ CallResponse, CastResponse, GenServer, GenServerHandle, send_after, }; use tokio::sync::mpsc; -use tracing::{error, info, warn}; +use tracing::{error, info, trace, warn}; use crate::store::StoreError; @@ -52,6 +52,8 @@ impl BlockChain { store, p2p_tx, key_manager, + pending_blocks: HashMap::new(), + in_flight_requests: HashSet::new(), } .start(); let time_until_genesis = (SystemTime::UNIX_EPOCH + Duration::from_secs(genesis_time)) @@ -88,6 +90,12 @@ struct BlockChainServer { store: Store, p2p_tx: mpsc::UnboundedSender, key_manager: key_manager::KeyManager, + + // Pending blocks waiting for their parent + pending_blocks: HashMap>, + + // Track in-flight block requests (prevent duplicates) + in_flight_requests: HashSet, } impl BlockChainServer { @@ -270,11 +278,71 @@ impl BlockChainServer { fn on_block(&mut self, signed_block: SignedBlockWithAttestation) { let slot = signed_block.message.block.slot; - if let Err(err) = self.process_block(signed_block) { - warn!(%slot, %err, "Failed to process block"); + let block_root = signed_block.message.block.tree_hash_root(); + + match self.process_block(signed_block.clone()) { + Ok(_) => { + info!(%slot, "Block processed successfully"); + + // Check if any pending blocks can now be processed + self.process_pending_children(block_root); + } + Err(StoreError::MissingParentState { parent_root, .. }) => { + info!(%slot, %parent_root, %block_root, "Block parent missing, storing as pending"); + + // Store block for later processing + self.pending_blocks + .entry(parent_root) + .or_default() + .push(signed_block); + + // Request missing parent from network + self.request_missing_block(parent_root); + } + Err(err) => { + warn!(%slot, %err, "Failed to process block"); + } } } + fn request_missing_block(&mut self, block_root: ethlambda_types::primitives::H256) { + // Check if already requested (avoid duplicate requests) + if self.in_flight_requests.contains(&block_root) { + trace!(%block_root, "Block already requested, skipping duplicate"); + return; + } + + // Mark as requested + self.in_flight_requests.insert(block_root); + + // Send request to P2P layer + if let Err(err) = self.p2p_tx.send(P2PMessage::FetchBlock(block_root)) { + error!(%block_root, %err, "Failed to send FetchBlock message to P2P"); + self.in_flight_requests.remove(&block_root); + } else { + info!(%block_root, "Requested missing block from network"); + } + } + + fn process_pending_children(&mut self, parent_root: ethlambda_types::primitives::H256) { + // Remove and process all blocks that were waiting for this parent + if let Some(children) = self.pending_blocks.remove(&parent_root) { + info!(%parent_root, num_children=%children.len(), + "Processing pending blocks after parent arrival"); + + for child_block in children { + let slot = child_block.message.block.slot; + trace!(%parent_root, %slot, "Processing pending child block"); + + // Process recursively - might unblock more descendants + self.on_block(child_block); + } + } + + // Also clear the in-flight request for this block + self.in_flight_requests.remove(&parent_root); + } + fn on_gossip_attestation(&mut self, attestation: SignedAttestation) { if let Err(err) = store::on_gossip_attestation(&mut self.store, attestation) { warn!(%err, "Failed to process gossiped attestation"); diff --git a/crates/net/p2p/Cargo.toml b/crates/net/p2p/Cargo.toml index 6ed7e3a..3b7c2ad 100644 --- a/crates/net/p2p/Cargo.toml +++ b/crates/net/p2p/Cargo.toml @@ -24,6 +24,8 @@ snap = "1.1" tokio.workspace = true tracing.workspace = true +rand = "0.8" + # Required for NodeEnr parsing ethrex-p2p = { git = "https://github.com/lambdaclass/ethrex", rev = "1af63a4de7c93eb7413b9b003df1be82e1484c69" } ethrex-rlp = { git = "https://github.com/lambdaclass/ethrex", rev = "1af63a4de7c93eb7413b9b003df1be82e1484c69" } diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 4c22d52..e4f237c 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashSet, net::{IpAddr, SocketAddr}, time::Duration, }; @@ -19,7 +20,7 @@ use libp2p::{ }; use sha2::Digest; use tokio::sync::mpsc; -use tracing::{info, trace, warn}; +use tracing::{error, info, trace, warn}; use crate::{ gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND, publish_attestation, publish_block}, @@ -176,6 +177,9 @@ async fn event_loop( block_topic: libp2p::gossipsub::IdentTopic, store: Store, ) { + // Track connected peers for block requests + let mut connected_peers: HashSet = HashSet::new(); + loop { tokio::select! { biased; @@ -184,13 +188,13 @@ async fn event_loop( let Some(message) = message else { break; }; - handle_p2p_message(&mut swarm, message, &attestation_topic, &block_topic).await; + handle_p2p_message(&mut swarm, message, &attestation_topic, &block_topic, &connected_peers).await; } event = swarm.next() => { let Some(event) = event else { break; }; - handle_swarm_event(event, &mut swarm, &mut blockchain, &store).await; + handle_swarm_event(event, &mut swarm, &mut blockchain, &store, &mut connected_peers).await; } } } @@ -201,6 +205,7 @@ async fn handle_swarm_event( swarm: &mut libp2p::Swarm, blockchain: &mut BlockChain, store: &Store, + connected_peers: &mut HashSet, ) { match event { SwarmEvent::Behaviour(BehaviourEvent::ReqResp(request_response::Event::Message { @@ -223,6 +228,7 @@ async fn handle_swarm_event( } => { let direction = connection_direction(&endpoint); if num_established.get() == 1 { + connected_peers.insert(peer_id); metrics::notify_peer_connected(&Some(peer_id), direction, "success"); // Send status request on first connection to this peer let our_status = build_status(store); @@ -261,6 +267,7 @@ async fn handle_swarm_event( } }; if num_established == 0 { + connected_peers.remove(&peer_id); metrics::notify_peer_disconnected(&Some(peer_id), direction, reason); } info!(%peer_id, %direction, %reason, "Peer disconnected"); @@ -289,6 +296,7 @@ async fn handle_p2p_message( message: P2PMessage, attestation_topic: &libp2p::gossipsub::IdentTopic, block_topic: &libp2p::gossipsub::IdentTopic, + connected_peers: &HashSet, ) { match message { P2PMessage::PublishAttestation(attestation) => { @@ -297,12 +305,46 @@ async fn handle_p2p_message( P2PMessage::PublishBlock(signed_block) => { publish_block(swarm, signed_block, block_topic).await; } - P2PMessage::FetchBlock(_root) => { - // TODO: Implement BlocksByRoot request - // Need to send a BlocksByRoot request to peers - warn!("FetchBlock message received but not yet implemented"); + P2PMessage::FetchBlock(root) => { + fetch_block_from_peer(swarm, root, connected_peers).await; + } + } +} + +async fn fetch_block_from_peer( + swarm: &mut libp2p::Swarm, + root: ethlambda_types::primitives::H256, + connected_peers: &HashSet, +) { + use rand::seq::SliceRandom; + + if connected_peers.is_empty() { + warn!(%root, "Cannot fetch block: no connected peers"); + return; + } + + // Select random peer + let peers: Vec<_> = connected_peers.iter().copied().collect(); + let peer = match peers.choose(&mut rand::thread_rng()) { + Some(&p) => p, + None => { + warn!(%root, "Failed to select random peer"); + return; } + }; + + // Create BlocksByRoot request with single root + let mut request = req_resp::BlocksByRootRequest::empty(); + if let Err(err) = request.push(root) { + error!(%root, ?err, "Failed to create BlocksByRoot request"); + return; } + + info!(%peer, %root, "Sending BlocksByRoot request for missing block"); + swarm + .behaviour_mut() + .req_resp + .send_request(&peer, Request::BlocksByRoot(request)); } pub struct Bootnode { From 2558c5273401c41f244608239b0068ef0806f122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:05:04 -0300 Subject: [PATCH 13/25] refactor: move block fetching helper to req_resp::handlers --- crates/net/p2p/src/lib.rs | 40 ++----------------------- crates/net/p2p/src/req_resp/handlers.rs | 40 ++++++++++++++++++++++++- crates/net/p2p/src/req_resp/mod.rs | 2 +- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index e4f237c..6fee97e 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -20,12 +20,12 @@ use libp2p::{ }; use sha2::Digest; use tokio::sync::mpsc; -use tracing::{error, info, trace, warn}; +use tracing::{info, trace, warn}; use crate::{ gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND, publish_attestation, publish_block}, req_resp::{Codec, BLOCKS_BY_ROOT_PROTOCOL_V1, MAX_COMPRESSED_PAYLOAD_SIZE, Request, STATUS_PROTOCOL_V1, - build_status, + build_status, fetch_block_from_peer, }, }; @@ -311,42 +311,6 @@ async fn handle_p2p_message( } } -async fn fetch_block_from_peer( - swarm: &mut libp2p::Swarm, - root: ethlambda_types::primitives::H256, - connected_peers: &HashSet, -) { - use rand::seq::SliceRandom; - - if connected_peers.is_empty() { - warn!(%root, "Cannot fetch block: no connected peers"); - return; - } - - // Select random peer - let peers: Vec<_> = connected_peers.iter().copied().collect(); - let peer = match peers.choose(&mut rand::thread_rng()) { - Some(&p) => p, - None => { - warn!(%root, "Failed to select random peer"); - return; - } - }; - - // Create BlocksByRoot request with single root - let mut request = req_resp::BlocksByRootRequest::empty(); - if let Err(err) = request.push(root) { - error!(%root, ?err, "Failed to create BlocksByRoot request"); - return; - } - - info!(%peer, %root, "Sending BlocksByRoot request for missing block"); - swarm - .behaviour_mut() - .req_resp - .send_request(&peer, Request::BlocksByRoot(request)); -} - pub struct Bootnode { ip: IpAddr, quic_port: u16, diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index ee40532..9bd7f5f 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -1,7 +1,10 @@ +use std::collections::HashSet; + use ethlambda_blockchain::BlockChain; use ethlambda_storage::Store; use libp2p::{PeerId, request_response}; -use tracing::{info, warn}; +use rand::seq::SliceRandom; +use tracing::{error, info, warn}; use ethlambda_types::block::SignedBlockWithAttestation; @@ -101,3 +104,38 @@ pub fn build_status(store: &Store) -> Status { }, } } + +/// Fetch a missing block from a random connected peer. +pub async fn fetch_block_from_peer( + swarm: &mut libp2p::Swarm, + root: ethlambda_types::primitives::H256, + connected_peers: &HashSet, +) { + if connected_peers.is_empty() { + warn!(%root, "Cannot fetch block: no connected peers"); + return; + } + + // Select random peer + let peers: Vec<_> = connected_peers.iter().copied().collect(); + let peer = match peers.choose(&mut rand::thread_rng()) { + Some(&p) => p, + None => { + warn!(%root, "Failed to select random peer"); + return; + } + }; + + // Create BlocksByRoot request with single root + let mut request = BlocksByRootRequest::empty(); + if let Err(err) = request.push(root) { + error!(%root, ?err, "Failed to create BlocksByRoot request"); + return; + } + + info!(%peer, %root, "Sending BlocksByRoot request for missing block"); + swarm + .behaviour_mut() + .req_resp + .send_request(&peer, Request::BlocksByRoot(request)); +} diff --git a/crates/net/p2p/src/req_resp/mod.rs b/crates/net/p2p/src/req_resp/mod.rs index 00e5439..654cba2 100644 --- a/crates/net/p2p/src/req_resp/mod.rs +++ b/crates/net/p2p/src/req_resp/mod.rs @@ -5,7 +5,7 @@ mod messages; pub use codec::Codec; pub use encoding::MAX_COMPRESSED_PAYLOAD_SIZE; -pub use handlers::{build_status, handle_req_resp_message}; +pub use handlers::{build_status, fetch_block_from_peer, handle_req_resp_message}; pub use messages::{ BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, Request, Response, ResponsePayload, ResponseResult, STATUS_PROTOCOL_V1, Status, From f0b30d55ad255e74179099293ad085b299dce1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:29:05 -0300 Subject: [PATCH 14/25] refactor: store event loop state in struct --- crates/net/p2p/src/gossipsub/handler.rs | 34 +++++------ crates/net/p2p/src/lib.rs | 75 +++++++++++-------------- crates/net/p2p/src/req_resp/handlers.rs | 39 ++++++------- 3 files changed, 65 insertions(+), 83 deletions(-) diff --git a/crates/net/p2p/src/gossipsub/handler.rs b/crates/net/p2p/src/gossipsub/handler.rs index 53b7988..f9caedf 100644 --- a/crates/net/p2p/src/gossipsub/handler.rs +++ b/crates/net/p2p/src/gossipsub/handler.rs @@ -1,4 +1,3 @@ -use ethlambda_blockchain::BlockChain; use ethlambda_types::{attestation::SignedAttestation, block::SignedBlockWithAttestation}; use libp2p::gossipsub::Event; use ssz::{Decode, Encode}; @@ -8,9 +7,9 @@ use super::{ encoding::{compress_message, decompress_message}, messages::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND}, }; -use crate::Behaviour; +use crate::P2PServer; -pub async fn handle_gossipsub_message(blockchain: &mut BlockChain, event: Event) { +pub async fn handle_gossipsub_message(server: &mut P2PServer, event: Event) { let Event::Message { propagation_source: _, message_id: _, @@ -34,7 +33,7 @@ pub async fn handle_gossipsub_message(blockchain: &mut BlockChain, event: Event) }; let slot = signed_block.message.block.slot; info!(%slot, "Received new block from gossipsub, sending for processing"); - blockchain.notify_new_block(signed_block).await; + server.blockchain.notify_new_block(signed_block).await; } Some(ATTESTATION_TOPIC_KIND) => { let Ok(uncompressed_data) = decompress_message(&message.data) @@ -51,7 +50,10 @@ pub async fn handle_gossipsub_message(blockchain: &mut BlockChain, event: Event) let slot = signed_attestation.message.slot; let validator = signed_attestation.validator_id; info!(%slot, %validator, "Received new attestation from gossipsub, sending for processing"); - blockchain.notify_new_attestation(signed_attestation).await; + server + .blockchain + .notify_new_attestation(signed_attestation) + .await; } _ => { trace!("Received message on unknown topic: {}", message.topic); @@ -59,11 +61,7 @@ pub async fn handle_gossipsub_message(blockchain: &mut BlockChain, event: Event) } } -pub async fn publish_attestation( - swarm: &mut libp2p::Swarm, - attestation: SignedAttestation, - topic: &libp2p::gossipsub::IdentTopic, -) { +pub async fn publish_attestation(server: &mut P2PServer, attestation: SignedAttestation) { let slot = attestation.message.slot; let validator = attestation.validator_id; @@ -74,21 +72,18 @@ pub async fn publish_attestation( let compressed = compress_message(&ssz_bytes); // Publish to gossipsub - let _ = swarm + let _ = server + .swarm .behaviour_mut() .gossipsub - .publish(topic.clone(), compressed) + .publish(server.attestation_topic.clone(), compressed) .inspect(|_| trace!(%slot, %validator, "Published attestation to gossipsub")) .inspect_err(|err| { tracing::warn!(%slot, %validator, %err, "Failed to publish attestation to gossipsub") }); } -pub async fn publish_block( - swarm: &mut libp2p::Swarm, - signed_block: SignedBlockWithAttestation, - topic: &libp2p::gossipsub::IdentTopic, -) { +pub async fn publish_block(server: &mut P2PServer, signed_block: SignedBlockWithAttestation) { let slot = signed_block.message.block.slot; let proposer = signed_block.message.block.proposer_index; @@ -99,10 +94,11 @@ pub async fn publish_block( let compressed = compress_message(&ssz_bytes); // Publish to gossipsub - let _ = swarm + let _ = server + .swarm .behaviour_mut() .gossipsub - .publish(topic.clone(), compressed) + .publish(server.block_topic.clone(), compressed) .inspect(|_| info!(%slot, %proposer, "Published block to gossipsub")) .inspect_err( |err| tracing::warn!(%slot, %proposer, %err, "Failed to publish block to gossipsub"), diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 6fee97e..c8d5f28 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -149,15 +149,17 @@ pub async fn start_p2p( info!("P2P node started on {listening_socket}"); - event_loop( + let server = P2PServer { swarm, blockchain, + store, p2p_rx, attestation_topic, block_topic, - store, - ) - .await; + connected_peers: HashSet::new(), + }; + + event_loop(server).await; } /// [libp2p Behaviour](libp2p::swarm::NetworkBehaviour) combining Gossipsub and Request-Response Behaviours @@ -167,58 +169,52 @@ pub(crate) struct Behaviour { req_resp: request_response::Behaviour, } +pub(crate) struct P2PServer { + pub(crate) swarm: libp2p::Swarm, + pub(crate) blockchain: BlockChain, + pub(crate) store: Store, + pub(crate) p2p_rx: mpsc::UnboundedReceiver, + pub(crate) attestation_topic: libp2p::gossipsub::IdentTopic, + pub(crate) block_topic: libp2p::gossipsub::IdentTopic, + pub(crate) connected_peers: HashSet, +} + /// Event loop for the P2P crate. /// Processes swarm events, incoming requests, responses, gossip, and outgoing messages from blockchain. -async fn event_loop( - mut swarm: libp2p::Swarm, - mut blockchain: BlockChain, - mut p2p_rx: mpsc::UnboundedReceiver, - attestation_topic: libp2p::gossipsub::IdentTopic, - block_topic: libp2p::gossipsub::IdentTopic, - store: Store, -) { - // Track connected peers for block requests - let mut connected_peers: HashSet = HashSet::new(); - +async fn event_loop(mut server: P2PServer) { loop { tokio::select! { biased; - message = p2p_rx.recv() => { + message = server.p2p_rx.recv() => { let Some(message) = message else { break; }; - handle_p2p_message(&mut swarm, message, &attestation_topic, &block_topic, &connected_peers).await; + handle_p2p_message(&mut server, message).await; } - event = swarm.next() => { + event = server.swarm.next() => { let Some(event) = event else { break; }; - handle_swarm_event(event, &mut swarm, &mut blockchain, &store, &mut connected_peers).await; + handle_swarm_event(&mut server, event).await; } } } } -async fn handle_swarm_event( - event: SwarmEvent, - swarm: &mut libp2p::Swarm, - blockchain: &mut BlockChain, - store: &Store, - connected_peers: &mut HashSet, -) { +async fn handle_swarm_event(server: &mut P2PServer, event: SwarmEvent) { match event { SwarmEvent::Behaviour(BehaviourEvent::ReqResp(request_response::Event::Message { peer, message, .. })) => { - req_resp::handle_req_resp_message(message, peer, swarm, blockchain, store).await; + req_resp::handle_req_resp_message(server, message, peer).await; } SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( message @ libp2p::gossipsub::Event::Message { .. }, )) => { - gossipsub::handle_gossipsub_message(blockchain, message).await; + gossipsub::handle_gossipsub_message(server, message).await; } SwarmEvent::ConnectionEstablished { peer_id, @@ -228,12 +224,13 @@ async fn handle_swarm_event( } => { let direction = connection_direction(&endpoint); if num_established.get() == 1 { - connected_peers.insert(peer_id); + server.connected_peers.insert(peer_id); metrics::notify_peer_connected(&Some(peer_id), direction, "success"); // Send status request on first connection to this peer - let our_status = build_status(store); + let our_status = build_status(&server.store); info!(%peer_id, %direction, finalized_slot=%our_status.finalized.slot, head_slot=%our_status.head.slot, "Added connection to new peer, sending status request"); - swarm + server + .swarm .behaviour_mut() .req_resp .send_request(&peer_id, Request::Status(our_status)); @@ -267,7 +264,7 @@ async fn handle_swarm_event( } }; if num_established == 0 { - connected_peers.remove(&peer_id); + server.connected_peers.remove(&peer_id); metrics::notify_peer_disconnected(&Some(peer_id), direction, reason); } info!(%peer_id, %direction, %reason, "Peer disconnected"); @@ -291,22 +288,16 @@ async fn handle_swarm_event( } } -async fn handle_p2p_message( - swarm: &mut libp2p::Swarm, - message: P2PMessage, - attestation_topic: &libp2p::gossipsub::IdentTopic, - block_topic: &libp2p::gossipsub::IdentTopic, - connected_peers: &HashSet, -) { +async fn handle_p2p_message(server: &mut P2PServer, message: P2PMessage) { match message { P2PMessage::PublishAttestation(attestation) => { - publish_attestation(swarm, attestation, attestation_topic).await; + publish_attestation(server, attestation).await; } P2PMessage::PublishBlock(signed_block) => { - publish_block(swarm, signed_block, block_topic).await; + publish_block(server, signed_block).await; } P2PMessage::FetchBlock(root) => { - fetch_block_from_peer(swarm, root, connected_peers).await; + fetch_block_from_peer(server, root).await; } } } diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index 9bd7f5f..f66fe21 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -1,6 +1,3 @@ -use std::collections::HashSet; - -use ethlambda_blockchain::BlockChain; use ethlambda_storage::Store; use libp2p::{PeerId, request_response}; use rand::seq::SliceRandom; @@ -9,24 +6,22 @@ use tracing::{error, info, warn}; use ethlambda_types::block::SignedBlockWithAttestation; use super::{BlocksByRootRequest, Request, Response, ResponsePayload, ResponseResult, Status}; -use crate::Behaviour; +use crate::P2PServer; pub async fn handle_req_resp_message( + server: &mut P2PServer, message: request_response::Message, peer: PeerId, - swarm: &mut libp2p::Swarm, - blockchain: &mut BlockChain, - store: &Store, ) { match message { request_response::Message::Request { request, channel, .. } => match request { Request::Status(status) => { - handle_status_request(swarm, status, channel, peer, store).await; + handle_status_request(server, status, channel, peer).await; } Request::BlocksByRoot(request) => { - handle_blocks_by_root_request(swarm, request, channel, peer).await; + handle_blocks_by_root_request(server, request, channel, peer).await; } }, request_response::Message::Response { response, .. } => match response.payload { @@ -34,22 +29,22 @@ pub async fn handle_req_resp_message( handle_status_response(status, peer).await; } ResponsePayload::BlocksByRoot(blocks) => { - handle_blocks_by_root_response(blocks, blockchain, peer).await; + handle_blocks_by_root_response(server, blocks, peer).await; } }, } } async fn handle_status_request( - swarm: &mut libp2p::Swarm, + server: &mut P2PServer, request: Status, channel: request_response::ResponseChannel, peer: PeerId, - store: &Store, ) { info!(finalized_slot=%request.finalized.slot, head_slot=%request.head.slot, "Received status request from peer {peer}"); - let our_status = build_status(store); - swarm + let our_status = build_status(&server.store); + server + .swarm .behaviour_mut() .req_resp .send_response( @@ -64,7 +59,7 @@ async fn handle_status_response(status: Status, peer: PeerId) { } async fn handle_blocks_by_root_request( - _swarm: &mut libp2p::Swarm, + _server: &mut P2PServer, request: BlocksByRootRequest, _channel: request_response::ResponseChannel, peer: PeerId, @@ -82,13 +77,13 @@ async fn handle_blocks_by_root_request( } async fn handle_blocks_by_root_response( + server: &mut P2PServer, block: SignedBlockWithAttestation, - blockchain: &mut BlockChain, peer: PeerId, ) { let slot = block.message.block.slot; info!(%peer, %slot, "Received BlocksByRoot response chunk"); - blockchain.notify_new_block(block).await; + server.blockchain.notify_new_block(block).await; } /// Build a Status message from the current Store state. @@ -107,17 +102,16 @@ pub fn build_status(store: &Store) -> Status { /// Fetch a missing block from a random connected peer. pub async fn fetch_block_from_peer( - swarm: &mut libp2p::Swarm, + server: &mut P2PServer, root: ethlambda_types::primitives::H256, - connected_peers: &HashSet, ) { - if connected_peers.is_empty() { + if server.connected_peers.is_empty() { warn!(%root, "Cannot fetch block: no connected peers"); return; } // Select random peer - let peers: Vec<_> = connected_peers.iter().copied().collect(); + let peers: Vec<_> = server.connected_peers.iter().copied().collect(); let peer = match peers.choose(&mut rand::thread_rng()) { Some(&p) => p, None => { @@ -134,7 +128,8 @@ pub async fn fetch_block_from_peer( } info!(%peer, %root, "Sending BlocksByRoot request for missing block"); - swarm + server + .swarm .behaviour_mut() .req_resp .send_request(&peer, Request::BlocksByRoot(request)); From be407dfaf363bc3fd128444b313298efb2d68fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:51:09 -0300 Subject: [PATCH 15/25] feat: log req-resp failures --- crates/net/p2p/src/lib.rs | 8 +--- crates/net/p2p/src/req_resp/handlers.rs | 64 +++++++++++++++++-------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index c8d5f28..b7ba30e 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -204,12 +204,8 @@ async fn event_loop(mut server: P2PServer) { async fn handle_swarm_event(server: &mut P2PServer, event: SwarmEvent) { match event { - SwarmEvent::Behaviour(BehaviourEvent::ReqResp(request_response::Event::Message { - peer, - message, - .. - })) => { - req_resp::handle_req_resp_message(server, message, peer).await; + SwarmEvent::Behaviour(BehaviourEvent::ReqResp(req_resp_event)) => { + req_resp::handle_req_resp_message(server, req_resp_event).await; } SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( message @ libp2p::gossipsub::Event::Message { .. }, diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index f66fe21..54eeef8 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -1,7 +1,7 @@ use ethlambda_storage::Store; use libp2p::{PeerId, request_response}; use rand::seq::SliceRandom; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; use ethlambda_types::block::SignedBlockWithAttestation; @@ -10,28 +10,50 @@ use crate::P2PServer; pub async fn handle_req_resp_message( server: &mut P2PServer, - message: request_response::Message, - peer: PeerId, + event: request_response::Event, ) { - match message { - request_response::Message::Request { - request, channel, .. - } => match request { - Request::Status(status) => { - handle_status_request(server, status, channel, peer).await; - } - Request::BlocksByRoot(request) => { - handle_blocks_by_root_request(server, request, channel, peer).await; - } - }, - request_response::Message::Response { response, .. } => match response.payload { - ResponsePayload::Status(status) => { - handle_status_response(status, peer).await; - } - ResponsePayload::BlocksByRoot(blocks) => { - handle_blocks_by_root_response(server, blocks, peer).await; - } + match event { + request_response::Event::Message { peer, message, .. } => match message { + request_response::Message::Request { + request, channel, .. + } => match request { + Request::Status(status) => { + handle_status_request(server, status, channel, peer).await; + } + Request::BlocksByRoot(request) => { + handle_blocks_by_root_request(server, request, channel, peer).await; + } + }, + request_response::Message::Response { response, .. } => match response.payload { + ResponsePayload::Status(status) => { + handle_status_response(status, peer).await; + } + ResponsePayload::BlocksByRoot(blocks) => { + handle_blocks_by_root_response(server, blocks, peer).await; + } + }, }, + request_response::Event::OutboundFailure { + peer, + request_id, + error, + .. + } => { + warn!(%peer, ?request_id, %error, "Outbound request failed"); + } + request_response::Event::InboundFailure { + peer, + request_id, + error, + .. + } => { + warn!(%peer, ?request_id, %error, "Inbound request failed"); + } + request_response::Event::ResponseSent { + peer, request_id, .. + } => { + debug!(%peer, ?request_id, "Response sent successfully"); + } } } From 27263e599d485e0a6d4ae56ddb7eec01303d972d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:13:45 -0300 Subject: [PATCH 16/25] fix: treat encoded-length as uncompressed size --- crates/net/p2p/src/req_resp/encoding.rs | 36 ++++++++++++++----------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/crates/net/p2p/src/req_resp/encoding.rs b/crates/net/p2p/src/req_resp/encoding.rs index a0fd207..b45d447 100644 --- a/crates/net/p2p/src/req_resp/encoding.rs +++ b/crates/net/p2p/src/req_resp/encoding.rs @@ -13,33 +13,36 @@ pub async fn decode_payload(io: &mut T) -> io::Result> where T: AsyncRead + Unpin + Send, { - // TODO: limit bytes received - let mut varint_buf = [0; 5]; - + let mut buf = vec![]; let read = io - .take(varint_buf.len() as u64) - .read(&mut varint_buf) + .take(MAX_COMPRESSED_PAYLOAD_SIZE as u64) + .read_to_end(&mut buf) .await?; - let (size, rest) = decode_varint(&varint_buf[..read])?; - if (size as usize) < rest.len() || size as usize > MAX_COMPRESSED_PAYLOAD_SIZE { + if read < 2 { return Err(io::Error::new( io::ErrorKind::InvalidData, - "invalid message size", + "message too short", )); } + let (size, rest) = decode_varint(&buf)?; - let mut message = vec![0; size as usize]; - if rest.is_empty() { - io.read_exact(&mut message).await?; - } else { - message[..rest.len()].copy_from_slice(rest); - io.read_exact(&mut message[rest.len()..]).await?; + if size as usize > MAX_PAYLOAD_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "message size exceeds maximum allowed", + )); } - let mut decoder = snap::read::FrameDecoder::new(&message[..]); + let mut decoder = snap::read::FrameDecoder::new(rest); let mut uncompressed = Vec::new(); io::Read::read_to_end(&mut decoder, &mut uncompressed)?; + if uncompressed.len() != size as usize { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "uncompressed size does not match received size", + )); + } Ok(uncompressed) } @@ -48,13 +51,14 @@ pub async fn write_payload(io: &mut T, encoded: &[u8]) -> io::Result<()> where T: AsyncWrite + Unpin, { + let uncompressed_size = encoded.len(); let mut compressor = FrameEncoder::new(encoded); let mut buf = Vec::new(); io::Read::read_to_end(&mut compressor, &mut buf)?; let mut size_buf = [0; 5]; - let varint_buf = encode_varint(buf.len() as u32, &mut size_buf); + let varint_buf = encode_varint(uncompressed_size as u32, &mut size_buf); io.write_all(varint_buf).await?; io.write_all(&buf).await?; From b4ffabdbaf662ff8c78ade263a862ceed5ccedf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:33:30 -0300 Subject: [PATCH 17/25] fix: split req-resp behaviour libp2p::request_response expects protocols to be equivalent between them, and negotiates with the peer which one to use. Since we specified the status first in the list of protocols, it seems to default to that one when we send a request. --- crates/net/p2p/src/lib.rs | 37 +++++++++++++++---------- crates/net/p2p/src/req_resp/handlers.rs | 4 +-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index b7ba30e..74134b4 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -73,23 +73,26 @@ pub async fn start_p2p( let gossipsub = libp2p::gossipsub::Behaviour::new(MessageAuthenticity::Anonymous, config) .expect("failed to initiate behaviour"); - let req_resp = request_response::Behaviour::new( - vec![ - ( - StreamProtocol::new(STATUS_PROTOCOL_V1), - request_response::ProtocolSupport::Full, - ), - ( - StreamProtocol::new(BLOCKS_BY_ROOT_PROTOCOL_V1), - request_response::ProtocolSupport::Full, - ), - ], + let status_req_resp = request_response::Behaviour::new( + vec![( + StreamProtocol::new(STATUS_PROTOCOL_V1), + request_response::ProtocolSupport::Full, + )], + Default::default(), + ); + + let blocks_by_root_req_resp = request_response::Behaviour::new( + vec![( + StreamProtocol::new(BLOCKS_BY_ROOT_PROTOCOL_V1), + request_response::ProtocolSupport::Full, + )], Default::default(), ); let behavior = Behaviour { gossipsub, - req_resp, + status_req_resp, + blocks_by_root_req_resp, }; // TODO: set peer scoring params @@ -166,7 +169,8 @@ pub async fn start_p2p( #[derive(NetworkBehaviour)] pub(crate) struct Behaviour { gossipsub: libp2p::gossipsub::Behaviour, - req_resp: request_response::Behaviour, + status_req_resp: request_response::Behaviour, + blocks_by_root_req_resp: request_response::Behaviour, } pub(crate) struct P2PServer { @@ -204,7 +208,10 @@ async fn event_loop(mut server: P2PServer) { async fn handle_swarm_event(server: &mut P2PServer, event: SwarmEvent) { match event { - SwarmEvent::Behaviour(BehaviourEvent::ReqResp(req_resp_event)) => { + SwarmEvent::Behaviour(BehaviourEvent::StatusReqResp(req_resp_event)) => { + req_resp::handle_req_resp_message(server, req_resp_event).await; + } + SwarmEvent::Behaviour(BehaviourEvent::BlocksByRootReqResp(req_resp_event)) => { req_resp::handle_req_resp_message(server, req_resp_event).await; } SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( @@ -228,7 +235,7 @@ async fn handle_swarm_event(server: &mut P2PServer, event: SwarmEvent Date: Tue, 27 Jan 2026 18:26:11 -0300 Subject: [PATCH 18/25] refactor: use imports --- crates/blockchain/src/lib.rs | 11 ++++++----- crates/net/rpc/src/lib.rs | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index dc5bf86..182e55d 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -3,6 +3,7 @@ use std::time::{Duration, SystemTime}; use ethlambda_state_transition::is_proposer; use ethlambda_storage::Store; +use ethlambda_types::primitives::H256; use ethlambda_types::{ attestation::{Attestation, AttestationData, SignedAttestation}, block::{BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation}, @@ -30,7 +31,7 @@ pub enum P2PMessage { /// Publish a block to the gossip network. PublishBlock(SignedBlockWithAttestation), /// Fetch a block by its root hash. - FetchBlock(ethlambda_types::primitives::H256), + FetchBlock(H256), } pub struct BlockChain { @@ -92,10 +93,10 @@ struct BlockChainServer { key_manager: key_manager::KeyManager, // Pending blocks waiting for their parent - pending_blocks: HashMap>, + pending_blocks: HashMap>, // Track in-flight block requests (prevent duplicates) - in_flight_requests: HashSet, + in_flight_requests: HashSet, } impl BlockChainServer { @@ -305,7 +306,7 @@ impl BlockChainServer { } } - fn request_missing_block(&mut self, block_root: ethlambda_types::primitives::H256) { + fn request_missing_block(&mut self, block_root: H256) { // Check if already requested (avoid duplicate requests) if self.in_flight_requests.contains(&block_root) { trace!(%block_root, "Block already requested, skipping duplicate"); @@ -324,7 +325,7 @@ impl BlockChainServer { } } - fn process_pending_children(&mut self, parent_root: ethlambda_types::primitives::H256) { + fn process_pending_children(&mut self, parent_root: H256) { // Remove and process all blocks that were waiting for this parent if let Some(children) = self.pending_blocks.remove(&parent_root) { info!(%parent_root, num_children=%children.len(), diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 88562bf..9546211 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -77,7 +77,7 @@ mod tests { use ethlambda_storage::{Store, backend::InMemoryBackend}; use ethlambda_types::{ block::{BlockBody, BlockHeader}, - primitives::TreeHash, + primitives::{H256, TreeHash}, state::{ChainConfig, Checkpoint, JustificationValidators, JustifiedSlots, State}, }; use http_body_util::BodyExt; @@ -90,13 +90,13 @@ mod tests { let genesis_header = BlockHeader { slot: 0, proposer_index: 0, - parent_root: ethlambda_types::primitives::H256::ZERO, - state_root: ethlambda_types::primitives::H256::ZERO, + parent_root: H256::ZERO, + state_root: H256::ZERO, body_root: BlockBody::default().tree_hash_root(), }; let genesis_checkpoint = Checkpoint { - root: ethlambda_types::primitives::H256::ZERO, + root: H256::ZERO, slot: 0, }; From debbaf1d8af107787171f75e00122faef4ac729b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:12:44 -0300 Subject: [PATCH 19/25] Revert "fix: split req-resp behaviour" This reverts commit b4ffabdbaf662ff8c78ade263a862ceed5ccedf1. --- crates/net/p2p/src/lib.rs | 37 ++++++++++--------------- crates/net/p2p/src/req_resp/handlers.rs | 4 +-- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 74134b4..b7ba30e 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -73,26 +73,23 @@ pub async fn start_p2p( let gossipsub = libp2p::gossipsub::Behaviour::new(MessageAuthenticity::Anonymous, config) .expect("failed to initiate behaviour"); - let status_req_resp = request_response::Behaviour::new( - vec![( - StreamProtocol::new(STATUS_PROTOCOL_V1), - request_response::ProtocolSupport::Full, - )], - Default::default(), - ); - - let blocks_by_root_req_resp = request_response::Behaviour::new( - vec![( - StreamProtocol::new(BLOCKS_BY_ROOT_PROTOCOL_V1), - request_response::ProtocolSupport::Full, - )], + let req_resp = request_response::Behaviour::new( + vec![ + ( + StreamProtocol::new(STATUS_PROTOCOL_V1), + request_response::ProtocolSupport::Full, + ), + ( + StreamProtocol::new(BLOCKS_BY_ROOT_PROTOCOL_V1), + request_response::ProtocolSupport::Full, + ), + ], Default::default(), ); let behavior = Behaviour { gossipsub, - status_req_resp, - blocks_by_root_req_resp, + req_resp, }; // TODO: set peer scoring params @@ -169,8 +166,7 @@ pub async fn start_p2p( #[derive(NetworkBehaviour)] pub(crate) struct Behaviour { gossipsub: libp2p::gossipsub::Behaviour, - status_req_resp: request_response::Behaviour, - blocks_by_root_req_resp: request_response::Behaviour, + req_resp: request_response::Behaviour, } pub(crate) struct P2PServer { @@ -208,10 +204,7 @@ async fn event_loop(mut server: P2PServer) { async fn handle_swarm_event(server: &mut P2PServer, event: SwarmEvent) { match event { - SwarmEvent::Behaviour(BehaviourEvent::StatusReqResp(req_resp_event)) => { - req_resp::handle_req_resp_message(server, req_resp_event).await; - } - SwarmEvent::Behaviour(BehaviourEvent::BlocksByRootReqResp(req_resp_event)) => { + SwarmEvent::Behaviour(BehaviourEvent::ReqResp(req_resp_event)) => { req_resp::handle_req_resp_message(server, req_resp_event).await; } SwarmEvent::Behaviour(BehaviourEvent::Gossipsub( @@ -235,7 +228,7 @@ async fn handle_swarm_event(server: &mut P2PServer, event: SwarmEvent Date: Tue, 27 Jan 2026 19:48:48 -0300 Subject: [PATCH 20/25] fix: implement protocol selection in libp2p fork --- Cargo.lock | 176 ++++++++---------------- crates/net/p2p/Cargo.toml | 5 +- crates/net/p2p/src/lib.rs | 11 +- crates/net/p2p/src/req_resp/handlers.rs | 11 +- 4 files changed, 82 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65ffe4f..dd70cad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2750,9 +2750,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -2777,15 +2774,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "hashlink" version = "0.10.0" @@ -3518,9 +3506,8 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libp2p" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce71348bf5838e46449ae240631117b487073d5f347c06d434caddcb91dceb5a" +version = "0.56.1" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "bytes", "either", @@ -3568,8 +3555,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16ccf824ee859ca83df301e1c0205270206223fd4b1f2e512a693e1912a8f4a" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3579,8 +3565,7 @@ dependencies = [ [[package]] name = "libp2p-autonat" version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fab5e25c49a7d48dac83d95d8f3bac0a290d8a5df717012f6e34ce9886396c0b" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "async-trait", "asynchronous-codec", @@ -3604,8 +3589,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18b8b607cf3bfa2f8c57db9c7d8569a315d5cc0a282e6bfd5ebfc0a9840b2a0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3615,8 +3599,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.43.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "249128cd37a2199aff30a7675dffa51caf073b51aa612d2f544b19932b9aebca" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "either", "fnv", @@ -3633,22 +3616,21 @@ dependencies = [ "rw-stream-sink", "thiserror 2.0.17", "tracing", - "unsigned-varint 0.8.0", + "unsigned-varint", "web-time", ] [[package]] name = "libp2p-dcutr" version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4107305e12158af3e66960b6181789c547394c9c9a8696f721521602bfc73a" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "asynchronous-codec", "either", "futures", "futures-bounded", "futures-timer", - "hashlink 0.10.0", + "hashlink", "libp2p-core", "libp2p-identity", "libp2p-swarm", @@ -3662,8 +3644,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "async-trait", "futures", @@ -3678,8 +3659,7 @@ dependencies = [ [[package]] name = "libp2p-floodsub" version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0914997f56315c83bc64ffb721cd4e764ad819370582db287232c5791469697" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "asynchronous-codec", "bytes", @@ -3699,9 +3679,8 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" -version = "0.49.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f58e37d8d6848e5c4c9e3c35c6f61133235bff2960c9c00a663b0849301221" +version = "0.50.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "async-channel", "asynchronous-codec", @@ -3713,11 +3692,12 @@ dependencies = [ "futures", "futures-timer", "getrandom 0.2.16", - "hashlink 0.9.1", + "hashlink", "hex_fmt", "libp2p-core", "libp2p-identity", "libp2p-swarm", + "prometheus-client", "quick-protobuf", "quick-protobuf-codec", "rand 0.8.5", @@ -3731,8 +3711,7 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "asynchronous-codec", "either", @@ -3775,9 +3754,8 @@ dependencies = [ [[package]] name = "libp2p-kad" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d3fd632a5872ec804d37e7413ceea20588f69d027a0fa3c46f82574f4dee60" +version = "0.49.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "asynchronous-codec", "bytes", @@ -3804,8 +3782,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66872d0f1ffcded2788683f76931be1c52e27f343edb93bc6d0bcd8887be443" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "hickory-proto", @@ -3815,7 +3792,7 @@ dependencies = [ "libp2p-swarm", "rand 0.8.5", "smallvec", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tracing", ] @@ -3823,8 +3800,7 @@ dependencies = [ [[package]] name = "libp2p-memory-connection-limits" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d052a767edd0235d5c29dacf46013955eabce1085781ce0d12a4fc66bf87cd" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3836,9 +3812,8 @@ dependencies = [ [[package]] name = "libp2p-metrics" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805a555148522cb3414493a5153451910cb1a146c53ffbf4385708349baf62b7" +version = "0.17.1" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "libp2p-core", @@ -3858,8 +3833,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.46.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc73eacbe6462a0eb92a6527cac6e63f02026e5407f8831bde8293f19217bfbf" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "asynchronous-codec", "bytes", @@ -3881,8 +3855,7 @@ dependencies = [ [[package]] name = "libp2p-ping" version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74bb7fcdfd9fead4144a3859da0b49576f171a8c8c7c0bfc7c541921d25e60d3" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "futures-timer", @@ -3897,8 +3870,7 @@ dependencies = [ [[package]] name = "libp2p-plaintext" version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e659439578fc6d305da8303834beb9d62f155f40e7f5b9d81c9f2b2c69d1926" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "asynchronous-codec", "bytes", @@ -3913,8 +3885,7 @@ dependencies = [ [[package]] name = "libp2p-pnet" version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf240b834dfa3f8b48feb2c4b87bb2cf82751543001b6ee86077f48183b18d52" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "pin-project", @@ -3927,8 +3898,7 @@ dependencies = [ [[package]] name = "libp2p-quic" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "futures-timer", @@ -3940,7 +3910,7 @@ dependencies = [ "rand 0.8.5", "ring", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -3949,8 +3919,7 @@ dependencies = [ [[package]] name = "libp2p-relay" version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9b0392ed623243ad298326b9f806d51191829ac7585cc825c54c6c67b04d9" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "asynchronous-codec", "bytes", @@ -3973,8 +3942,7 @@ dependencies = [ [[package]] name = "libp2p-rendezvous" version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15285d828c2b4a34cb660c2e74cd6938116daceab1f4357bae933d5b08cca933" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "async-trait", "asynchronous-codec", @@ -3996,8 +3964,7 @@ dependencies = [ [[package]] name = "libp2p-request-response" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f1cca83488b90102abac7b67d5c36fc65bc02ed47620228af7ed002e6a1478" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "async-trait", "cbor4ii", @@ -4016,15 +3983,14 @@ dependencies = [ [[package]] name = "libp2p-swarm" version = "0.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce88c6c4bf746c8482480345ea3edfd08301f49e026889d1cbccfa1808a9ed9e" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "either", "fnv", "futures", "futures-timer", "getrandom 0.2.16", - "hashlink 0.10.0", + "hashlink", "libp2p-core", "libp2p-identity", "libp2p-swarm-derive", @@ -4040,8 +4006,7 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.35.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "heck", "quote", @@ -4051,8 +4016,7 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.44.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6585b9309699f58704ec9ab0bb102eca7a3777170fa91a8678d73ca9cafa93" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "futures-timer", @@ -4067,8 +4031,7 @@ dependencies = [ [[package]] name = "libp2p-tls" version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "futures-rustls", @@ -4085,9 +4048,8 @@ dependencies = [ [[package]] name = "libp2p-uds" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0413aa7a1cc51c409358186a46a198ad9195a782dae6b9a95ea3acf5db67569d" +version = "0.43.1" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "libp2p-core", @@ -4096,9 +4058,8 @@ dependencies = [ [[package]] name = "libp2p-upnp" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4757e65fe69399c1a243bbb90ec1ae5a2114b907467bf09f3575e899815bb8d3" +version = "0.6.0" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "futures-timer", @@ -4112,8 +4073,7 @@ dependencies = [ [[package]] name = "libp2p-webrtc-utils" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490abff5ee5f9a7a77f0145c79cc97c76941231a3626f4dee18ebf2abb95618f" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "asynchronous-codec", "bytes", @@ -4134,8 +4094,7 @@ dependencies = [ [[package]] name = "libp2p-webrtc-websys" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3830f0bf6f0f16ded2c735599fe70baea43a8c1a2d76152216693329217301dd" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "bytes", "futures", @@ -4155,9 +4114,8 @@ dependencies = [ [[package]] name = "libp2p-websocket" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520e29066a48674c007bc11defe5dce49908c24cafd8fad2f5e1a6a8726ced53" +version = "0.45.2" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "either", "futures", @@ -4177,8 +4135,7 @@ dependencies = [ [[package]] name = "libp2p-websocket-websys" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73d85b4dc8c2044f58508461bd8bb12f541217c0038ade8cce0ddc1607b8f72" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "bytes", "futures", @@ -4194,8 +4151,7 @@ dependencies = [ [[package]] name = "libp2p-webtransport-websys" version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34bc528d7fa278e1324a88978114a610deaa9e75c8e2230cd868321c512b3f43" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "js-sys", @@ -4215,8 +4171,7 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f15df094914eb4af272acf9adaa9e287baa269943f32ea348ba29cfb9bfc60d8" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "either", "futures", @@ -4491,7 +4446,7 @@ dependencies = [ "percent-encoding", "serde", "static_assertions", - "unsigned-varint 0.8.0", + "unsigned-varint", "url", ] @@ -4515,7 +4470,7 @@ checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" dependencies = [ "core2", "serde", - "unsigned-varint 0.8.0", + "unsigned-varint", ] [[package]] @@ -4537,15 +4492,14 @@ dependencies = [ [[package]] name = "multistream-select" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "bytes", "futures", - "log", "pin-project", "smallvec", - "unsigned-varint 0.7.2", + "tracing", + "unsigned-varint", ] [[package]] @@ -5595,9 +5549,9 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" +checksum = "e4500adecd7af8e0e9f4dbce15cfee07ce913fbf6ad605cc468b83f2d531ee94" dependencies = [ "dtoa", "itoa", @@ -5607,9 +5561,9 @@ dependencies = [ [[package]] name = "prometheus-client-derive-encode" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" dependencies = [ "proc-macro2", "quote", @@ -5702,14 +5656,13 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "asynchronous-codec", "bytes", "quick-protobuf", - "thiserror 1.0.69", - "unsigned-varint 0.8.0", + "thiserror 2.0.17", + "unsigned-varint", ] [[package]] @@ -6277,8 +6230,7 @@ dependencies = [ [[package]] name = "rw-stream-sink" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +source = "git+https://github.com/lambdaclass/rust-libp2p.git?rev=cd6cc3b1e5db2c5e23e133c2201c23b063fc4895#cd6cc3b1e5db2c5e23e133c2201c23b063fc4895" dependencies = [ "futures", "pin-project", @@ -7381,12 +7333,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "unsigned-varint" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" - [[package]] name = "unsigned-varint" version = "0.8.0" diff --git a/crates/net/p2p/Cargo.toml b/crates/net/p2p/Cargo.toml index 3b7c2ad..9c26e34 100644 --- a/crates/net/p2p/Cargo.toml +++ b/crates/net/p2p/Cargo.toml @@ -17,7 +17,10 @@ ethlambda-types.workspace = true async-trait = "0.1" -libp2p = { version = "0.56", features = ["full"] } +# Fork with request-response feature for outbound protocol selection +libp2p = { git = "https://github.com/lambdaclass/rust-libp2p.git", rev = "cd6cc3b1e5db2c5e23e133c2201c23b063fc4895", features = [ + "full", +] } snap = "1.1" diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index b7ba30e..c90a97b 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -24,8 +24,9 @@ use tracing::{info, trace, warn}; use crate::{ gossipsub::{ATTESTATION_TOPIC_KIND, BLOCK_TOPIC_KIND, publish_attestation, publish_block}, - req_resp::{Codec, BLOCKS_BY_ROOT_PROTOCOL_V1, MAX_COMPRESSED_PAYLOAD_SIZE, Request, STATUS_PROTOCOL_V1, - build_status, fetch_block_from_peer, + req_resp::{ + BLOCKS_BY_ROOT_PROTOCOL_V1, Codec, MAX_COMPRESSED_PAYLOAD_SIZE, Request, + STATUS_PROTOCOL_V1, build_status, fetch_block_from_peer, }, }; @@ -229,7 +230,11 @@ async fn handle_swarm_event(server: &mut P2PServer, event: SwarmEvent Date: Tue, 27 Jan 2026 23:53:50 -0300 Subject: [PATCH 21/25] feat: add block fetch request retrying --- crates/net/p2p/src/lib.rs | 62 ++++++++++++++++++++- crates/net/p2p/src/req_resp/handlers.rs | 73 ++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 10 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index c90a97b..1edf7a2 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, net::{IpAddr, SocketAddr}, time::Duration, }; @@ -15,7 +15,7 @@ use libp2p::{ gossipsub::{MessageAuthenticity, ValidationMode}, identity::{PublicKey, secp256k1}, multiaddr::Protocol, - request_response, + request_response::{self, OutboundRequestId}, swarm::{NetworkBehaviour, SwarmEvent}, }; use sha2::Digest; @@ -36,6 +36,15 @@ mod req_resp; pub use metrics::populate_name_registry; +const MAX_FETCH_RETRIES: u32 = 3; +const INITIAL_BACKOFF_MS: u64 = 1000; +const BACKOFF_MULTIPLIER: u64 = 2; + +struct PendingRequest { + attempts: u32, + last_peer: Option, +} + pub async fn start_p2p( node_key: Vec, bootnodes: Vec, @@ -150,6 +159,8 @@ pub async fn start_p2p( info!("P2P node started on {listening_socket}"); + let (retry_tx, retry_rx) = mpsc::unbounded_channel(); + let server = P2PServer { swarm, blockchain, @@ -158,6 +169,10 @@ pub async fn start_p2p( attestation_topic, block_topic, connected_peers: HashSet::new(), + pending_requests: HashMap::new(), + request_id_map: HashMap::new(), + retry_tx, + retry_rx, }; event_loop(server).await; @@ -178,6 +193,10 @@ pub(crate) struct P2PServer { pub(crate) attestation_topic: libp2p::gossipsub::IdentTopic, pub(crate) block_topic: libp2p::gossipsub::IdentTopic, pub(crate) connected_peers: HashSet, + pub(crate) pending_requests: HashMap, + pub(crate) request_id_map: HashMap, + retry_tx: mpsc::UnboundedSender, + retry_rx: mpsc::UnboundedReceiver, } /// Event loop for the P2P crate. @@ -199,6 +218,9 @@ async fn event_loop(mut server: P2PServer) { }; handle_swarm_event(&mut server, event).await; } + Some(root) = server.retry_rx.recv() => { + handle_retry(&mut server, root).await; + } } } } @@ -298,11 +320,45 @@ async fn handle_p2p_message(server: &mut P2PServer, message: P2PMessage) { publish_block(server, signed_block).await; } P2PMessage::FetchBlock(root) => { - fetch_block_from_peer(server, root).await; + // Deduplicate - if already pending, ignore + if server.pending_requests.contains_key(&root) { + trace!(%root, "Block fetch already in progress, ignoring duplicate"); + return; + } + + // Send request and track it + if let Some(request_id) = fetch_block_from_peer(server, root).await { + server.pending_requests.insert( + root, + PendingRequest { + attempts: 1, + last_peer: None, + }, + ); + server.request_id_map.insert(request_id, root); + } } } } +async fn handle_retry(server: &mut P2PServer, root: ethlambda_types::primitives::H256) { + // Check if still pending (might have succeeded during backoff) + if !server.pending_requests.contains_key(&root) { + trace!(%root, "Block fetch completed during backoff, skipping retry"); + return; + } + + info!(%root, "Retrying block fetch after backoff"); + + // Retry the fetch (uses random peer selection) + if let Some(request_id) = fetch_block_from_peer(server, root).await { + server.request_id_map.insert(request_id, root); + } else { + tracing::error!(%root, "No peers available for retry, giving up"); + server.pending_requests.remove(&root); + } +} + pub struct Bootnode { ip: IpAddr, quic_port: u16, diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index e49e95f..731da10 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -1,9 +1,14 @@ use ethlambda_storage::Store; -use libp2p::{PeerId, request_response}; +use libp2p::{ + PeerId, + request_response::{self, OutboundRequestId}, +}; use rand::seq::SliceRandom; +use tokio::time::Duration; use tracing::{debug, error, info, warn}; use ethlambda_types::block::SignedBlockWithAttestation; +use ethlambda_types::primitives::TreeHash; use super::{ BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, Request, Response, ResponsePayload, @@ -43,6 +48,11 @@ pub async fn handle_req_resp_message( .. } => { warn!(%peer, ?request_id, %error, "Outbound request failed"); + + // Check if this was a block fetch request + if let Some(root) = server.request_id_map.remove(&request_id) { + handle_fetch_failure(server, root, peer, error).await; + } } request_response::Event::InboundFailure { peer, @@ -107,7 +117,16 @@ async fn handle_blocks_by_root_response( peer: PeerId, ) { let slot = block.message.block.slot; - info!(%peer, %slot, "Received BlocksByRoot response chunk"); + let root = block.message.block.tree_hash_root(); + + info!(%peer, %slot, %root, "Received BlocksByRoot response"); + + // Clean up tracking (success!) + if server.pending_requests.remove(&root).is_some() { + info!(%root, "Block fetch succeeded"); + server.request_id_map.retain(|_, r| *r != root); + } + server.blockchain.notify_new_block(block).await; } @@ -129,10 +148,10 @@ pub fn build_status(store: &Store) -> Status { pub async fn fetch_block_from_peer( server: &mut P2PServer, root: ethlambda_types::primitives::H256, -) { +) -> Option { if server.connected_peers.is_empty() { warn!(%root, "Cannot fetch block: no connected peers"); - return; + return None; } // Select random peer @@ -141,7 +160,7 @@ pub async fn fetch_block_from_peer( Some(&p) => p, None => { warn!(%root, "Failed to select random peer"); - return; + return None; } }; @@ -149,11 +168,11 @@ pub async fn fetch_block_from_peer( let mut request = BlocksByRootRequest::empty(); if let Err(err) = request.push(root) { error!(%root, ?err, "Failed to create BlocksByRoot request"); - return; + return None; } info!(%peer, %root, "Sending BlocksByRoot request for missing block"); - server + let request_id = server .swarm .behaviour_mut() .req_resp @@ -162,4 +181,44 @@ pub async fn fetch_block_from_peer( Request::BlocksByRoot(request), libp2p::StreamProtocol::new(BLOCKS_BY_ROOT_PROTOCOL_V1), ); + + // Update last_peer in tracking + if let Some(pending) = server.pending_requests.get_mut(&root) { + pending.last_peer = Some(peer); + } + + Some(request_id) +} + +async fn handle_fetch_failure( + server: &mut P2PServer, + root: ethlambda_types::primitives::H256, + peer: PeerId, + error: request_response::OutboundFailure, +) { + let Some(pending) = server.pending_requests.get_mut(&root) else { + return; + }; + + if pending.attempts >= super::super::MAX_FETCH_RETRIES { + error!(%root, %peer, attempts=%pending.attempts, %error, + "Block fetch failed after max retries, giving up"); + server.pending_requests.remove(&root); + return; + } + + let backoff_ms = super::super::INITIAL_BACKOFF_MS + * super::super::BACKOFF_MULTIPLIER.pow(pending.attempts - 1); + let backoff = Duration::from_millis(backoff_ms); + + warn!(%root, %peer, attempts=%pending.attempts, ?backoff, %error, + "Block fetch failed, scheduling retry"); + + pending.attempts += 1; + + let retry_tx = server.retry_tx.clone(); + tokio::spawn(async move { + tokio::time::sleep(backoff).await; + let _ = retry_tx.send(root); + }); } From c0eba9b1c6086fb98803ef0fbc2c639f53ab17fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:01:40 -0300 Subject: [PATCH 22/25] refactor: move bookkeeping to fetch_block_from_peer --- crates/net/p2p/src/lib.rs | 27 ++++++------------- crates/net/p2p/src/req_resp/handlers.rs | 36 +++++++++++++++---------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index 1edf7a2..b01a32a 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -40,9 +40,9 @@ const MAX_FETCH_RETRIES: u32 = 3; const INITIAL_BACKOFF_MS: u64 = 1000; const BACKOFF_MULTIPLIER: u64 = 2; -struct PendingRequest { - attempts: u32, - last_peer: Option, +pub(crate) struct PendingRequest { + pub(crate) attempts: u32, + pub(crate) last_peer: Option, } pub async fn start_p2p( @@ -326,17 +326,8 @@ async fn handle_p2p_message(server: &mut P2PServer, message: P2PMessage) { return; } - // Send request and track it - if let Some(request_id) = fetch_block_from_peer(server, root).await { - server.pending_requests.insert( - root, - PendingRequest { - attempts: 1, - last_peer: None, - }, - ); - server.request_id_map.insert(request_id, root); - } + // Send request and track it (tracking handled internally by fetch_block_from_peer) + fetch_block_from_peer(server, root).await; } } } @@ -350,11 +341,9 @@ async fn handle_retry(server: &mut P2PServer, root: ethlambda_types::primitives: info!(%root, "Retrying block fetch after backoff"); - // Retry the fetch (uses random peer selection) - if let Some(request_id) = fetch_block_from_peer(server, root).await { - server.request_id_map.insert(request_id, root); - } else { - tracing::error!(%root, "No peers available for retry, giving up"); + // Retry the fetch (tracking handled internally by fetch_block_from_peer) + if !fetch_block_from_peer(server, root).await { + tracing::error!(%root, "Failed to retry block fetch, giving up"); server.pending_requests.remove(&root); } } diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index 731da10..74f37c5 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -1,8 +1,5 @@ use ethlambda_storage::Store; -use libp2p::{ - PeerId, - request_response::{self, OutboundRequestId}, -}; +use libp2p::{PeerId, request_response}; use rand::seq::SliceRandom; use tokio::time::Duration; use tracing::{debug, error, info, warn}; @@ -14,7 +11,7 @@ use super::{ BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, Request, Response, ResponsePayload, ResponseResult, Status, }; -use crate::P2PServer; +use crate::{P2PServer, PendingRequest}; pub async fn handle_req_resp_message( server: &mut P2PServer, @@ -145,13 +142,14 @@ pub fn build_status(store: &Store) -> Status { } /// Fetch a missing block from a random connected peer. +/// Handles tracking in both pending_requests and request_id_map. pub async fn fetch_block_from_peer( server: &mut P2PServer, root: ethlambda_types::primitives::H256, -) -> Option { +) -> bool { if server.connected_peers.is_empty() { warn!(%root, "Cannot fetch block: no connected peers"); - return None; + return false; } // Select random peer @@ -160,7 +158,7 @@ pub async fn fetch_block_from_peer( Some(&p) => p, None => { warn!(%root, "Failed to select random peer"); - return None; + return false; } }; @@ -168,7 +166,7 @@ pub async fn fetch_block_from_peer( let mut request = BlocksByRootRequest::empty(); if let Err(err) = request.push(root) { error!(%root, ?err, "Failed to create BlocksByRoot request"); - return None; + return false; } info!(%peer, %root, "Sending BlocksByRoot request for missing block"); @@ -182,12 +180,22 @@ pub async fn fetch_block_from_peer( libp2p::StreamProtocol::new(BLOCKS_BY_ROOT_PROTOCOL_V1), ); - // Update last_peer in tracking - if let Some(pending) = server.pending_requests.get_mut(&root) { - pending.last_peer = Some(peer); - } + // Track the request if not already tracked (new request) + let pending = server + .pending_requests + .entry(root) + .or_insert(PendingRequest { + attempts: 1, + last_peer: None, + }); + + // Update last_peer + pending.last_peer = Some(peer); + + // Map request_id to root for failure handling + server.request_id_map.insert(request_id, root); - Some(request_id) + true } async fn handle_fetch_failure( From d182bca0dd459980220ef252045e9e8ba3622a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:04:32 -0300 Subject: [PATCH 23/25] refactor: remove BlockchainServer.in_flight_requests --- crates/blockchain/src/lib.rs | 21 ++------------------- crates/net/p2p/src/lib.rs | 3 ++- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 182e55d..823c89e 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::time::{Duration, SystemTime}; use ethlambda_state_transition::is_proposer; @@ -54,7 +54,6 @@ impl BlockChain { p2p_tx, key_manager, pending_blocks: HashMap::new(), - in_flight_requests: HashSet::new(), } .start(); let time_until_genesis = (SystemTime::UNIX_EPOCH + Duration::from_secs(genesis_time)) @@ -94,9 +93,6 @@ struct BlockChainServer { // Pending blocks waiting for their parent pending_blocks: HashMap>, - - // Track in-flight block requests (prevent duplicates) - in_flight_requests: HashSet, } impl BlockChainServer { @@ -307,19 +303,9 @@ impl BlockChainServer { } fn request_missing_block(&mut self, block_root: H256) { - // Check if already requested (avoid duplicate requests) - if self.in_flight_requests.contains(&block_root) { - trace!(%block_root, "Block already requested, skipping duplicate"); - return; - } - - // Mark as requested - self.in_flight_requests.insert(block_root); - - // Send request to P2P layer + // Send request to P2P layer (deduplication handled by P2P module) if let Err(err) = self.p2p_tx.send(P2PMessage::FetchBlock(block_root)) { error!(%block_root, %err, "Failed to send FetchBlock message to P2P"); - self.in_flight_requests.remove(&block_root); } else { info!(%block_root, "Requested missing block from network"); } @@ -339,9 +325,6 @@ impl BlockChainServer { self.on_block(child_block); } } - - // Also clear the in-flight request for this block - self.in_flight_requests.remove(&parent_root); } fn on_gossip_attestation(&mut self, attestation: SignedAttestation) { diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index b01a32a..c975dd3 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -6,6 +6,7 @@ use std::{ use ethlambda_blockchain::{BlockChain, P2PMessage}; use ethlambda_storage::Store; +use ethlambda_types::primitives::H256; use ethrex_common::H264; use ethrex_p2p::types::NodeRecord; use ethrex_rlp::decode::RLPDecode; @@ -332,7 +333,7 @@ async fn handle_p2p_message(server: &mut P2PServer, message: P2PMessage) { } } -async fn handle_retry(server: &mut P2PServer, root: ethlambda_types::primitives::H256) { +async fn handle_retry(server: &mut P2PServer, root: H256) { // Check if still pending (might have succeeded during backoff) if !server.pending_requests.contains_key(&root) { trace!(%root, "Block fetch completed during backoff, skipping retry"); From dd87b14ead3abf8a58a7ed1a747d9331636fccb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:15:10 -0300 Subject: [PATCH 24/25] chore: tweak retries, initial backoff, and multiplier --- crates/net/p2p/src/lib.rs | 7 ++++--- crates/net/p2p/src/req_resp/handlers.rs | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index c975dd3..06f77ad 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -37,9 +37,10 @@ mod req_resp; pub use metrics::populate_name_registry; -const MAX_FETCH_RETRIES: u32 = 3; -const INITIAL_BACKOFF_MS: u64 = 1000; -const BACKOFF_MULTIPLIER: u64 = 2; +// 10ms, 40ms, 160ms, 640ms, 2560ms +const MAX_FETCH_RETRIES: u32 = 5; +const INITIAL_BACKOFF_MS: u64 = 10; +const BACKOFF_MULTIPLIER: u64 = 4; pub(crate) struct PendingRequest { pub(crate) attempts: u32, diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index 74f37c5..d8e670c 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -11,7 +11,7 @@ use super::{ BLOCKS_BY_ROOT_PROTOCOL_V1, BlocksByRootRequest, Request, Response, ResponsePayload, ResponseResult, Status, }; -use crate::{P2PServer, PendingRequest}; +use crate::{BACKOFF_MULTIPLIER, INITIAL_BACKOFF_MS, MAX_FETCH_RETRIES, P2PServer, PendingRequest}; pub async fn handle_req_resp_message( server: &mut P2PServer, @@ -208,15 +208,14 @@ async fn handle_fetch_failure( return; }; - if pending.attempts >= super::super::MAX_FETCH_RETRIES { + if pending.attempts >= MAX_FETCH_RETRIES { error!(%root, %peer, attempts=%pending.attempts, %error, "Block fetch failed after max retries, giving up"); server.pending_requests.remove(&root); return; } - let backoff_ms = super::super::INITIAL_BACKOFF_MS - * super::super::BACKOFF_MULTIPLIER.pow(pending.attempts - 1); + let backoff_ms = INITIAL_BACKOFF_MS * BACKOFF_MULTIPLIER.pow(pending.attempts - 1); let backoff = Duration::from_millis(backoff_ms); warn!(%root, %peer, attempts=%pending.attempts, ?backoff, %error, From 5a5001a20efd0070ebc48b83da97ed68cc50ad97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:27:25 -0300 Subject: [PATCH 25/25] fix: avoid calling store::on_block if parent is missing --- crates/blockchain/src/lib.rs | 31 ++++++++++++++++++------------- crates/blockchain/src/store.rs | 5 +++-- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 823c89e..3fe6eb4 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -276,26 +276,31 @@ impl BlockChainServer { fn on_block(&mut self, signed_block: SignedBlockWithAttestation) { let slot = signed_block.message.block.slot; let block_root = signed_block.message.block.tree_hash_root(); + let parent_root = signed_block.message.block.parent_root; - match self.process_block(signed_block.clone()) { + // Check if parent block exists before attempting to process + if !self.store.contains_block(&parent_root) { + info!(%slot, %parent_root, %block_root, "Block parent missing, storing as pending"); + + // Store block for later processing + self.pending_blocks + .entry(parent_root) + .or_default() + .push(signed_block); + + // Request missing parent from network + self.request_missing_block(parent_root); + return; + } + + // Parent exists, proceed with processing + match self.process_block(signed_block) { Ok(_) => { info!(%slot, "Block processed successfully"); // Check if any pending blocks can now be processed self.process_pending_children(block_root); } - Err(StoreError::MissingParentState { parent_root, .. }) => { - info!(%slot, %parent_root, %block_root, "Block parent missing, storing as pending"); - - // Store block for later processing - self.pending_blocks - .entry(parent_root) - .or_default() - .push(signed_block); - - // Request missing parent from network - self.request_missing_block(parent_root); - } Err(err) => { warn!(%slot, %err, "Failed to process block"); } diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 71d280f..8b28e37 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -299,8 +299,9 @@ pub fn on_block( return Ok(()); } - // Verify parent chain is available - // TODO: sync parent chain if parent is missing + // Verify parent state is available + // Note: Parent block existence is checked by the caller before calling this function. + // This check ensures the state has been computed for the parent block. let parent_state = store .get_state(&block.parent_root)