From e5a10ad37436e088093a1c85e0dc403b57a8eb6f Mon Sep 17 00:00:00 2001 From: diego Date: Tue, 4 Feb 2025 21:16:35 +0100 Subject: [PATCH 01/63] handshake --- anchor/network/Cargo.toml | 11 + anchor/network/src/behaviour.rs | 23 +- anchor/network/src/handshake/behaviour.rs | 313 ++++++++++++++++++ anchor/network/src/handshake/codec.rs | 175 ++++++++++ anchor/network/src/handshake/error.rs | 25 ++ anchor/network/src/handshake/mod.rs | 4 + .../network/src/handshake/record/envelope.rs | 108 ++++++ anchor/network/src/handshake/record/mod.rs | 3 + anchor/network/src/handshake/record/record.rs | 13 + .../network/src/handshake/record/signing.rs | 87 +++++ anchor/network/src/handshake/types.rs | 86 +++++ anchor/network/src/lib.rs | 2 +- anchor/network/src/network.rs | 39 +++ anchor/network/src/types.rs | 1 + anchor/network/src/types/node_info.rs | 123 +++++++ 15 files changed, 1011 insertions(+), 2 deletions(-) create mode 100644 anchor/network/src/handshake/behaviour.rs create mode 100644 anchor/network/src/handshake/codec.rs create mode 100644 anchor/network/src/handshake/error.rs create mode 100644 anchor/network/src/handshake/mod.rs create mode 100644 anchor/network/src/handshake/record/envelope.rs create mode 100644 anchor/network/src/handshake/record/mod.rs create mode 100644 anchor/network/src/handshake/record/record.rs create mode 100644 anchor/network/src/handshake/record/signing.rs create mode 100644 anchor/network/src/handshake/types.rs create mode 100644 anchor/network/src/types/node_info.rs diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index eac709ebf..ed4263a8c 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -22,7 +22,9 @@ libp2p = { version = "0.54", default-features = false, features = [ "gossipsub", "quic", "ping", + "request-response", ] } +#libp2p-request-response = "0.26.3" lighthouse_network = { workspace = true } serde = { workspace = true } ssz_types = "0.10" @@ -32,6 +34,15 @@ tokio = { workspace = true } tracing = { workspace = true } types = { workspace = true } version = { workspace = true } +serde_json = "1.0.137" +thiserror = "1.0.69" +prost = "0.13.4" +async-trait = "0.1.85" +rand = "0.8.5" + +# Add to parse or generate .proto +[build-dependencies] +prost-build = "0.13.4" [dev-dependencies] async-channel = { workspace = true } diff --git a/anchor/network/src/behaviour.rs b/anchor/network/src/behaviour.rs index baa2dd177..7796bb972 100644 --- a/anchor/network/src/behaviour.rs +++ b/anchor/network/src/behaviour.rs @@ -1,6 +1,9 @@ +use discv5::libp2p_identity::PeerId; use crate::discovery::Discovery; +use crate::handshake::behaviour::{HandshakeBehaviour, PeerInfo, PeerInfoStore, Subnets, SubnetsIndex}; +use libp2p::request_response::Behaviour; use libp2p::swarm::NetworkBehaviour; -use libp2p::{gossipsub, identify, ping}; +use libp2p::{gossipsub, identify, ping, request_response}; #[derive(NetworkBehaviour)] pub struct AnchorBehaviour { @@ -12,4 +15,22 @@ pub struct AnchorBehaviour { pub gossipsub: gossipsub::Behaviour, /// Discv5 Discovery protocol. pub discovery: Discovery, + + pub handshake: HandshakeBehaviour, +} + +#[derive(Default)] +struct DummyPeerInfoStore {} +impl PeerInfoStore for DummyPeerInfoStore { + fn update(&self, peer: PeerId, f: impl FnOnce(&mut PeerInfo)) { + + } } + +#[derive(Default)] +struct DummySubnetsIndex {} +impl SubnetsIndex for DummySubnetsIndex { + fn update_peer_subnets(&self, peer: PeerId, subnets: Subnets) { + + } +} \ No newline at end of file diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs new file mode 100644 index 000000000..72c1e702b --- /dev/null +++ b/anchor/network/src/handshake/behaviour.rs @@ -0,0 +1,313 @@ +use discv5::libp2p_identity::Keypair; +use discv5::multiaddr::Multiaddr; +use libp2p::core::transport::PortUse; +use libp2p::core::Endpoint; +use libp2p::request_response::{ + self, Behaviour, Config, Event, OutboundRequestId, ProtocolSupport, +}; +use libp2p::swarm::{ + ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, + THandlerOutEvent, ToSwarm, +}; +use libp2p::{PeerId, StreamProtocol}; +use prost::Message; +use std::collections::HashMap; +use std::error::Error; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; +use std::time::Instant; +use tracing::debug; +use crate::handshake::codec::EnvelopeCodec; +use crate::handshake::record::envelope::Envelope; +use crate::handshake::record::signing::{consume_envelope, seal_record}; +use crate::handshake::types::NodeInfo; + +/// Event emitted on handshake completion or failure. +#[derive(Debug)] +pub enum HandshakeEvent { + Completed { peer: PeerId, info: NodeInfo }, + Failed { peer: PeerId, error: String }, +} + +/// Trait for updating peer information. +pub trait PeerInfoStore: Send + Sync { + fn update(&self, peer: PeerId, f: impl FnOnce(&mut PeerInfo)); +} + +/// Trait for updating peer subnets. +pub trait SubnetsIndex: Send + Sync { + fn update_peer_subnets(&self, peer: PeerId, subnets: Subnets); +} + +/// Information about a peer. +#[derive(Clone, Debug, Default)] +pub struct PeerInfo { + pub last_handshake: Option, + pub last_error: Option, +} + +/// Subnets type (example implementation). +#[derive(Clone, Debug, Default)] +pub struct Subnets; + +impl Subnets { + pub fn from_str(s: &str) -> Result> { + // Parse subnets from string + Ok(Subnets) + } +} + +/// Network behaviour handling the handshake protocol. +pub struct HandshakeBehaviour { + /// Request-response behaviour for the handshake protocol. + behaviour: Behaviour, + /// Pending outgoing handshake requests. + pending_handshakes: HashMap, + /// Keypair for signing envelopes. + keypair: Keypair, + /// Local node's information. + local_node_info: Arc>, + /// Filters to apply on received node info. + //filters: Vec Result<(), Box> + Send + Sync>>, + /// Peer info storage. + //peer_info: Arc

, + /// Subnets index. + //subnets_index: Arc, + /// Events to emit. + events: Vec, +} + +//impl HandshakeBehaviour +impl HandshakeBehaviour +// where +// P: PeerInfoStore, +// S: SubnetsIndex, +{ + pub fn new( + keypair: Keypair, + local_node_info: Arc>, + // peer_info: Arc

, + // subnets_index: Arc, + // filters: Vec Result<(), Box> + Send + Sync>>, + ) -> Self { + // NodeInfoProtocol is the protocol.ID used for handshake + const NODE_INFO_PROTOCOL: &'static str = "/ssv/info/0.0.1"; + + let protocol = StreamProtocol::new(NODE_INFO_PROTOCOL); + let behaviour = Behaviour::new([(protocol, ProtocolSupport::Full)], Config::default()); + + Self { + behaviour: behaviour, + pending_handshakes: HashMap::new(), + keypair, + local_node_info, + // filters, + // peer_info, + // subnets_index, + events: Vec::new(), + } + } + + /// Create a signed envelope containing local node info. + fn sealed_node_record(&self) -> Envelope { + let node_info = self.local_node_info.lock().unwrap().clone(); + seal_record(&node_info, &self.keypair).unwrap() + } + + /// Verify an incoming envelope and apply filters. + fn verify_envelope( + &mut self, + envelope: &Envelope, + peer: PeerId, + ) -> Result> { + let (_, mut node_info) = consume_envelope::(&envelope.encode_to_vec()?)?; + + // Apply all filters + // for filter in &self.filters { + // filter(peer, &node_info)?; + // } + + // Update peer info + // self.peer_info.update(peer, |info| { + // info.last_handshake = Some(Instant::now()); + // info.last_error = None; + // }); + + // Update subnets + // if let Ok(subnets) = Subnets::from_str(node_info.metadata.subnets.as_str()) { + // self.subnets_index.update_peer_subnets(peer, subnets); + // } + + Ok(node_info) + } +} + +impl NetworkBehaviour for HandshakeBehaviour +//impl NetworkBehaviour for HandshakeBehaviour +// where +// P: PeerInfoStore, +// S: SubnetsIndex, +{ + type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; + type ToSwarm = HandshakeEvent; + + fn handle_established_inbound_connection( + &mut self, + connection_id: ConnectionId, + peer: PeerId, + local_addr: &Multiaddr, + remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + self.behaviour.handle_established_inbound_connection( + connection_id, + peer, + local_addr, + remote_addr, + ) + } + + fn handle_established_outbound_connection( + &mut self, + connection_id: ConnectionId, + peer: PeerId, + addr: &Multiaddr, + role_override: Endpoint, + port_use: PortUse, + ) -> Result, ConnectionDenied> { + self.behaviour.handle_established_outbound_connection( + connection_id, + peer, + addr, + role_override, + port_use, + ) + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + // Initiate handshake on new connection + if let FromSwarm::ConnectionEstablished(conn_est) = &event { + let peer = conn_est.peer_id; + let request = self.sealed_node_record(); + let request_id = self.behaviour.send_request(&peer, request); + self.pending_handshakes.insert(request_id, peer); + } + + // Delegate other events to inner behaviour + self.behaviour.on_swarm_event(event); + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: THandlerOutEvent, + ) { + self.behaviour + .on_connection_handler_event(peer_id, connection_id, event); + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + // Process events from inner request-response behaviour + while let Poll::Ready(event) = self.behaviour.poll(cx) { + match event { + ToSwarm::GenerateEvent(event) => match event { + Event::Message { + peer, + message: + request_response::Message::Request { + request, channel, .. + }, + } => { + debug!("Received handshake request"); + // Handle incoming request: send response then verify + let response = self.sealed_node_record(); + match self.behaviour.send_response(channel, response) { + Ok(_) => {} + Err(e) => { + self.events.push(HandshakeEvent::Failed { + peer, + error: "error".to_string(), + }); + } + } + + match self.verify_envelope(&request, peer) { + Ok(info) => self.events.push(HandshakeEvent::Completed { peer, info }), + Err(e) => self.events.push(HandshakeEvent::Failed { + peer, + error: "error".to_string(), + }), + } + } + Event::Message { + message: + request_response::Message::Response { + request_id, + response, + .. + }, + .. + } => { + // Handle outgoing response + if let Some(peer) = self.pending_handshakes.remove(&request_id) { + debug!(?response, "Received handshake response"); + match self.verify_envelope(&response, peer) { + Ok(info) => { + self.events.push(HandshakeEvent::Completed { peer, info }) + } + Err(e) => self.events.push(HandshakeEvent::Failed { + peer, + error: "error".to_string(), + }), + } + } + } + Event::OutboundFailure { + request_id, + peer, + error, + .. + } => { + if let Some(peer) = self.pending_handshakes.remove(&request_id) { + self.events.push(HandshakeEvent::Failed { + peer, + error: format!("Outbound failure: {error}"), + }); + debug!(?error, "Outbound failure"); + } + } + Event::InboundFailure { peer, error, .. } => { + self.events.push(HandshakeEvent::Failed { + peer, + error: format!("Inbound failure: {error}"), + }); + } + _ => {} + }, + ToSwarm::Dial { opts } => return Poll::Ready(ToSwarm::Dial { opts }), + ToSwarm::NotifyHandler { + peer_id, + handler, + event, + } => { + return Poll::Ready(ToSwarm::NotifyHandler { + peer_id, + handler, + event, + }); + } + _ => {} + } + } + + // Emit queued events + if !self.events.is_empty() { + return Poll::Ready(ToSwarm::GenerateEvent(self.events.remove(0))); + } + + Poll::Pending + } +} diff --git a/anchor/network/src/handshake/codec.rs b/anchor/network/src/handshake/codec.rs new file mode 100644 index 000000000..7fef55b6c --- /dev/null +++ b/anchor/network/src/handshake/codec.rs @@ -0,0 +1,175 @@ +use crate::handshake::record::envelope::Envelope; +use futures::{AsyncReadExt, AsyncWriteExt}; +use libp2p::futures::{AsyncRead, AsyncWrite}; +use libp2p::request_response::Codec; +use std::io; +use async_trait::async_trait; +use libp2p::StreamProtocol; +use prost::bytes::BytesMut; +use prost::encoding::{decode_varint, encode_varint, encoded_len_varint}; +use prost::Message; +use tracing::debug; + + +/// Reads a varint‑encoded length from the async stream using the Prost decoder. +/// We read one byte at a time (maximum 10 bytes) until we see a byte with its +/// high‐bit clear. Then we call Prost’s `decode_varint` on the accumulated slice. +async fn read_varint_length(io: &mut T) -> io::Result { + // A varint is at most 10 bytes long. + let mut buf = [0u8; 10]; + let mut pos = 0; + loop { + if pos >= buf.len() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "varint too long", + )); + } + // Read one byte. + io.read_exact(&mut buf[pos..pos + 1]).await?; + // If the high-bit is clear, we have reached the end of the varint. + if buf[pos] & 0x80 == 0 { + // Create a slice containing the varint bytes. + let mut slice = &buf[..pos + 1]; + // Use Prost’s varint decoder. + let value = decode_varint(&mut slice) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + return Ok(value as usize); + } + pos += 1; + } +} + +/// Writes the given length as a varint‑encoded prefix to the async stream, +/// using Prost’s `encode_varint` function. +async fn write_varint_length(io: &mut T, value: usize) -> io::Result<()> { + let cap = encoded_len_varint(value as u64); + let mut buf = BytesMut::with_capacity(cap); + encode_varint(value as u64, &mut buf); + io.write_all(&buf).await?; + Ok(()) +} + +/// A `Codec` that reads/writes an **`Envelope`** in a length-prefixed Protobuf style: +/// - <4-byte big-endian length> +#[derive(Clone, Debug, Default)] +pub struct EnvelopeCodec; + +#[async_trait] +impl Codec for EnvelopeCodec { + type Protocol = StreamProtocol; + type Request = Envelope; + type Response = Envelope; + + async fn read_request( + &mut self, + _protocol: &Self::Protocol, + io: &mut T, + ) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + debug!("reading handsake request"); + // read length + let mut len_buf = [0u8; 4]; + io.read_exact(&mut len_buf).await?; + let msg_len = u32::from_be_bytes(len_buf) as usize; + + // read that many bytes + let mut msg_buf = vec![0u8; msg_len]; + io.read_exact(&mut msg_buf).await?; + + // decode + let env = Envelope::decode_from_slice(&msg_buf) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + debug!("read handshake request"); + Ok(env) + } + + async fn read_response( + &mut self, + _protocol: &Self::Protocol, + io: &mut T, + ) -> io::Result + where + T: AsyncRead + Unpin + Send, + { + // debug!("reading handshake response"); + // // same approach + // let mut len_buf = [0u8; 4]; + // io.read_exact(&mut len_buf).await?; + // let msg_len = u32::from_be_bytes(len_buf) as usize; + // + // let mut msg_buf = vec![0u8; msg_len]; + // io.read_exact(&mut msg_buf).await?; + // + // let env = Envelope::decode_from_slice(&msg_buf); + // //.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + // match env { + // Ok(env) => { + // debug!("read handshake response"); + // Ok(env) }, + // Err(e) => { + // debug!(?e, "error decoding envelope"); + // Err(io::Error::new(io::ErrorKind::InvalidData, e)) + // } + // } + debug!("reading handshake response"); + match read_varint_length(io).await { + Ok(msg_len) => { + let mut msg_buf = vec![0u8; msg_len]; + io.read_exact(&mut msg_buf).await?; + let env = Envelope::decode_from_slice(&msg_buf) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + debug!("read handshake response"); + Ok(env) + } + Err(error) => { + debug!(?error, "error reading handshake response"); + Err(io::Error::new(io::ErrorKind::InvalidData, "invalid varint")) + } + } + } + + async fn write_request( + &mut self, + _protocol: &Self::Protocol, + io: &mut T, + req: Self::Request, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + debug!(req = ?req, "writing handshake request"); + let raw = req.encode_to_vec()?; + // Write the varint length prefix. + write_varint_length(io, raw.len()).await?; + // Write the message bytes. + io.write_all(&raw).await?; + io.close().await?; + debug!("wrote handshake request"); + Ok(()) + } + + async fn write_response( + &mut self, + _protocol: &Self::Protocol, + io: &mut T, + res: Self::Response, + ) -> io::Result<()> + where + T: AsyncWrite + Unpin + Send, + { + debug!("writing handshake response"); + let raw = res + .encode_to_vec() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + let len = (raw.len() as u32).to_be_bytes(); + io.write_all(&len).await?; + io.write_all(&raw).await?; + let r = io.close().await; + debug!("wrote handshake response"); + r + } +} diff --git a/anchor/network/src/handshake/error.rs b/anchor/network/src/handshake/error.rs new file mode 100644 index 000000000..4d23b9d5e --- /dev/null +++ b/anchor/network/src/handshake/error.rs @@ -0,0 +1,25 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum HandshakeError { + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("Invalid signature")] + InvalidSignature, + + #[error("Network ID mismatch")] + NetworkMismatch, + + #[error("Subnets format error")] + SubnetsFormat, + + #[error("Peer rejected")] + PeerRejected, + + #[error("Crypto error: {0}")] + Crypto(String), + + InvalidMessageFormat, + ResponseFailed, +} \ No newline at end of file diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs new file mode 100644 index 000000000..6761b4411 --- /dev/null +++ b/anchor/network/src/handshake/mod.rs @@ -0,0 +1,4 @@ +pub mod behaviour; +mod codec; +pub mod record; +pub mod types; diff --git a/anchor/network/src/handshake/record/envelope.rs b/anchor/network/src/handshake/record/envelope.rs new file mode 100644 index 000000000..67934aeb4 --- /dev/null +++ b/anchor/network/src/handshake/record/envelope.rs @@ -0,0 +1,108 @@ +use prost::Message; +use strum::Display; + +/// The Envelope structure exactly matching Go's Envelope fields and tags: +/// 1 => public_key +/// 2 => payload_type +/// 3 => payload +/// 4 => signature +/// +/// All are `bytes`, just like in Go. +#[derive(Clone, PartialEq, Message)] +pub struct Envelope { + #[prost(bytes = "vec", tag = "1")] + pub public_key: Vec, + + #[prost(bytes = "vec", tag = "2")] + pub payload_type: Vec, + + #[prost(bytes = "vec", tag = "3")] + pub payload: Vec, + + #[prost(bytes = "vec", tag = "4")] + pub signature: Vec, +} + +impl Envelope { + /// Encode the Envelope to a Protobuf byte array (like `proto.Marshal` in Go). + pub fn encode_to_vec(&self) -> Result, prost::EncodeError> { + let mut buf = Vec::with_capacity(self.encoded_len()); + self.encode(&mut buf)?; + Ok(buf) + } + + /// Decode an Envelope from a Protobuf byte array (like `proto.Unmarshal` in Go). + pub fn decode_from_slice(data: &[u8]) -> Result { + Envelope::decode(data) + } +} + +#[cfg(test)] +mod tests { + use std::error::Error; + use libp2p::identity::Keypair; + use super::*; // brings `seal`, `consume_envelope`, `Record`, etc. into scope + use rand::rngs::OsRng; + use crate::handshake::record::record::Record; + use crate::handshake::record::signing::{consume_envelope, seal_record}; + + // A minimal “Record” that matches the logic in the Go test + #[derive(Default, Debug, Clone)] + struct SimpleRecord { + domain: String, + codec: Vec, + message: String, + } + + impl Record for SimpleRecord { + const DOMAIN: &'static str = "libp2p-testing"; + const CODEC: &'static [u8] = b"/libp2p/testdata"; + + fn marshal_record(&self) -> Result, Box> { + Ok(self.message.as_bytes().to_vec()) + } + fn unmarshal_record(&mut self, data: &[u8]) -> Result<(), Box> { + self.message = String::from_utf8(data.to_vec()) + .map_err(|e| format!("utf8 error: {e}"))?; + Ok(()) + } + } + + #[test] + fn test_envelope_happy_path() { + // 1. Create a new keypair for testing + let keypair = Keypair::generate_ed25519(); + + // 2. Create a record + let mut rec = SimpleRecord { + domain: "libp2p-testing".into(), + codec: b"/libp2p/testdata".to_vec(), + message: "hello world!".into(), + }; + + // 3. Seal it + let env = seal_record(&rec, &keypair).expect("seal should succeed"); + + // 4. Check envelope fields + assert_eq!(env.payload_type, rec.codec); + // domain is not stored directly in Envelope, + // but in canonical_data used for signature checking + + // 5. Serialize the Envelope to bytes + let serialized = env.encode_to_vec().unwrap(); + + // 6. Consume and verify + let (roundtrip_env, rec2) = + consume_envelope::(&serialized).expect("consume_envelope should succeed"); + + // 7. Check the payload is the same + assert_eq!(roundtrip_env.payload, env.payload, "payload mismatch"); + assert_eq!( + roundtrip_env.signature, env.signature, + "signature mismatch" + ); + + // 8. Check the domain record + assert_eq!(rec2.message, "hello world!", "unexpected message"); + } +} \ No newline at end of file diff --git a/anchor/network/src/handshake/record/mod.rs b/anchor/network/src/handshake/record/mod.rs new file mode 100644 index 000000000..3181a5176 --- /dev/null +++ b/anchor/network/src/handshake/record/mod.rs @@ -0,0 +1,3 @@ +pub mod envelope; +pub mod record; +pub mod signing; diff --git a/anchor/network/src/handshake/record/record.rs b/anchor/network/src/handshake/record/record.rs new file mode 100644 index 000000000..d6b055ddb --- /dev/null +++ b/anchor/network/src/handshake/record/record.rs @@ -0,0 +1,13 @@ +use std::error::Error; + +/// The `Record` trait parallels the idea in Go that each record knows how to: +/// - Provide a domain (for signing separation) +/// - Provide a "codec" (payload type) +/// - Marshal to bytes, unmarshal from bytes +pub trait Record { + const DOMAIN: &'static str; + const CODEC: &'static [u8]; + + fn marshal_record(&self) -> Result, Box>; + fn unmarshal_record(&mut self, data: &[u8]) -> Result<(), Box>; +} diff --git a/anchor/network/src/handshake/record/signing.rs b/anchor/network/src/handshake/record/signing.rs new file mode 100644 index 000000000..f2fbf7e40 --- /dev/null +++ b/anchor/network/src/handshake/record/signing.rs @@ -0,0 +1,87 @@ +use crate::handshake::record::envelope::Envelope; +use crate::handshake::record::record::Record; +use libp2p::identity::{Keypair, PublicKey}; +use std::error::Error; +use std::fmt::format; + +/// Seals a `Record` into an Envelope by: +/// 1) marshalling record to bytes, +/// 2) building "unsigned" data (domain + codec + payload), +/// 3) signing with ed25519, +/// 4) storing into `Envelope`. +pub fn seal_record(record: &R, keypair: &Keypair) -> Result> { + let domain = R::DOMAIN; + if domain.is_empty() { + return Err("domain must not be empty".into()); + } + let payload_type = R::CODEC; + if payload_type.is_empty() { + return Err("payload_type must not be empty".into()); + } + + // 1) marshal + let raw_payload = record.marshal_record()?; + + // 2) build the "unsigned" data + let unsigned = make_unsigned(domain.as_bytes(), payload_type, &raw_payload); + + // 3) sign + let sig = keypair.sign(&unsigned)?; + + // 4) build Envelope + let env = Envelope { + public_key: keypair.public().encode_protobuf(), + payload_type: payload_type.to_vec(), + payload: raw_payload, + signature: sig, + }; + Ok(env) +} + +/// Consumes an Envelope => verify signature => parse the record. +pub fn consume_envelope( + bytes: &[u8], +) -> Result<(Envelope, R), Box> { + let env = Envelope::decode_from_slice(bytes)?; + + let domain = R::DOMAIN; + let payload_type = R::CODEC; + + let unsigned = make_unsigned(domain.as_bytes(), payload_type, &env.payload); + + // parse the record from env.payload + let mut rec = R::default(); + rec.unmarshal_record(&env.payload)?; + + // parse pubkey + // if env.public_key.len() != 32 { + // return Err(format!("invalid ed25519 public key length: {}", env.public_key.len()).into()); + // } + //let mut pk_bytes = [0u8; 32]; + //pk_bytes.copy_from_slice(&env.public_key); + let pk = PublicKey::try_decode_protobuf(&*env.clone().public_key.to_vec()).unwrap(); + + + if !pk.verify(&unsigned, &env.signature) { + return Err("signature verification failed".into()); + } + + Ok((env, rec)) +} + +fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec { + use prost::encoding::encode_varint; + let mut out = Vec::new(); + + encode_varint(domain.len() as u64, &mut out); + out.extend_from_slice(domain); + + encode_varint(payload_type.len() as u64, &mut out); + out.extend_from_slice(payload_type); + + encode_varint(payload.len() as u64, &mut out); + out.extend_from_slice(payload); + + out +} + diff --git a/anchor/network/src/handshake/types.rs b/anchor/network/src/handshake/types.rs new file mode 100644 index 000000000..725a2a275 --- /dev/null +++ b/anchor/network/src/handshake/types.rs @@ -0,0 +1,86 @@ +use crate::handshake::record::record::Record; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::error::Error; + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +pub struct NodeMetadata { + pub node_version: String, + pub execution_node: String, + pub consensus_node: String, + pub subnets: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +pub struct NodeInfo { + fork_version: String, //deprecated + pub network_id: String, + pub metadata: NodeMetadata, +} + +impl NodeInfo { + pub fn new(network_id: String, metadata: NodeMetadata) -> Self { + NodeInfo { + fork_version: "".to_string(), + network_id, + metadata, + } + } +} + +impl Record for NodeInfo { + const DOMAIN: &'static str = "ssv"; + + const CODEC: &'static [u8] = b"ssv:nodeinfo"; + + /// Serialize `NodeInfo` to JSON bytes. + fn marshal_record(&self) -> Result, Box> { + let data = serde_json::to_vec(self)?; + Ok(data) + } + + /// Deserialize `NodeInfo` from JSON bytes, replacing `self`. + fn unmarshal_record(&mut self, data: &[u8]) -> Result<(), Box> { + let parsed: NodeInfo = serde_json::from_slice(data)?; + *self = parsed; + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum HandshakeMessage { + Request(NodeInfo), + Response(NodeInfo), +} + + +#[cfg(test)] +mod tests { + use crate::handshake::record::record::Record; + use crate::handshake::types::{NodeInfo, NodeMetadata}; + + #[test] + fn test_node_info_marshal_unmarshal() { + // Create a sample NodeInfo instance + let node_info = NodeInfo::new( + "holesky".to_string(), + NodeMetadata { + node_version: "geth/x".to_string(), + execution_node: "geth/x".to_string(), + consensus_node: "prysm/x".to_string(), + subnets: "00000000000000000000000000000000".to_string(), + }, + ); + + // Marshal the NodeInfo into bytes + let data = node_info.marshal_record().expect("Marshal failed"); + + // Unmarshal the bytes back into a NodeInfo instance + let mut parsed_node_info = NodeInfo::default(); + parsed_node_info + .unmarshal_record(&data) + .expect("Unmarshal failed"); + + assert_eq!(node_info, parsed_node_info); + } +} \ No newline at end of file diff --git a/anchor/network/src/lib.rs b/anchor/network/src/lib.rs index 49ad72200..44b096aa1 100644 --- a/anchor/network/src/lib.rs +++ b/anchor/network/src/lib.rs @@ -3,11 +3,11 @@ mod behaviour; mod config; mod discovery; +mod handshake; mod keypair_utils; mod network; mod transport; pub mod types; - pub use config::Config; pub use lighthouse_network::{ListenAddr, ListenAddress}; pub use network::Network; diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 06e7ca6b7..bc240cb02 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -1,5 +1,6 @@ use std::num::{NonZeroU8, NonZeroUsize}; use std::pin::Pin; +use std::sync::{Arc, Mutex}; use std::time::Duration; use futures::StreamExt; @@ -22,9 +23,11 @@ use crate::keypair_utils::load_private_key; use crate::transport::build_transport; use crate::Config; +use crate::handshake::behaviour::HandshakeBehaviour; use crate::types::ssv_message::SignedSSVMessage; use lighthouse_network::EnrExt; use ssz::Decode; +use crate::handshake::types::{NodeInfo, NodeMetadata}; use subnet_tracker::{SubnetEvent, SubnetId}; use tokio::sync::mpsc; @@ -157,6 +160,13 @@ impl Network { _ => { debug!(event = ?behaviour_event, "Unhandled behaviour event"); } + // HandshakeEvent::Completed { peer, their_info } => { + // info!(%peer, "Handshake completed"); + // // Update peer store with their_info + // } + // HandshakeEvent::Failed { peer, error } => { + // warn!(%peer, %error, "Handshake failed"); + // } }, // TODO handle other swarm events _ => { @@ -212,6 +222,18 @@ fn subnet_to_topic(subnet: SubnetId) -> IdentTopic { IdentTopic::new(format!("ssv.{}", *subnet)) } +// fn handle_handshake_event(ev: HandshakeEvent) { +// match ev { +// HandshakeEvent::Completed { peer, their_info } => { +// info!(%peer, "Handshake completed"); +// // Update peer store with their_info +// } +// HandshakeEvent::Failed { peer, error } => { +// warn!(%peer, %error, "Handshake failed"); +// } +// } +// } + async fn build_anchor_behaviour( local_keypair: Keypair, network_config: &Config, @@ -266,11 +288,28 @@ async fn build_anchor_behaviour( discovery }; + let domain = format!("0x{}", hex::encode(vec![0x0, 0x0, 0x5, 0x2])); + println!("Domain: {}", domain); + let node_info = NodeInfo::new( + domain, + NodeMetadata { + node_version: "1.0.0".to_string(), + execution_node: "geth/v1.10.8".to_string(), + consensus_node: "lighthouse/v1.5.0".to_string(), + subnets: "ffffffffffffffffffffffffffffffff".to_string(), + }, + ); + let handshake = HandshakeBehaviour::new( + local_keypair.clone(), + Arc::new(Mutex::new(node_info)), + ); + AnchorBehaviour { identify, ping: ping::Behaviour::default(), gossipsub, discovery, + handshake, } } diff --git a/anchor/network/src/types.rs b/anchor/network/src/types.rs index f4bf59391..e89e2db2b 100644 --- a/anchor/network/src/types.rs +++ b/anchor/network/src/types.rs @@ -1,2 +1,3 @@ mod gossip_kind; +pub(crate) mod node_info; pub mod ssv_message; diff --git a/anchor/network/src/types/node_info.rs b/anchor/network/src/types/node_info.rs new file mode 100644 index 000000000..8559bc9bf --- /dev/null +++ b/anchor/network/src/types/node_info.rs @@ -0,0 +1,123 @@ +// use serde::{Deserialize, Serialize}; +// +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct NodeMetadata { +// // NodeVersion is the ssv-node version, it is a required field +// pub node_version: String, +// // ExecutionNode is the "name/version" of the eth1 node +// pub execution_node: String, +// // ConsensusNode is the "name/version" of the beacon node +// pub consensus_node: String, +// // Subnets represents the subnets that our node is subscribed to +// pub subnets: String, +// } +// +// impl NodeMetadata { +// /// Example validation you might do: +// pub fn validate(&self) -> Result<(), String> { +// if self.subnets.len() != 5 { +// Err(format!("Invalid subnets length: got {}, expected 5", self.subnets.len())) +// } else { +// Ok(()) +// } +// } +// } +// +// /// The node info that we exchange during handshake. +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct NodeInfo { +// pub network_id: String, // e.g. "anchor-testnet" +// pub metadata: Option, +// } +// +// impl NodeInfo { +// pub fn new(network_id: impl Into) -> Self { +// Self { +// network_id: network_id.into(), +// metadata: None, +// } +// } +// +// /// Validate fields if you want to ensure subnets or network id are correct +// pub fn validate(&self) -> Result<(), String> { +// // Example: check the metadata if present +// if let Some(md) = &self.metadata { +// md.validate()?; +// } +// Ok(()) +// } +// } +// +// // ---------------------------------------------------------------------- +// // 2. An "Envelope" type (like Go’s "record.Envelope") if you want to sign +// // or store signature fields. If the snippet doesn't show them, skip. +// // Shown here just to illustrate how you might separate them. +// // ---------------------------------------------------------------------- +// +// /// If you had a signature in your real code, you'd keep it in Envelope, not in NodeInfo. +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct NodeInfoEnvelope { +// /// The raw JSON of NodeInfo (or a sealed version if you sign). +// pub data: Vec, +// } +// +// /// Convert a `NodeInfo` into a JSON "envelope" (without signature). +// impl From for NodeInfoEnvelope { +// fn from(node_info: NodeInfo) -> Self { +// let data = serde_json::to_vec(&node_info).expect("serialization cannot fail"); +// Self { data } +// } +// } +// +// /// Convert back from envelope to `NodeInfo`, doing any signature checks if needed. +// impl TryFrom for NodeInfo { +// type Error = String; +// +// fn try_from(env: NodeInfoEnvelope) -> Result { +// let info: NodeInfo = serde_json::from_slice(&env.data) +// .map_err(|e| format!("Failed to parse NodeInfo: {e}"))?; +// info.validate()?; +// Ok(info) +// } +// } +// +// // ---------------------------------------------------------------------- +// // 3. PeerInfo store, matching your snippet's "peerInfos.UpdatePeerInfo" usage +// // ---------------------------------------------------------------------- +// +// #[derive(Debug)] +// pub struct PeerInfo { +// pub last_handshake: Option, +// pub last_handshake_error: Option, +// } +// +// impl PeerInfo { +// pub fn new() -> Self { +// Self { +// last_handshake: None, +// last_handshake_error: None, +// } +// } +// } +// +// #[derive(Default)] +// pub struct PeerInfoIndex { +// inner: RwLock>, +// } +// +// impl PeerInfoIndex { +// pub fn new() -> Self { +// Self { +// inner: Default::default(), +// } +// } +// +// /// This parallels the Go code snippet's "h.updatePeerInfo(pid, err)" logic. +// /// If there's an error, store it. Otherwise mark success. +// pub fn update_peer_info(&self, peer_id: &PeerId, err: Option<&str>) { +// let mut map = self.inner.write().unwrap(); +// let pinfo = map.entry(*peer_id).or_insert(PeerInfo::new()); +// pinfo.last_handshake = Some(SystemTime::now()); +// pinfo.last_handshake_error = err.map(str::to_string); +// } +// } From 1b36113d5552fa832b0b8666a3df3ebcd50097d7 Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 5 Feb 2025 14:59:13 +0100 Subject: [PATCH 02/63] remove length prefixing --- anchor/network/src/handshake/codec.rs | 105 +++----------------------- 1 file changed, 12 insertions(+), 93 deletions(-) diff --git a/anchor/network/src/handshake/codec.rs b/anchor/network/src/handshake/codec.rs index 7fef55b6c..aa777bdda 100644 --- a/anchor/network/src/handshake/codec.rs +++ b/anchor/network/src/handshake/codec.rs @@ -10,48 +10,7 @@ use prost::encoding::{decode_varint, encode_varint, encoded_len_varint}; use prost::Message; use tracing::debug; - -/// Reads a varint‑encoded length from the async stream using the Prost decoder. -/// We read one byte at a time (maximum 10 bytes) until we see a byte with its -/// high‐bit clear. Then we call Prost’s `decode_varint` on the accumulated slice. -async fn read_varint_length(io: &mut T) -> io::Result { - // A varint is at most 10 bytes long. - let mut buf = [0u8; 10]; - let mut pos = 0; - loop { - if pos >= buf.len() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "varint too long", - )); - } - // Read one byte. - io.read_exact(&mut buf[pos..pos + 1]).await?; - // If the high-bit is clear, we have reached the end of the varint. - if buf[pos] & 0x80 == 0 { - // Create a slice containing the varint bytes. - let mut slice = &buf[..pos + 1]; - // Use Prost’s varint decoder. - let value = decode_varint(&mut slice) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - return Ok(value as usize); - } - pos += 1; - } -} - -/// Writes the given length as a varint‑encoded prefix to the async stream, -/// using Prost’s `encode_varint` function. -async fn write_varint_length(io: &mut T, value: usize) -> io::Result<()> { - let cap = encoded_len_varint(value as u64); - let mut buf = BytesMut::with_capacity(cap); - encode_varint(value as u64, &mut buf); - io.write_all(&buf).await?; - Ok(()) -} - -/// A `Codec` that reads/writes an **`Envelope`** in a length-prefixed Protobuf style: -/// - <4-byte big-endian length> +/// A `Codec` that reads/writes an **`Envelope`** #[derive(Clone, Debug, Default)] pub struct EnvelopeCodec; @@ -70,19 +29,12 @@ impl Codec for EnvelopeCodec { T: AsyncRead + Unpin + Send, { debug!("reading handsake request"); - // read length - let mut len_buf = [0u8; 4]; - io.read_exact(&mut len_buf).await?; - let msg_len = u32::from_be_bytes(len_buf) as usize; - - // read that many bytes - let mut msg_buf = vec![0u8; msg_len]; - io.read_exact(&mut msg_buf).await?; - - // decode + let mut msg_buf = Vec::new(); + let num_bytes_read = io.read_to_end(&mut msg_buf).await?; + debug!(?num_bytes_read, "read handshake request"); let env = Envelope::decode_from_slice(&msg_buf) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - debug!("read handshake request"); + debug!(?env, "decoded handshake request"); Ok(env) } @@ -94,41 +46,14 @@ impl Codec for EnvelopeCodec { where T: AsyncRead + Unpin + Send, { - // debug!("reading handshake response"); - // // same approach - // let mut len_buf = [0u8; 4]; - // io.read_exact(&mut len_buf).await?; - // let msg_len = u32::from_be_bytes(len_buf) as usize; - // - // let mut msg_buf = vec![0u8; msg_len]; - // io.read_exact(&mut msg_buf).await?; - // - // let env = Envelope::decode_from_slice(&msg_buf); - // //.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - // match env { - // Ok(env) => { - // debug!("read handshake response"); - // Ok(env) }, - // Err(e) => { - // debug!(?e, "error decoding envelope"); - // Err(io::Error::new(io::ErrorKind::InvalidData, e)) - // } - // } debug!("reading handshake response"); - match read_varint_length(io).await { - Ok(msg_len) => { - let mut msg_buf = vec![0u8; msg_len]; - io.read_exact(&mut msg_buf).await?; - let env = Envelope::decode_from_slice(&msg_buf) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - debug!("read handshake response"); - Ok(env) - } - Err(error) => { - debug!(?error, "error reading handshake response"); - Err(io::Error::new(io::ErrorKind::InvalidData, "invalid varint")) - } - } + let mut msg_buf = Vec::new(); + let num_bytes_read = io.read_to_end(&mut msg_buf).await?; + debug!(?num_bytes_read, "read handshake response"); + let env = Envelope::decode_from_slice(&msg_buf) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + debug!(?env, "decoded handshake response"); + Ok(env) } async fn write_request( @@ -142,9 +67,6 @@ impl Codec for EnvelopeCodec { { debug!(req = ?req, "writing handshake request"); let raw = req.encode_to_vec()?; - // Write the varint length prefix. - write_varint_length(io, raw.len()).await?; - // Write the message bytes. io.write_all(&raw).await?; io.close().await?; debug!("wrote handshake request"); @@ -164,9 +86,6 @@ impl Codec for EnvelopeCodec { let raw = res .encode_to_vec() .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - let len = (raw.len() as u32).to_be_bytes(); - io.write_all(&len).await?; io.write_all(&raw).await?; let r = io.close().await; debug!("wrote handshake response"); From 8dfba383effd2696af9aad7535a1ce50f8e2911f Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 5 Feb 2025 14:59:52 +0100 Subject: [PATCH 03/63] fixing serialization --- anchor/network/src/handshake/types.rs | 97 ++++++++++++++++++++++----- anchor/network/src/network.rs | 4 +- 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/anchor/network/src/handshake/types.rs b/anchor/network/src/handshake/types.rs index 725a2a275..075b0a4b5 100644 --- a/anchor/network/src/handshake/types.rs +++ b/anchor/network/src/handshake/types.rs @@ -5,23 +5,32 @@ use std::error::Error; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct NodeMetadata { + #[serde(rename = "NodeVersion")] pub node_version: String, + #[serde(rename = "ExecutionNode")] pub execution_node: String, + #[serde(rename = "ConsensusNode")] pub consensus_node: String, + #[serde(rename = "Subnets")] pub subnets: String, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct NodeInfo { - fork_version: String, //deprecated pub network_id: String, - pub metadata: NodeMetadata, + pub metadata: Option, +} + +// This is the direct Rust equivalent to your 'serializable' struct +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +struct Serializable { + #[serde(rename = "Entries")] + entries: Vec, } impl NodeInfo { - pub fn new(network_id: String, metadata: NodeMetadata) -> Self { + pub fn new(network_id: String, metadata: Option) -> Self { NodeInfo { - fork_version: "".to_string(), network_id, metadata, } @@ -35,14 +44,34 @@ impl Record for NodeInfo { /// Serialize `NodeInfo` to JSON bytes. fn marshal_record(&self) -> Result, Box> { - let data = serde_json::to_vec(self)?; + let mut entries = vec![ + "".to_string(), // formerly forkVersion, now deprecated + self.network_id.clone(), // network id + ]; + + if let Some(meta) = &self.metadata { + let raw_meta = serde_json::to_vec(meta)?; + entries.push(String::from_utf8(raw_meta)?); + } + + // Serialize as JSON + let ser = Serializable { entries }; + let data = serde_json::to_vec(&ser)?; Ok(data) } /// Deserialize `NodeInfo` from JSON bytes, replacing `self`. fn unmarshal_record(&mut self, data: &[u8]) -> Result<(), Box> { - let parsed: NodeInfo = serde_json::from_slice(data)?; - *self = parsed; + let ser: Serializable = serde_json::from_slice(data)?; + if ser.entries.len() < 2 { + return Err("node info must have at least 2 entries".into()); + } + // skip ser.entries[0]: old forkVersion + self.network_id = ser.entries[1].clone(); + if ser.entries.len() >= 3 { + let meta = serde_json::from_slice(ser.entries[2].as_bytes())?; + self.metadata = Some(meta); + } Ok(()) } } @@ -56,31 +85,67 @@ pub enum HandshakeMessage { #[cfg(test)] mod tests { + use libp2p::identity::Keypair; use crate::handshake::record::record::Record; + use crate::handshake::record::signing::{consume_envelope, seal_record}; use crate::handshake::types::{NodeInfo, NodeMetadata}; #[test] - fn test_node_info_marshal_unmarshal() { + fn test_node_info_seal_consume() { // Create a sample NodeInfo instance let node_info = NodeInfo::new( "holesky".to_string(), - NodeMetadata { + Some(NodeMetadata { node_version: "geth/x".to_string(), execution_node: "geth/x".to_string(), consensus_node: "prysm/x".to_string(), subnets: "00000000000000000000000000000000".to_string(), - }, + }), ); // Marshal the NodeInfo into bytes - let data = node_info.marshal_record().expect("Marshal failed"); + let envelope = seal_record(&node_info, &Keypair::generate_secp256k1()).expect("Seal failed"); + + let data = envelope.encode_to_vec().unwrap(); - // Unmarshal the bytes back into a NodeInfo instance - let mut parsed_node_info = NodeInfo::default(); - parsed_node_info - .unmarshal_record(&data) - .expect("Unmarshal failed"); + let (parsed_env, parsed_node_info) = consume_envelope(&data).expect("Consume failed"); assert_eq!(node_info, parsed_node_info); } + + #[test] + fn test_node_info_marshal_unmarshal() { + // The old serialized data from the Go code + // (note the "Subnets":"ffffffffffffffffffffffffffffffff") + let old_serialized_data = br#"{"Entries":["", "testnet", "{\"NodeVersion\":\"v0.1.12\",\"ExecutionNode\":\"geth/x\",\"ConsensusNode\":\"prysm/x\",\"Subnets\":\"ffffffffffffffffffffffffffffffff\"}"]}"#; + + // The "current" NodeInfo data + let current_data = NodeInfo { + network_id: "testnet".to_string(), + metadata: Some(NodeMetadata { + node_version: "v0.1.12".into(), + execution_node: "geth/x".into(), + consensus_node: "prysm/x".into(), + subnets: "ffffffffffffffffffffffffffffffff".into(), + }), + }; + + // 1) Marshal current_data + let data = current_data.marshal_record() + .expect("marshal_record should succeed"); + + // 2) Unmarshal into parsed_rec + let mut parsed_rec = NodeInfo::default(); + parsed_rec.unmarshal_record(&data) + .expect("unmarshal_record should succeed"); + + // 3) Now unmarshal the old format data into the same struct + parsed_rec.unmarshal_record(old_serialized_data) + .expect("unmarshal old data should succeed"); + + // 4) Compare + // The Go test checks reflect.DeepEqual(currentSerializedData, parsedRec) + // We can do the same in Rust using assert_eq. + assert_eq!(current_data, parsed_rec); + } } \ No newline at end of file diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index bc240cb02..e72c9445e 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -292,12 +292,12 @@ async fn build_anchor_behaviour( println!("Domain: {}", domain); let node_info = NodeInfo::new( domain, - NodeMetadata { + Some(NodeMetadata { node_version: "1.0.0".to_string(), execution_node: "geth/v1.10.8".to_string(), consensus_node: "lighthouse/v1.5.0".to_string(), subnets: "ffffffffffffffffffffffffffffffff".to_string(), - }, + }), ); let handshake = HandshakeBehaviour::new( local_keypair.clone(), From fec657934e8fb189284e996ea302c1d61de60a02 Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 5 Feb 2025 14:59:57 +0100 Subject: [PATCH 04/63] fix tag --- anchor/network/src/handshake/record/envelope.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anchor/network/src/handshake/record/envelope.rs b/anchor/network/src/handshake/record/envelope.rs index 67934aeb4..79c8f68f2 100644 --- a/anchor/network/src/handshake/record/envelope.rs +++ b/anchor/network/src/handshake/record/envelope.rs @@ -19,7 +19,7 @@ pub struct Envelope { #[prost(bytes = "vec", tag = "3")] pub payload: Vec, - #[prost(bytes = "vec", tag = "4")] + #[prost(bytes = "vec", tag = "5")] pub signature: Vec, } From 2f898bcb52bcda24686825f5e1662c2eaae428ec Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 5 Feb 2025 15:11:31 +0100 Subject: [PATCH 05/63] delete node_info from network types --- anchor/network/src/types.rs | 1 - anchor/network/src/types/node_info.rs | 123 -------------------------- 2 files changed, 124 deletions(-) delete mode 100644 anchor/network/src/types/node_info.rs diff --git a/anchor/network/src/types.rs b/anchor/network/src/types.rs index e89e2db2b..f4bf59391 100644 --- a/anchor/network/src/types.rs +++ b/anchor/network/src/types.rs @@ -1,3 +1,2 @@ mod gossip_kind; -pub(crate) mod node_info; pub mod ssv_message; diff --git a/anchor/network/src/types/node_info.rs b/anchor/network/src/types/node_info.rs deleted file mode 100644 index 8559bc9bf..000000000 --- a/anchor/network/src/types/node_info.rs +++ /dev/null @@ -1,123 +0,0 @@ -// use serde::{Deserialize, Serialize}; -// -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct NodeMetadata { -// // NodeVersion is the ssv-node version, it is a required field -// pub node_version: String, -// // ExecutionNode is the "name/version" of the eth1 node -// pub execution_node: String, -// // ConsensusNode is the "name/version" of the beacon node -// pub consensus_node: String, -// // Subnets represents the subnets that our node is subscribed to -// pub subnets: String, -// } -// -// impl NodeMetadata { -// /// Example validation you might do: -// pub fn validate(&self) -> Result<(), String> { -// if self.subnets.len() != 5 { -// Err(format!("Invalid subnets length: got {}, expected 5", self.subnets.len())) -// } else { -// Ok(()) -// } -// } -// } -// -// /// The node info that we exchange during handshake. -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct NodeInfo { -// pub network_id: String, // e.g. "anchor-testnet" -// pub metadata: Option, -// } -// -// impl NodeInfo { -// pub fn new(network_id: impl Into) -> Self { -// Self { -// network_id: network_id.into(), -// metadata: None, -// } -// } -// -// /// Validate fields if you want to ensure subnets or network id are correct -// pub fn validate(&self) -> Result<(), String> { -// // Example: check the metadata if present -// if let Some(md) = &self.metadata { -// md.validate()?; -// } -// Ok(()) -// } -// } -// -// // ---------------------------------------------------------------------- -// // 2. An "Envelope" type (like Go’s "record.Envelope") if you want to sign -// // or store signature fields. If the snippet doesn't show them, skip. -// // Shown here just to illustrate how you might separate them. -// // ---------------------------------------------------------------------- -// -// /// If you had a signature in your real code, you'd keep it in Envelope, not in NodeInfo. -// #[derive(Debug, Clone, Serialize, Deserialize)] -// pub struct NodeInfoEnvelope { -// /// The raw JSON of NodeInfo (or a sealed version if you sign). -// pub data: Vec, -// } -// -// /// Convert a `NodeInfo` into a JSON "envelope" (without signature). -// impl From for NodeInfoEnvelope { -// fn from(node_info: NodeInfo) -> Self { -// let data = serde_json::to_vec(&node_info).expect("serialization cannot fail"); -// Self { data } -// } -// } -// -// /// Convert back from envelope to `NodeInfo`, doing any signature checks if needed. -// impl TryFrom for NodeInfo { -// type Error = String; -// -// fn try_from(env: NodeInfoEnvelope) -> Result { -// let info: NodeInfo = serde_json::from_slice(&env.data) -// .map_err(|e| format!("Failed to parse NodeInfo: {e}"))?; -// info.validate()?; -// Ok(info) -// } -// } -// -// // ---------------------------------------------------------------------- -// // 3. PeerInfo store, matching your snippet's "peerInfos.UpdatePeerInfo" usage -// // ---------------------------------------------------------------------- -// -// #[derive(Debug)] -// pub struct PeerInfo { -// pub last_handshake: Option, -// pub last_handshake_error: Option, -// } -// -// impl PeerInfo { -// pub fn new() -> Self { -// Self { -// last_handshake: None, -// last_handshake_error: None, -// } -// } -// } -// -// #[derive(Default)] -// pub struct PeerInfoIndex { -// inner: RwLock>, -// } -// -// impl PeerInfoIndex { -// pub fn new() -> Self { -// Self { -// inner: Default::default(), -// } -// } -// -// /// This parallels the Go code snippet's "h.updatePeerInfo(pid, err)" logic. -// /// If there's an error, store it. Otherwise mark success. -// pub fn update_peer_info(&self, peer_id: &PeerId, err: Option<&str>) { -// let mut map = self.inner.write().unwrap(); -// let pinfo = map.entry(*peer_id).or_insert(PeerInfo::new()); -// pinfo.last_handshake = Some(SystemTime::now()); -// pinfo.last_handshake_error = err.map(str::to_string); -// } -// } From 335de8c6413a3da33bcee13c0ff744c911946265 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 16:31:49 +0100 Subject: [PATCH 06/63] remove unnecessary deps # Conflicts: # Cargo.lock # anchor/network/Cargo.toml --- Cargo.lock | 49 +++++++++++++++++++++++++++++++++++++++ anchor/network/Cargo.toml | 5 ---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55ebd75f6..f9dca6443 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4204,6 +4204,7 @@ dependencies = [ "libp2p-ping", "libp2p-plaintext", "libp2p-quic", + "libp2p-request-response", "libp2p-swarm", "libp2p-tcp", "libp2p-upnp", @@ -4500,6 +4501,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "libp2p-request-response" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1356c9e376a94a75ae830c42cdaea3d4fe1290ba409a22c809033d1b7dcab0a6" +dependencies = [ + "async-trait", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand", + "smallvec", + "tracing", + "void", + "web-time", +] + [[package]] name = "libp2p-swarm" version = "0.45.1" @@ -5141,6 +5162,7 @@ name = "network" version = "0.1.0" dependencies = [ "async-channel", + "async-trait", "dirs 6.0.0", "discv5", "ethereum_ssz 0.8.2", @@ -5149,10 +5171,14 @@ dependencies = [ "hex", "libp2p", "lighthouse_network", + "prost", + "rand", "serde", + "serde_json", "ssz_types 0.10.0", "subnet_tracker", "task_executor", + "thiserror 1.0.69", "tokio", "tracing", "types", @@ -5885,6 +5911,29 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "prost" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "proto_array" version = "0.2.0" diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index ed4263a8c..9d737a78e 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -24,7 +24,6 @@ libp2p = { version = "0.54", default-features = false, features = [ "ping", "request-response", ] } -#libp2p-request-response = "0.26.3" lighthouse_network = { workspace = true } serde = { workspace = true } ssz_types = "0.10" @@ -40,9 +39,5 @@ prost = "0.13.4" async-trait = "0.1.85" rand = "0.8.5" -# Add to parse or generate .proto -[build-dependencies] -prost-build = "0.13.4" - [dev-dependencies] async-channel = { workspace = true } From 537a988d3590f3c7575e04ee06bf3a5b21cb2b56 Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 5 Feb 2025 19:57:09 +0100 Subject: [PATCH 07/63] handle handshake event --- anchor/network/src/network.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index e72c9445e..b134e4024 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -23,7 +23,7 @@ use crate::keypair_utils::load_private_key; use crate::transport::build_transport; use crate::Config; -use crate::handshake::behaviour::HandshakeBehaviour; +use crate::handshake::behaviour::{HandshakeBehaviour, HandshakeEvent}; use crate::types::ssv_message::SignedSSVMessage; use lighthouse_network::EnrExt; use ssz::Decode; @@ -156,6 +156,9 @@ impl Network { } } } + AnchorBehaviourEvent::Handshake(ev) => { + handle_handshake_event(ev); + } // TODO handle other behaviour events _ => { debug!(event = ?behaviour_event, "Unhandled behaviour event"); @@ -222,17 +225,17 @@ fn subnet_to_topic(subnet: SubnetId) -> IdentTopic { IdentTopic::new(format!("ssv.{}", *subnet)) } -// fn handle_handshake_event(ev: HandshakeEvent) { -// match ev { -// HandshakeEvent::Completed { peer, their_info } => { -// info!(%peer, "Handshake completed"); -// // Update peer store with their_info -// } -// HandshakeEvent::Failed { peer, error } => { -// warn!(%peer, %error, "Handshake failed"); -// } -// } -// } +fn handle_handshake_event(ev: HandshakeEvent) { + match ev { + HandshakeEvent::Completed { peer, their_info } => { + debug!(%peer, "Handshake completed"); + // Update peer store with their_info + } + HandshakeEvent::Failed { peer, error } => { + debug!(%peer, %error, "Handshake failed"); + } + } +} async fn build_anchor_behaviour( local_keypair: Keypair, From 5162ae650eebf8f94e8f2576262b9806e01e5729 Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 5 Feb 2025 20:00:21 +0100 Subject: [PATCH 08/63] many fixes --- anchor/network/src/behaviour.rs | 21 +-- anchor/network/src/handshake/behaviour.rs | 159 ++++++------------ anchor/network/src/handshake/codec.rs | 7 +- .../network/src/handshake/record/envelope.rs | 13 +- .../network/src/handshake/record/signing.rs | 19 +-- anchor/network/src/handshake/types.rs | 15 +- anchor/network/src/network.rs | 7 - 7 files changed, 74 insertions(+), 167 deletions(-) diff --git a/anchor/network/src/behaviour.rs b/anchor/network/src/behaviour.rs index 7796bb972..acc8e4c5b 100644 --- a/anchor/network/src/behaviour.rs +++ b/anchor/network/src/behaviour.rs @@ -1,9 +1,8 @@ -use discv5::libp2p_identity::PeerId; use crate::discovery::Discovery; -use crate::handshake::behaviour::{HandshakeBehaviour, PeerInfo, PeerInfoStore, Subnets, SubnetsIndex}; use libp2p::request_response::Behaviour; +use crate::handshake::behaviour::HandshakeBehaviour; use libp2p::swarm::NetworkBehaviour; -use libp2p::{gossipsub, identify, ping, request_response}; +use libp2p::{gossipsub, identify, ping}; #[derive(NetworkBehaviour)] pub struct AnchorBehaviour { @@ -18,19 +17,3 @@ pub struct AnchorBehaviour { pub handshake: HandshakeBehaviour, } - -#[derive(Default)] -struct DummyPeerInfoStore {} -impl PeerInfoStore for DummyPeerInfoStore { - fn update(&self, peer: PeerId, f: impl FnOnce(&mut PeerInfo)) { - - } -} - -#[derive(Default)] -struct DummySubnetsIndex {} -impl SubnetsIndex for DummySubnetsIndex { - fn update_peer_subnets(&self, peer: PeerId, subnets: Subnets) { - - } -} \ No newline at end of file diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 72c1e702b..53453f712 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -2,9 +2,7 @@ use discv5::libp2p_identity::Keypair; use discv5::multiaddr::Multiaddr; use libp2p::core::transport::PortUse; use libp2p::core::Endpoint; -use libp2p::request_response::{ - self, Behaviour, Config, Event, OutboundRequestId, ProtocolSupport, -}; +use libp2p::request_response::{self, Behaviour, Config, Event, OutboundRequestId, ProtocolSupport, ResponseChannel}; use libp2p::swarm::{ ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, @@ -19,44 +17,17 @@ use std::time::Instant; use tracing::debug; use crate::handshake::codec::EnvelopeCodec; use crate::handshake::record::envelope::Envelope; -use crate::handshake::record::signing::{consume_envelope, seal_record}; +use crate::handshake::record::record::Record; +use crate::handshake::record::signing::{parse_envelope, seal_record}; use crate::handshake::types::NodeInfo; /// Event emitted on handshake completion or failure. #[derive(Debug)] pub enum HandshakeEvent { - Completed { peer: PeerId, info: NodeInfo }, + Completed { peer: PeerId, their_info: NodeInfo }, Failed { peer: PeerId, error: String }, } -/// Trait for updating peer information. -pub trait PeerInfoStore: Send + Sync { - fn update(&self, peer: PeerId, f: impl FnOnce(&mut PeerInfo)); -} - -/// Trait for updating peer subnets. -pub trait SubnetsIndex: Send + Sync { - fn update_peer_subnets(&self, peer: PeerId, subnets: Subnets); -} - -/// Information about a peer. -#[derive(Clone, Debug, Default)] -pub struct PeerInfo { - pub last_handshake: Option, - pub last_error: Option, -} - -/// Subnets type (example implementation). -#[derive(Clone, Debug, Default)] -pub struct Subnets; - -impl Subnets { - pub fn from_str(s: &str) -> Result> { - // Parse subnets from string - Ok(Subnets) - } -} - /// Network behaviour handling the handshake protocol. pub struct HandshakeBehaviour { /// Request-response behaviour for the handshake protocol. @@ -67,28 +38,15 @@ pub struct HandshakeBehaviour { keypair: Keypair, /// Local node's information. local_node_info: Arc>, - /// Filters to apply on received node info. - //filters: Vec Result<(), Box> + Send + Sync>>, - /// Peer info storage. - //peer_info: Arc

, - /// Subnets index. - //subnets_index: Arc, /// Events to emit. events: Vec, } -//impl HandshakeBehaviour impl HandshakeBehaviour -// where -// P: PeerInfoStore, -// S: SubnetsIndex, { pub fn new( keypair: Keypair, local_node_info: Arc>, - // peer_info: Arc

, - // subnets_index: Arc, - // filters: Vec Result<(), Box> + Send + Sync>>, ) -> Self { // NodeInfoProtocol is the protocol.ID used for handshake const NODE_INFO_PROTOCOL: &'static str = "/ssv/info/0.0.1"; @@ -101,9 +59,6 @@ impl HandshakeBehaviour pending_handshakes: HashMap::new(), keypair, local_node_info, - // filters, - // peer_info, - // subnets_index, events: Vec::new(), } } @@ -115,38 +70,44 @@ impl HandshakeBehaviour } /// Verify an incoming envelope and apply filters. - fn verify_envelope( + fn verify_node_info( &mut self, - envelope: &Envelope, + node_info: &NodeInfo, peer: PeerId, - ) -> Result> { - let (_, mut node_info) = consume_envelope::(&envelope.encode_to_vec()?)?; - - // Apply all filters - // for filter in &self.filters { - // filter(peer, &node_info)?; - // } + ) -> Result<(), Box> { - // Update peer info - // self.peer_info.update(peer, |info| { - // info.last_handshake = Some(Instant::now()); - // info.last_error = None; - // }); + if node_info.network_id != self.local_node_info.lock().unwrap().network_id { + return Err("network id mismatch".into()) + } - // Update subnets - // if let Ok(subnets) = Subnets::from_str(node_info.metadata.subnets.as_str()) { - // self.subnets_index.update_peer_subnets(peer, subnets); - // } + Ok(()) + } - Ok(node_info) + fn handle_handshake_request(&mut self, peer: PeerId, channel: ResponseChannel) { + // Handle incoming request: send response then verify + let response = self.sealed_node_record(); + match self.behaviour.send_response(channel, response.clone()) { + Ok(_) => {} + Err(e) => { + self.events.push(HandshakeEvent::Failed { + peer, + error: "error".to_string(), + }); + } + } + let mut their_info = NodeInfo::default(); + their_info.unmarshal_record(&response.payload); + match self.verify_node_info(&their_info, peer) { + Ok(_) => self.events.push(HandshakeEvent::Completed { peer, their_info }), + Err(e) => self.events.push(HandshakeEvent::Failed { + peer, + error: "error".to_string(), + }), + } } } impl NetworkBehaviour for HandshakeBehaviour -//impl NetworkBehaviour for HandshakeBehaviour -// where -// P: PeerInfoStore, -// S: SubnetsIndex, { type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; type ToSwarm = HandshakeEvent; @@ -222,25 +183,7 @@ impl NetworkBehaviour for HandshakeBehaviour }, } => { debug!("Received handshake request"); - // Handle incoming request: send response then verify - let response = self.sealed_node_record(); - match self.behaviour.send_response(channel, response) { - Ok(_) => {} - Err(e) => { - self.events.push(HandshakeEvent::Failed { - peer, - error: "error".to_string(), - }); - } - } - - match self.verify_envelope(&request, peer) { - Ok(info) => self.events.push(HandshakeEvent::Completed { peer, info }), - Err(e) => self.events.push(HandshakeEvent::Failed { - peer, - error: "error".to_string(), - }), - } + self.handle_handshake_request(peer, channel); } Event::Message { message: @@ -254,14 +197,22 @@ impl NetworkBehaviour for HandshakeBehaviour // Handle outgoing response if let Some(peer) = self.pending_handshakes.remove(&request_id) { debug!(?response, "Received handshake response"); - match self.verify_envelope(&response, peer) { - Ok(info) => { - self.events.push(HandshakeEvent::Completed { peer, info }) + + let mut their_info = NodeInfo::default(); + their_info.unmarshal_record(&response.payload); + + match self.verify_node_info(&their_info, peer) { + Ok(_) => { + debug!(?their_info, "Handshake completed"); + self.events.push(HandshakeEvent::Completed { peer, their_info }) } - Err(e) => self.events.push(HandshakeEvent::Failed { - peer, - error: "error".to_string(), - }), + Err(e) => { + self.events.push(HandshakeEvent::Failed { + peer, + error: "error".to_string(), + }); + debug!(?e, "Handshake failed"); + }, } } } @@ -287,18 +238,6 @@ impl NetworkBehaviour for HandshakeBehaviour } _ => {} }, - ToSwarm::Dial { opts } => return Poll::Ready(ToSwarm::Dial { opts }), - ToSwarm::NotifyHandler { - peer_id, - handler, - event, - } => { - return Poll::Ready(ToSwarm::NotifyHandler { - peer_id, - handler, - event, - }); - } _ => {} } } diff --git a/anchor/network/src/handshake/codec.rs b/anchor/network/src/handshake/codec.rs index aa777bdda..3ef624d25 100644 --- a/anchor/network/src/handshake/codec.rs +++ b/anchor/network/src/handshake/codec.rs @@ -9,6 +9,8 @@ use prost::bytes::BytesMut; use prost::encoding::{decode_varint, encode_varint, encoded_len_varint}; use prost::Message; use tracing::debug; +use crate::handshake::record::signing::parse_envelope; +use crate::handshake::types::NodeInfo; /// A `Codec` that reads/writes an **`Envelope`** #[derive(Clone, Debug, Default)] @@ -50,8 +52,9 @@ impl Codec for EnvelopeCodec { let mut msg_buf = Vec::new(); let num_bytes_read = io.read_to_end(&mut msg_buf).await?; debug!(?num_bytes_read, "read handshake response"); - let env = Envelope::decode_from_slice(&msg_buf) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + let env = parse_envelope::(&msg_buf).unwrap(); + debug!(?env, "decoded handshake response"); Ok(env) } diff --git a/anchor/network/src/handshake/record/envelope.rs b/anchor/network/src/handshake/record/envelope.rs index 79c8f68f2..d596ddd75 100644 --- a/anchor/network/src/handshake/record/envelope.rs +++ b/anchor/network/src/handshake/record/envelope.rs @@ -44,7 +44,7 @@ mod tests { use super::*; // brings `seal`, `consume_envelope`, `Record`, etc. into scope use rand::rngs::OsRng; use crate::handshake::record::record::Record; - use crate::handshake::record::signing::{consume_envelope, seal_record}; + use crate::handshake::record::signing::{parse_envelope, seal_record}; // A minimal “Record” that matches the logic in the Go test #[derive(Default, Debug, Clone)] @@ -92,8 +92,8 @@ mod tests { let serialized = env.encode_to_vec().unwrap(); // 6. Consume and verify - let (roundtrip_env, rec2) = - consume_envelope::(&serialized).expect("consume_envelope should succeed"); + let roundtrip_env = + parse_envelope::(&serialized).expect("consume_envelope should succeed"); // 7. Check the payload is the same assert_eq!(roundtrip_env.payload, env.payload, "payload mismatch"); @@ -102,7 +102,12 @@ mod tests { "signature mismatch" ); + let mut parsed_rec = SimpleRecord::default(); + parsed_rec + .unmarshal_record(&roundtrip_env.payload) + .expect("unmarshal_record should succeed"); + // 8. Check the domain record - assert_eq!(rec2.message, "hello world!", "unexpected message"); + assert_eq!(parsed_rec.message, "hello world!", "unexpected message"); } } \ No newline at end of file diff --git a/anchor/network/src/handshake/record/signing.rs b/anchor/network/src/handshake/record/signing.rs index f2fbf7e40..2bd80f2a1 100644 --- a/anchor/network/src/handshake/record/signing.rs +++ b/anchor/network/src/handshake/record/signing.rs @@ -39,9 +39,9 @@ pub fn seal_record(record: &R, keypair: &Keypair) -> Result verify signature => parse the record. -pub fn consume_envelope( +pub fn parse_envelope( bytes: &[u8], -) -> Result<(Envelope, R), Box> { +) -> Result<(Envelope), Box> { let env = Envelope::decode_from_slice(bytes)?; let domain = R::DOMAIN; @@ -49,24 +49,13 @@ pub fn consume_envelope( let unsigned = make_unsigned(domain.as_bytes(), payload_type, &env.payload); - // parse the record from env.payload - let mut rec = R::default(); - rec.unmarshal_record(&env.payload)?; - - // parse pubkey - // if env.public_key.len() != 32 { - // return Err(format!("invalid ed25519 public key length: {}", env.public_key.len()).into()); - // } - //let mut pk_bytes = [0u8; 32]; - //pk_bytes.copy_from_slice(&env.public_key); - let pk = PublicKey::try_decode_protobuf(&*env.clone().public_key.to_vec()).unwrap(); - + let pk = PublicKey::try_decode_protobuf(&*env.public_key.to_vec()).unwrap(); if !pk.verify(&unsigned, &env.signature) { return Err("signature verification failed".into()); } - Ok((env, rec)) + Ok(env) } fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec { diff --git a/anchor/network/src/handshake/types.rs b/anchor/network/src/handshake/types.rs index 075b0a4b5..95c8cd960 100644 --- a/anchor/network/src/handshake/types.rs +++ b/anchor/network/src/handshake/types.rs @@ -40,7 +40,7 @@ impl NodeInfo { impl Record for NodeInfo { const DOMAIN: &'static str = "ssv"; - const CODEC: &'static [u8] = b"ssv:nodeinfo"; + const CODEC: &'static [u8] = b"ssv/nodeinfo"; /// Serialize `NodeInfo` to JSON bytes. fn marshal_record(&self) -> Result, Box> { @@ -76,18 +76,11 @@ impl Record for NodeInfo { } } -#[derive(Serialize, Deserialize, Debug)] -pub enum HandshakeMessage { - Request(NodeInfo), - Response(NodeInfo), -} - - #[cfg(test)] mod tests { use libp2p::identity::Keypair; use crate::handshake::record::record::Record; - use crate::handshake::record::signing::{consume_envelope, seal_record}; + use crate::handshake::record::signing::{parse_envelope, seal_record}; use crate::handshake::types::{NodeInfo, NodeMetadata}; #[test] @@ -108,7 +101,9 @@ mod tests { let data = envelope.encode_to_vec().unwrap(); - let (parsed_env, parsed_node_info) = consume_envelope(&data).expect("Consume failed"); + let parsed_env = parse_envelope::(&data).expect("Consume failed"); + let mut parsed_node_info = NodeInfo::default(); + parsed_node_info.unmarshal_record(&parsed_env.payload).expect("TODO: panic message"); assert_eq!(node_info, parsed_node_info); } diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index b134e4024..ceccc69fc 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -163,13 +163,6 @@ impl Network { _ => { debug!(event = ?behaviour_event, "Unhandled behaviour event"); } - // HandshakeEvent::Completed { peer, their_info } => { - // info!(%peer, "Handshake completed"); - // // Update peer store with their_info - // } - // HandshakeEvent::Failed { peer, error } => { - // warn!(%peer, %error, "Handshake failed"); - // } }, // TODO handle other swarm events _ => { From 60c1c13dbc0e35688cae54abbeb3ba3dc4ff2f43 Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 5 Feb 2025 22:58:18 +0100 Subject: [PATCH 09/63] remove record --- anchor/network/src/handshake/behaviour.rs | 50 +++++++------ anchor/network/src/handshake/codec.rs | 2 +- .../network/src/handshake/record/envelope.rs | 75 ------------------- anchor/network/src/handshake/record/mod.rs | 1 - anchor/network/src/handshake/record/record.rs | 13 ---- .../network/src/handshake/record/signing.rs | 44 ++--------- anchor/network/src/handshake/types.rs | 55 +++++++++++--- 7 files changed, 76 insertions(+), 164 deletions(-) delete mode 100644 anchor/network/src/handshake/record/record.rs diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 53453f712..16883e51b 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -17,8 +17,7 @@ use std::time::Instant; use tracing::debug; use crate::handshake::codec::EnvelopeCodec; use crate::handshake::record::envelope::Envelope; -use crate::handshake::record::record::Record; -use crate::handshake::record::signing::{parse_envelope, seal_record}; +use crate::handshake::record::signing::{parse_envelope}; use crate::handshake::types::NodeInfo; /// Event emitted on handshake completion or failure. @@ -66,7 +65,7 @@ impl HandshakeBehaviour /// Create a signed envelope containing local node info. fn sealed_node_record(&self) -> Envelope { let node_info = self.local_node_info.lock().unwrap().clone(); - seal_record(&node_info, &self.keypair).unwrap() + node_info.seal(&self.keypair).unwrap() } /// Verify an incoming envelope and apply filters. @@ -105,6 +104,28 @@ impl HandshakeBehaviour }), } } + + fn handle_handshake_response(&mut self, request_id: &OutboundRequestId, response: &Envelope) { + // Handle outgoing response + if let Some(peer) = self.pending_handshakes.remove(&request_id) { + let mut their_info = NodeInfo::default(); + their_info.unmarshal_record(&response.payload); + + match self.verify_node_info(&their_info, peer) { + Ok(_) => { + debug!(?their_info, "Handshake completed"); + self.events.push(HandshakeEvent::Completed { peer, their_info }) + } + Err(e) => { + self.events.push(HandshakeEvent::Failed { + peer, + error: "error".to_string(), + }); + debug!(?e, "Handshake failed"); + }, + } + } + } } impl NetworkBehaviour for HandshakeBehaviour @@ -194,27 +215,8 @@ impl NetworkBehaviour for HandshakeBehaviour }, .. } => { - // Handle outgoing response - if let Some(peer) = self.pending_handshakes.remove(&request_id) { - debug!(?response, "Received handshake response"); - - let mut their_info = NodeInfo::default(); - their_info.unmarshal_record(&response.payload); - - match self.verify_node_info(&their_info, peer) { - Ok(_) => { - debug!(?their_info, "Handshake completed"); - self.events.push(HandshakeEvent::Completed { peer, their_info }) - } - Err(e) => { - self.events.push(HandshakeEvent::Failed { - peer, - error: "error".to_string(), - }); - debug!(?e, "Handshake failed"); - }, - } - } + debug!(?response, "Received handshake response"); + self.handle_handshake_response(&request_id, &response); } Event::OutboundFailure { request_id, diff --git a/anchor/network/src/handshake/codec.rs b/anchor/network/src/handshake/codec.rs index 3ef624d25..f1097d685 100644 --- a/anchor/network/src/handshake/codec.rs +++ b/anchor/network/src/handshake/codec.rs @@ -53,7 +53,7 @@ impl Codec for EnvelopeCodec { let num_bytes_read = io.read_to_end(&mut msg_buf).await?; debug!(?num_bytes_read, "read handshake response"); - let env = parse_envelope::(&msg_buf).unwrap(); + let env = parse_envelope(&msg_buf).unwrap(); debug!(?env, "decoded handshake response"); Ok(env) diff --git a/anchor/network/src/handshake/record/envelope.rs b/anchor/network/src/handshake/record/envelope.rs index d596ddd75..8b835e012 100644 --- a/anchor/network/src/handshake/record/envelope.rs +++ b/anchor/network/src/handshake/record/envelope.rs @@ -35,79 +35,4 @@ impl Envelope { pub fn decode_from_slice(data: &[u8]) -> Result { Envelope::decode(data) } -} - -#[cfg(test)] -mod tests { - use std::error::Error; - use libp2p::identity::Keypair; - use super::*; // brings `seal`, `consume_envelope`, `Record`, etc. into scope - use rand::rngs::OsRng; - use crate::handshake::record::record::Record; - use crate::handshake::record::signing::{parse_envelope, seal_record}; - - // A minimal “Record” that matches the logic in the Go test - #[derive(Default, Debug, Clone)] - struct SimpleRecord { - domain: String, - codec: Vec, - message: String, - } - - impl Record for SimpleRecord { - const DOMAIN: &'static str = "libp2p-testing"; - const CODEC: &'static [u8] = b"/libp2p/testdata"; - - fn marshal_record(&self) -> Result, Box> { - Ok(self.message.as_bytes().to_vec()) - } - fn unmarshal_record(&mut self, data: &[u8]) -> Result<(), Box> { - self.message = String::from_utf8(data.to_vec()) - .map_err(|e| format!("utf8 error: {e}"))?; - Ok(()) - } - } - - #[test] - fn test_envelope_happy_path() { - // 1. Create a new keypair for testing - let keypair = Keypair::generate_ed25519(); - - // 2. Create a record - let mut rec = SimpleRecord { - domain: "libp2p-testing".into(), - codec: b"/libp2p/testdata".to_vec(), - message: "hello world!".into(), - }; - - // 3. Seal it - let env = seal_record(&rec, &keypair).expect("seal should succeed"); - - // 4. Check envelope fields - assert_eq!(env.payload_type, rec.codec); - // domain is not stored directly in Envelope, - // but in canonical_data used for signature checking - - // 5. Serialize the Envelope to bytes - let serialized = env.encode_to_vec().unwrap(); - - // 6. Consume and verify - let roundtrip_env = - parse_envelope::(&serialized).expect("consume_envelope should succeed"); - - // 7. Check the payload is the same - assert_eq!(roundtrip_env.payload, env.payload, "payload mismatch"); - assert_eq!( - roundtrip_env.signature, env.signature, - "signature mismatch" - ); - - let mut parsed_rec = SimpleRecord::default(); - parsed_rec - .unmarshal_record(&roundtrip_env.payload) - .expect("unmarshal_record should succeed"); - - // 8. Check the domain record - assert_eq!(parsed_rec.message, "hello world!", "unexpected message"); - } } \ No newline at end of file diff --git a/anchor/network/src/handshake/record/mod.rs b/anchor/network/src/handshake/record/mod.rs index 3181a5176..b588ec757 100644 --- a/anchor/network/src/handshake/record/mod.rs +++ b/anchor/network/src/handshake/record/mod.rs @@ -1,3 +1,2 @@ pub mod envelope; -pub mod record; pub mod signing; diff --git a/anchor/network/src/handshake/record/record.rs b/anchor/network/src/handshake/record/record.rs deleted file mode 100644 index d6b055ddb..000000000 --- a/anchor/network/src/handshake/record/record.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::error::Error; - -/// The `Record` trait parallels the idea in Go that each record knows how to: -/// - Provide a domain (for signing separation) -/// - Provide a "codec" (payload type) -/// - Marshal to bytes, unmarshal from bytes -pub trait Record { - const DOMAIN: &'static str; - const CODEC: &'static [u8]; - - fn marshal_record(&self) -> Result, Box>; - fn unmarshal_record(&mut self, data: &[u8]) -> Result<(), Box>; -} diff --git a/anchor/network/src/handshake/record/signing.rs b/anchor/network/src/handshake/record/signing.rs index 2bd80f2a1..0af3e370f 100644 --- a/anchor/network/src/handshake/record/signing.rs +++ b/anchor/network/src/handshake/record/signing.rs @@ -1,51 +1,17 @@ use crate::handshake::record::envelope::Envelope; -use crate::handshake::record::record::Record; use libp2p::identity::{Keypair, PublicKey}; use std::error::Error; use std::fmt::format; - -/// Seals a `Record` into an Envelope by: -/// 1) marshalling record to bytes, -/// 2) building "unsigned" data (domain + codec + payload), -/// 3) signing with ed25519, -/// 4) storing into `Envelope`. -pub fn seal_record(record: &R, keypair: &Keypair) -> Result> { - let domain = R::DOMAIN; - if domain.is_empty() { - return Err("domain must not be empty".into()); - } - let payload_type = R::CODEC; - if payload_type.is_empty() { - return Err("payload_type must not be empty".into()); - } - - // 1) marshal - let raw_payload = record.marshal_record()?; - - // 2) build the "unsigned" data - let unsigned = make_unsigned(domain.as_bytes(), payload_type, &raw_payload); - - // 3) sign - let sig = keypair.sign(&unsigned)?; - - // 4) build Envelope - let env = Envelope { - public_key: keypair.public().encode_protobuf(), - payload_type: payload_type.to_vec(), - payload: raw_payload, - signature: sig, - }; - Ok(env) -} +use crate::handshake::types::NodeInfo; /// Consumes an Envelope => verify signature => parse the record. -pub fn parse_envelope( +pub fn parse_envelope( bytes: &[u8], ) -> Result<(Envelope), Box> { let env = Envelope::decode_from_slice(bytes)?; - let domain = R::DOMAIN; - let payload_type = R::CODEC; + let domain = NodeInfo::DOMAIN; + let payload_type = NodeInfo::CODEC; let unsigned = make_unsigned(domain.as_bytes(), payload_type, &env.payload); @@ -58,7 +24,7 @@ pub fn parse_envelope( Ok(env) } -fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec { +pub fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec { use prost::encoding::encode_varint; let mut out = Vec::new(); diff --git a/anchor/network/src/handshake/types.rs b/anchor/network/src/handshake/types.rs index 95c8cd960..b3342082a 100644 --- a/anchor/network/src/handshake/types.rs +++ b/anchor/network/src/handshake/types.rs @@ -1,7 +1,9 @@ -use crate::handshake::record::record::Record; use serde::{Deserialize, Serialize}; use serde_json; use std::error::Error; +use discv5::libp2p_identity::Keypair; +use crate::handshake::record::envelope::Envelope; +use crate::handshake::record::signing::make_unsigned; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct NodeMetadata { @@ -21,7 +23,7 @@ pub struct NodeInfo { pub metadata: Option, } -// This is the direct Rust equivalent to your 'serializable' struct +// This is the direct Rust equivalent to Go 'serializable' struct #[derive(Debug, Clone, Serialize, Deserialize, Default)] struct Serializable { #[serde(rename = "Entries")] @@ -35,12 +37,10 @@ impl NodeInfo { metadata, } } -} -impl Record for NodeInfo { - const DOMAIN: &'static str = "ssv"; + pub(crate) const DOMAIN: &'static str = "ssv"; - const CODEC: &'static [u8] = b"ssv/nodeinfo"; + pub(crate) const CODEC: &'static [u8] = b"ssv/nodeinfo"; /// Serialize `NodeInfo` to JSON bytes. fn marshal_record(&self) -> Result, Box> { @@ -61,7 +61,7 @@ impl Record for NodeInfo { } /// Deserialize `NodeInfo` from JSON bytes, replacing `self`. - fn unmarshal_record(&mut self, data: &[u8]) -> Result<(), Box> { + pub fn unmarshal_record(&mut self, data: &[u8]) -> Result<(), Box> { let ser: Serializable = serde_json::from_slice(data)?; if ser.entries.len() < 2 { return Err("node info must have at least 2 entries".into()); @@ -74,13 +74,46 @@ impl Record for NodeInfo { } Ok(()) } + + /// Seals a `Record` into an Envelope by: + /// 1) marshalling record to bytes, + /// 2) building "unsigned" data (domain + codec + payload), + /// 3) signing with ed25519, + /// 4) storing into `Envelope`. + pub fn seal(&self, keypair: &Keypair) -> Result> { + let domain = Self::DOMAIN; + if domain.is_empty() { + return Err("domain must not be empty".into()); + } + let payload_type = Self::CODEC; + if payload_type.is_empty() { + return Err("payload_type must not be empty".into()); + } + + // 1) marshal + let raw_payload = self.marshal_record()?; + + // 2) build the "unsigned" data + let unsigned = make_unsigned(domain.as_bytes(), payload_type, &raw_payload); + + // 3) sign + let sig = keypair.sign(&unsigned)?; + + // 4) build Envelope + let env = Envelope { + public_key: keypair.public().encode_protobuf(), + payload_type: payload_type.to_vec(), + payload: raw_payload, + signature: sig, + }; + Ok(env) + } } #[cfg(test)] mod tests { use libp2p::identity::Keypair; - use crate::handshake::record::record::Record; - use crate::handshake::record::signing::{parse_envelope, seal_record}; + use crate::handshake::record::signing::{parse_envelope}; use crate::handshake::types::{NodeInfo, NodeMetadata}; #[test] @@ -97,11 +130,11 @@ mod tests { ); // Marshal the NodeInfo into bytes - let envelope = seal_record(&node_info, &Keypair::generate_secp256k1()).expect("Seal failed"); + let envelope = node_info.seal(&Keypair::generate_secp256k1()).expect("Seal failed"); let data = envelope.encode_to_vec().unwrap(); - let parsed_env = parse_envelope::(&data).expect("Consume failed"); + let parsed_env = parse_envelope(&data).expect("Consume failed"); let mut parsed_node_info = NodeInfo::default(); parsed_node_info.unmarshal_record(&parsed_env.payload).expect("TODO: panic message"); From 98b97bc9428f13b5f9841da59fe901be80fd400b Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 6 Feb 2025 11:14:45 +0100 Subject: [PATCH 10/63] delete record mod and move things around --- anchor/network/src/handshake/behaviour.rs | 3 +- anchor/network/src/handshake/codec.rs | 3 +- anchor/network/src/handshake/envelope.rs | 77 +++++++++++++++++++ anchor/network/src/handshake/mod.rs | 2 +- .../network/src/handshake/record/envelope.rs | 38 --------- anchor/network/src/handshake/record/mod.rs | 2 - .../network/src/handshake/record/signing.rs | 42 ---------- anchor/network/src/handshake/types.rs | 5 +- 8 files changed, 82 insertions(+), 90 deletions(-) create mode 100644 anchor/network/src/handshake/envelope.rs delete mode 100644 anchor/network/src/handshake/record/envelope.rs delete mode 100644 anchor/network/src/handshake/record/mod.rs delete mode 100644 anchor/network/src/handshake/record/signing.rs diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 16883e51b..7e3ad0e57 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -16,8 +16,7 @@ use std::task::{Context, Poll}; use std::time::Instant; use tracing::debug; use crate::handshake::codec::EnvelopeCodec; -use crate::handshake::record::envelope::Envelope; -use crate::handshake::record::signing::{parse_envelope}; +use crate::handshake::envelope::Envelope; use crate::handshake::types::NodeInfo; /// Event emitted on handshake completion or failure. diff --git a/anchor/network/src/handshake/codec.rs b/anchor/network/src/handshake/codec.rs index f1097d685..05c079475 100644 --- a/anchor/network/src/handshake/codec.rs +++ b/anchor/network/src/handshake/codec.rs @@ -1,4 +1,4 @@ -use crate::handshake::record::envelope::Envelope; +use crate::handshake::envelope::{parse_envelope, Envelope}; use futures::{AsyncReadExt, AsyncWriteExt}; use libp2p::futures::{AsyncRead, AsyncWrite}; use libp2p::request_response::Codec; @@ -9,7 +9,6 @@ use prost::bytes::BytesMut; use prost::encoding::{decode_varint, encode_varint, encoded_len_varint}; use prost::Message; use tracing::debug; -use crate::handshake::record::signing::parse_envelope; use crate::handshake::types::NodeInfo; /// A `Codec` that reads/writes an **`Envelope`** diff --git a/anchor/network/src/handshake/envelope.rs b/anchor/network/src/handshake/envelope.rs new file mode 100644 index 000000000..7cdad45a9 --- /dev/null +++ b/anchor/network/src/handshake/envelope.rs @@ -0,0 +1,77 @@ +use std::error::Error; +use discv5::libp2p_identity::PublicKey; +use prost::Message; +use strum::Display; +use crate::handshake::types::NodeInfo; + +/// The Envelope structure exactly matching Go's Envelope fields and tags: +/// 1 => public_key +/// 2 => payload_type +/// 3 => payload +/// 4 => signature +/// +/// All are `bytes`, just like in Go. +#[derive(Clone, PartialEq, Message)] +pub struct Envelope { + #[prost(bytes = "vec", tag = "1")] + pub public_key: Vec, + + #[prost(bytes = "vec", tag = "2")] + pub payload_type: Vec, + + #[prost(bytes = "vec", tag = "3")] + pub payload: Vec, + + #[prost(bytes = "vec", tag = "5")] + pub signature: Vec, +} + +impl Envelope { + /// Encode the Envelope to a Protobuf byte array (like `proto.Marshal` in Go). + pub fn encode_to_vec(&self) -> Result, prost::EncodeError> { + let mut buf = Vec::with_capacity(self.encoded_len()); + self.encode(&mut buf)?; + Ok(buf) + } + + /// Decode an Envelope from a Protobuf byte array (like `proto.Unmarshal` in Go). + pub fn decode_from_slice(data: &[u8]) -> Result { + Envelope::decode(data) + } +} + +/// Consumes an Envelope => verify signature => parse the record. +pub fn parse_envelope( + bytes: &[u8], +) -> Result<(Envelope), Box> { + let env = Envelope::decode_from_slice(bytes)?; + + let domain = NodeInfo::DOMAIN; + let payload_type = NodeInfo::CODEC; + + let unsigned = make_unsigned(domain.as_bytes(), payload_type, &env.payload); + + let pk = PublicKey::try_decode_protobuf(&*env.public_key.to_vec()).unwrap(); + + if !pk.verify(&unsigned, &env.signature) { + return Err("signature verification failed".into()); + } + + Ok(env) +} + +pub fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec { + use prost::encoding::encode_varint; + let mut out = Vec::new(); + + encode_varint(domain.len() as u64, &mut out); + out.extend_from_slice(domain); + + encode_varint(payload_type.len() as u64, &mut out); + out.extend_from_slice(payload_type); + + encode_varint(payload.len() as u64, &mut out); + out.extend_from_slice(payload); + + out +} diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 6761b4411..6df5bb127 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -1,4 +1,4 @@ pub mod behaviour; mod codec; -pub mod record; pub mod types; +pub mod envelope; diff --git a/anchor/network/src/handshake/record/envelope.rs b/anchor/network/src/handshake/record/envelope.rs deleted file mode 100644 index 8b835e012..000000000 --- a/anchor/network/src/handshake/record/envelope.rs +++ /dev/null @@ -1,38 +0,0 @@ -use prost::Message; -use strum::Display; - -/// The Envelope structure exactly matching Go's Envelope fields and tags: -/// 1 => public_key -/// 2 => payload_type -/// 3 => payload -/// 4 => signature -/// -/// All are `bytes`, just like in Go. -#[derive(Clone, PartialEq, Message)] -pub struct Envelope { - #[prost(bytes = "vec", tag = "1")] - pub public_key: Vec, - - #[prost(bytes = "vec", tag = "2")] - pub payload_type: Vec, - - #[prost(bytes = "vec", tag = "3")] - pub payload: Vec, - - #[prost(bytes = "vec", tag = "5")] - pub signature: Vec, -} - -impl Envelope { - /// Encode the Envelope to a Protobuf byte array (like `proto.Marshal` in Go). - pub fn encode_to_vec(&self) -> Result, prost::EncodeError> { - let mut buf = Vec::with_capacity(self.encoded_len()); - self.encode(&mut buf)?; - Ok(buf) - } - - /// Decode an Envelope from a Protobuf byte array (like `proto.Unmarshal` in Go). - pub fn decode_from_slice(data: &[u8]) -> Result { - Envelope::decode(data) - } -} \ No newline at end of file diff --git a/anchor/network/src/handshake/record/mod.rs b/anchor/network/src/handshake/record/mod.rs deleted file mode 100644 index b588ec757..000000000 --- a/anchor/network/src/handshake/record/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod envelope; -pub mod signing; diff --git a/anchor/network/src/handshake/record/signing.rs b/anchor/network/src/handshake/record/signing.rs deleted file mode 100644 index 0af3e370f..000000000 --- a/anchor/network/src/handshake/record/signing.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::handshake::record::envelope::Envelope; -use libp2p::identity::{Keypair, PublicKey}; -use std::error::Error; -use std::fmt::format; -use crate::handshake::types::NodeInfo; - -/// Consumes an Envelope => verify signature => parse the record. -pub fn parse_envelope( - bytes: &[u8], -) -> Result<(Envelope), Box> { - let env = Envelope::decode_from_slice(bytes)?; - - let domain = NodeInfo::DOMAIN; - let payload_type = NodeInfo::CODEC; - - let unsigned = make_unsigned(domain.as_bytes(), payload_type, &env.payload); - - let pk = PublicKey::try_decode_protobuf(&*env.public_key.to_vec()).unwrap(); - - if !pk.verify(&unsigned, &env.signature) { - return Err("signature verification failed".into()); - } - - Ok(env) -} - -pub fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec { - use prost::encoding::encode_varint; - let mut out = Vec::new(); - - encode_varint(domain.len() as u64, &mut out); - out.extend_from_slice(domain); - - encode_varint(payload_type.len() as u64, &mut out); - out.extend_from_slice(payload_type); - - encode_varint(payload.len() as u64, &mut out); - out.extend_from_slice(payload); - - out -} - diff --git a/anchor/network/src/handshake/types.rs b/anchor/network/src/handshake/types.rs index b3342082a..708a71fa4 100644 --- a/anchor/network/src/handshake/types.rs +++ b/anchor/network/src/handshake/types.rs @@ -2,8 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_json; use std::error::Error; use discv5::libp2p_identity::Keypair; -use crate::handshake::record::envelope::Envelope; -use crate::handshake::record::signing::make_unsigned; +use crate::handshake::envelope::{make_unsigned, Envelope}; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct NodeMetadata { @@ -113,7 +112,7 @@ impl NodeInfo { #[cfg(test)] mod tests { use libp2p::identity::Keypair; - use crate::handshake::record::signing::{parse_envelope}; + use crate::handshake::envelope::parse_envelope; use crate::handshake::types::{NodeInfo, NodeMetadata}; #[test] From 1d41a3e47667794dacc57a7da240c59ba6bf899c Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 6 Feb 2025 15:09:49 +0100 Subject: [PATCH 11/63] improve error handling --- anchor/network/src/handshake/behaviour.rs | 86 ++++++++++------------- anchor/network/src/handshake/error.rs | 19 +++-- anchor/network/src/handshake/mod.rs | 1 + anchor/network/src/handshake/types.rs | 29 +++++--- anchor/network/src/network.rs | 8 +-- 5 files changed, 72 insertions(+), 71 deletions(-) diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 7e3ad0e57..1e110e2bd 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -17,13 +17,14 @@ use std::time::Instant; use tracing::debug; use crate::handshake::codec::EnvelopeCodec; use crate::handshake::envelope::Envelope; +use crate::handshake::error::HandshakeError; use crate::handshake::types::NodeInfo; /// Event emitted on handshake completion or failure. #[derive(Debug)] pub enum HandshakeEvent { - Completed { peer: PeerId, their_info: NodeInfo }, - Failed { peer: PeerId, error: String }, + Completed { peer_id: PeerId, their_info: NodeInfo }, + Failed { peer_id: PeerId, error: HandshakeError }, } /// Network behaviour handling the handshake protocol. @@ -72,57 +73,47 @@ impl HandshakeBehaviour &mut self, node_info: &NodeInfo, peer: PeerId, - ) -> Result<(), Box> { - - if node_info.network_id != self.local_node_info.lock().unwrap().network_id { - return Err("network id mismatch".into()) + ) -> Result<(), HandshakeError> { + let theirs = self.local_node_info.lock().unwrap().network_id.clone(); + if node_info.network_id != *theirs { + return Err(HandshakeError::NetworkMismatch { ours: node_info.network_id.clone(), theirs }) } - Ok(()) } - fn handle_handshake_request(&mut self, peer: PeerId, channel: ResponseChannel) { + fn handle_handshake_request(&mut self, peer_id: PeerId, request: Envelope, channel: ResponseChannel) { // Handle incoming request: send response then verify let response = self.sealed_node_record(); match self.behaviour.send_response(channel, response.clone()) { - Ok(_) => {} + Ok(_) => { + self.unmarshall_and_verify(peer_id, &response); + } Err(e) => { - self.events.push(HandshakeEvent::Failed { - peer, - error: "error".to_string(), - }); + // There was an error sending the response. The InboundFailure handler will be called } } + } + + fn handle_handshake_response(&mut self, peer_id: PeerId, request_id: &OutboundRequestId, response: &Envelope) { + self.unmarshall_and_verify(peer_id, &response); + } + + fn unmarshall_and_verify(&mut self, peer_id: PeerId, response: &Envelope) { let mut their_info = NodeInfo::default(); - their_info.unmarshal_record(&response.payload); - match self.verify_node_info(&their_info, peer) { - Ok(_) => self.events.push(HandshakeEvent::Completed { peer, their_info }), - Err(e) => self.events.push(HandshakeEvent::Failed { - peer, - error: "error".to_string(), - }), + + if let Err(e) = their_info.unmarshal(&response.payload) { + self.events.push(HandshakeEvent::Failed { + peer_id, + error: HandshakeError::UnmarshalError(e), + }); } - } - fn handle_handshake_response(&mut self, request_id: &OutboundRequestId, response: &Envelope) { - // Handle outgoing response - if let Some(peer) = self.pending_handshakes.remove(&request_id) { - let mut their_info = NodeInfo::default(); - their_info.unmarshal_record(&response.payload); - - match self.verify_node_info(&their_info, peer) { - Ok(_) => { - debug!(?their_info, "Handshake completed"); - self.events.push(HandshakeEvent::Completed { peer, their_info }) - } - Err(e) => { - self.events.push(HandshakeEvent::Failed { - peer, - error: "error".to_string(), - }); - debug!(?e, "Handshake failed"); - }, - } + match self.verify_node_info(&their_info, peer_id) { + Ok(_) => self.events.push(HandshakeEvent::Completed { peer_id, their_info }), + Err(e) => self.events.push(HandshakeEvent::Failed { + peer_id, + error: e, + }), } } } @@ -203,19 +194,19 @@ impl NetworkBehaviour for HandshakeBehaviour }, } => { debug!("Received handshake request"); - self.handle_handshake_request(peer, channel); + self.handle_handshake_request(peer, request, channel); } Event::Message { + peer, message: request_response::Message::Response { request_id, response, .. }, - .. } => { debug!(?response, "Received handshake response"); - self.handle_handshake_response(&request_id, &response); + self.handle_handshake_response(peer, &request_id, &response); } Event::OutboundFailure { request_id, @@ -225,16 +216,15 @@ impl NetworkBehaviour for HandshakeBehaviour } => { if let Some(peer) = self.pending_handshakes.remove(&request_id) { self.events.push(HandshakeEvent::Failed { - peer, - error: format!("Outbound failure: {error}"), + peer_id: peer, + error: HandshakeError::Outbound(error), }); - debug!(?error, "Outbound failure"); } } Event::InboundFailure { peer, error, .. } => { self.events.push(HandshakeEvent::Failed { - peer, - error: format!("Inbound failure: {error}"), + peer_id: peer, + error: HandshakeError::Inbound(error), }); } _ => {} diff --git a/anchor/network/src/handshake/error.rs b/anchor/network/src/handshake/error.rs index 4d23b9d5e..1168ab855 100644 --- a/anchor/network/src/handshake/error.rs +++ b/anchor/network/src/handshake/error.rs @@ -1,25 +1,22 @@ -use thiserror::Error; +use libp2p::request_response::{InboundFailure, OutboundFailure}; +use crate::handshake::types::UnmarshalError; -#[derive(Error, Debug)] +#[derive(Debug)] pub enum HandshakeError { - #[error("Serialization error: {0}")] - Serialization(#[from] serde_json::Error), - - #[error("Invalid signature")] InvalidSignature, - #[error("Network ID mismatch")] - NetworkMismatch, + NetworkMismatch { ours: String, theirs: String }, - #[error("Subnets format error")] SubnetsFormat, - #[error("Peer rejected")] PeerRejected, - #[error("Crypto error: {0}")] Crypto(String), InvalidMessageFormat, ResponseFailed, + + UnmarshalError(UnmarshalError), + Inbound(InboundFailure), + Outbound(OutboundFailure), } \ No newline at end of file diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 6df5bb127..d5c6492d7 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -2,3 +2,4 @@ pub mod behaviour; mod codec; pub mod types; pub mod envelope; +mod error; diff --git a/anchor/network/src/handshake/types.rs b/anchor/network/src/handshake/types.rs index 708a71fa4..0bbe3b3c5 100644 --- a/anchor/network/src/handshake/types.rs +++ b/anchor/network/src/handshake/types.rs @@ -3,6 +3,7 @@ use serde_json; use std::error::Error; use discv5::libp2p_identity::Keypair; use crate::handshake::envelope::{make_unsigned, Envelope}; +use crate::handshake::types::UnmarshalError::ValidationError; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct NodeMetadata { @@ -29,6 +30,18 @@ struct Serializable { entries: Vec, } +#[derive(Debug)] +pub enum UnmarshalError { + SerializationError(String), + ValidationError(String), +} + +impl From for UnmarshalError { + fn from(error: serde_json::Error) -> Self { + UnmarshalError::SerializationError(error.to_string()) + } +} + impl NodeInfo { pub fn new(network_id: String, metadata: Option) -> Self { NodeInfo { @@ -42,7 +55,7 @@ impl NodeInfo { pub(crate) const CODEC: &'static [u8] = b"ssv/nodeinfo"; /// Serialize `NodeInfo` to JSON bytes. - fn marshal_record(&self) -> Result, Box> { + fn marshal(&self) -> Result, Box> { let mut entries = vec![ "".to_string(), // formerly forkVersion, now deprecated self.network_id.clone(), // network id @@ -60,10 +73,10 @@ impl NodeInfo { } /// Deserialize `NodeInfo` from JSON bytes, replacing `self`. - pub fn unmarshal_record(&mut self, data: &[u8]) -> Result<(), Box> { + pub fn unmarshal(&mut self, data: &[u8]) -> Result<(), UnmarshalError> { let ser: Serializable = serde_json::from_slice(data)?; if ser.entries.len() < 2 { - return Err("node info must have at least 2 entries".into()); + return Err(ValidationError("node info must have at least 2 entries".into())); } // skip ser.entries[0]: old forkVersion self.network_id = ser.entries[1].clone(); @@ -90,7 +103,7 @@ impl NodeInfo { } // 1) marshal - let raw_payload = self.marshal_record()?; + let raw_payload = self.marshal()?; // 2) build the "unsigned" data let unsigned = make_unsigned(domain.as_bytes(), payload_type, &raw_payload); @@ -135,7 +148,7 @@ mod tests { let parsed_env = parse_envelope(&data).expect("Consume failed"); let mut parsed_node_info = NodeInfo::default(); - parsed_node_info.unmarshal_record(&parsed_env.payload).expect("TODO: panic message"); + parsed_node_info.unmarshal(&parsed_env.payload).expect("TODO: panic message"); assert_eq!(node_info, parsed_node_info); } @@ -158,16 +171,16 @@ mod tests { }; // 1) Marshal current_data - let data = current_data.marshal_record() + let data = current_data.marshal() .expect("marshal_record should succeed"); // 2) Unmarshal into parsed_rec let mut parsed_rec = NodeInfo::default(); - parsed_rec.unmarshal_record(&data) + parsed_rec.unmarshal(&data) .expect("unmarshal_record should succeed"); // 3) Now unmarshal the old format data into the same struct - parsed_rec.unmarshal_record(old_serialized_data) + parsed_rec.unmarshal(old_serialized_data) .expect("unmarshal old data should succeed"); // 4) Compare diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index ceccc69fc..28d13eefd 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -220,12 +220,12 @@ fn subnet_to_topic(subnet: SubnetId) -> IdentTopic { fn handle_handshake_event(ev: HandshakeEvent) { match ev { - HandshakeEvent::Completed { peer, their_info } => { - debug!(%peer, "Handshake completed"); + HandshakeEvent::Completed { peer_id, their_info } => { + debug!(%peer_id, ?their_info, "Handshake completed"); // Update peer store with their_info } - HandshakeEvent::Failed { peer, error } => { - debug!(%peer, %error, "Handshake failed"); + HandshakeEvent::Failed { peer_id, error } => { + debug!(%peer_id, ?error, "Handshake failed"); } } } From b2bd4bf63e02ab5524ffb7c1555fc3c69ffcd038 Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 6 Feb 2025 15:23:30 +0100 Subject: [PATCH 12/63] remove pending_handshakes --- anchor/network/src/handshake/behaviour.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 1e110e2bd..262f62f1e 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -31,8 +31,6 @@ pub enum HandshakeEvent { pub struct HandshakeBehaviour { /// Request-response behaviour for the handshake protocol. behaviour: Behaviour, - /// Pending outgoing handshake requests. - pending_handshakes: HashMap, /// Keypair for signing envelopes. keypair: Keypair, /// Local node's information. @@ -55,7 +53,6 @@ impl HandshakeBehaviour Self { behaviour: behaviour, - pending_handshakes: HashMap::new(), keypair, local_node_info, events: Vec::new(), @@ -160,8 +157,7 @@ impl NetworkBehaviour for HandshakeBehaviour if let FromSwarm::ConnectionEstablished(conn_est) = &event { let peer = conn_est.peer_id; let request = self.sealed_node_record(); - let request_id = self.behaviour.send_request(&peer, request); - self.pending_handshakes.insert(request_id, peer); + self.behaviour.send_request(&peer, request); } // Delegate other events to inner behaviour @@ -214,12 +210,10 @@ impl NetworkBehaviour for HandshakeBehaviour error, .. } => { - if let Some(peer) = self.pending_handshakes.remove(&request_id) { - self.events.push(HandshakeEvent::Failed { - peer_id: peer, - error: HandshakeError::Outbound(error), - }); - } + self.events.push(HandshakeEvent::Failed { + peer_id: peer, + error: HandshakeError::Outbound(error), + }); } Event::InboundFailure { peer, error, .. } => { self.events.push(HandshakeEvent::Failed { From 573dd5411478b52251ff4cd63c163d7f391d0c8e Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 6 Feb 2025 17:47:36 +0100 Subject: [PATCH 13/63] fix wrong network id --- anchor/network/src/handshake/behaviour.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 262f62f1e..96b309094 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -52,7 +52,7 @@ impl HandshakeBehaviour let behaviour = Behaviour::new([(protocol, ProtocolSupport::Full)], Config::default()); Self { - behaviour: behaviour, + behaviour, keypair, local_node_info, events: Vec::new(), @@ -71,9 +71,9 @@ impl HandshakeBehaviour node_info: &NodeInfo, peer: PeerId, ) -> Result<(), HandshakeError> { - let theirs = self.local_node_info.lock().unwrap().network_id.clone(); - if node_info.network_id != *theirs { - return Err(HandshakeError::NetworkMismatch { ours: node_info.network_id.clone(), theirs }) + let ours = self.local_node_info.lock().unwrap().network_id.clone(); + if node_info.network_id != *ours { + return Err(HandshakeError::NetworkMismatch { ours, theirs: node_info.network_id.clone()}) } Ok(()) } From 0114cdcbdbfb62e1d059076460a5f18aba367a12 Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 6 Feb 2025 17:48:30 +0100 Subject: [PATCH 14/63] add NotifyHandler again --- anchor/network/src/handshake/behaviour.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 96b309094..247f9ab41 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -223,6 +223,17 @@ impl NetworkBehaviour for HandshakeBehaviour } _ => {} }, + ToSwarm::NotifyHandler { + peer_id, + handler, + event, + } => { + return Poll::Ready(ToSwarm::NotifyHandler { + peer_id, + handler, + event, + }); + } _ => {} } } From e65e5b4cd06b7ca28d9efa5e591c155edfb49c88 Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 6 Feb 2025 18:52:11 +0100 Subject: [PATCH 15/63] small change --- anchor/network/src/handshake/codec.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anchor/network/src/handshake/codec.rs b/anchor/network/src/handshake/codec.rs index 05c079475..d3f3443ae 100644 --- a/anchor/network/src/handshake/codec.rs +++ b/anchor/network/src/handshake/codec.rs @@ -89,8 +89,8 @@ impl Codec for EnvelopeCodec { .encode_to_vec() .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; io.write_all(&raw).await?; - let r = io.close().await; + io.close().await?; debug!("wrote handshake response"); - r + Ok(()) } } From 5d2f6dfbc7986bcb8ec9a04f040840653967460d Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 12:05:42 +0100 Subject: [PATCH 16/63] use NodeInfoProvider trait --- anchor/network/src/handshake/behaviour.rs | 17 ++++++++++------ anchor/network/src/network.rs | 24 +++++++++++++++++++++-- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 247f9ab41..6aa1b37c1 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -20,6 +20,11 @@ use crate::handshake::envelope::Envelope; use crate::handshake::error::HandshakeError; use crate::handshake::types::NodeInfo; +pub trait NodeInfoProvider: Send + Sync { + /// Returns a clone of the current node information. + fn get_node_info(&self) -> NodeInfo; +} + /// Event emitted on handshake completion or failure. #[derive(Debug)] pub enum HandshakeEvent { @@ -33,8 +38,8 @@ pub struct HandshakeBehaviour { behaviour: Behaviour, /// Keypair for signing envelopes. keypair: Keypair, - /// Local node's information. - local_node_info: Arc>, + /// Local node's information provider. + node_info_provider: Box, /// Events to emit. events: Vec, } @@ -43,7 +48,7 @@ impl HandshakeBehaviour { pub fn new( keypair: Keypair, - local_node_info: Arc>, + local_node_info: Box, ) -> Self { // NodeInfoProtocol is the protocol.ID used for handshake const NODE_INFO_PROTOCOL: &'static str = "/ssv/info/0.0.1"; @@ -54,14 +59,14 @@ impl HandshakeBehaviour Self { behaviour, keypair, - local_node_info, + node_info_provider: local_node_info, events: Vec::new(), } } /// Create a signed envelope containing local node info. fn sealed_node_record(&self) -> Envelope { - let node_info = self.local_node_info.lock().unwrap().clone(); + let node_info = self.node_info_provider.get_node_info(); node_info.seal(&self.keypair).unwrap() } @@ -71,7 +76,7 @@ impl HandshakeBehaviour node_info: &NodeInfo, peer: PeerId, ) -> Result<(), HandshakeError> { - let ours = self.local_node_info.lock().unwrap().network_id.clone(); + let ours = self.node_info_provider.get_node_info().network_id; if node_info.network_id != *ours { return Err(HandshakeError::NetworkMismatch { ours, theirs: node_info.network_id.clone()}) } diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 28d13eefd..3f228fd9a 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -23,7 +23,7 @@ use crate::keypair_utils::load_private_key; use crate::transport::build_transport; use crate::Config; -use crate::handshake::behaviour::{HandshakeBehaviour, HandshakeEvent}; +use crate::handshake::behaviour::{HandshakeBehaviour, HandshakeEvent, NodeInfoProvider}; use crate::types::ssv_message::SignedSSVMessage; use lighthouse_network::EnrExt; use ssz::Decode; @@ -297,7 +297,7 @@ async fn build_anchor_behaviour( ); let handshake = HandshakeBehaviour::new( local_keypair.clone(), - Arc::new(Mutex::new(node_info)), + Box::new(DefaultNodeInfoProvider::new(node_info)), ); AnchorBehaviour { @@ -362,6 +362,26 @@ fn build_swarm( .build() } + +pub struct DefaultNodeInfoProvider { + node_info: Arc>, +} + +impl DefaultNodeInfoProvider { + pub fn new(node_info: NodeInfo) -> Self { + Self { + node_info: Arc::new(Mutex::new(node_info)), + } + } +} + +impl NodeInfoProvider for DefaultNodeInfoProvider { + fn get_node_info(&self) -> NodeInfo { + // In a real implementation, consider handling lock poisoning. + self.node_info.lock().unwrap().clone() + } +} + #[cfg(test)] mod test { use crate::network::Network; From 97f6534a8abc7652680d7471677efbb51547b101 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 12:26:22 +0100 Subject: [PATCH 17/63] rename HandshakeEvent --- anchor/network/src/handshake/behaviour.rs | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 6aa1b37c1..878f50cb5 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -2,7 +2,7 @@ use discv5::libp2p_identity::Keypair; use discv5::multiaddr::Multiaddr; use libp2p::core::transport::PortUse; use libp2p::core::Endpoint; -use libp2p::request_response::{self, Behaviour, Config, Event, OutboundRequestId, ProtocolSupport, ResponseChannel}; +use libp2p::request_response::{self, Behaviour, Config, Event as RequestResponseEvent, OutboundRequestId, ProtocolSupport, ResponseChannel}; use libp2p::swarm::{ ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, @@ -27,7 +27,7 @@ pub trait NodeInfoProvider: Send + Sync { /// Event emitted on handshake completion or failure. #[derive(Debug)] -pub enum HandshakeEvent { +pub enum Event { Completed { peer_id: PeerId, their_info: NodeInfo }, Failed { peer_id: PeerId, error: HandshakeError }, } @@ -41,7 +41,7 @@ pub struct HandshakeBehaviour { /// Local node's information provider. node_info_provider: Box, /// Events to emit. - events: Vec, + events: Vec, } impl HandshakeBehaviour @@ -104,15 +104,15 @@ impl HandshakeBehaviour let mut their_info = NodeInfo::default(); if let Err(e) = their_info.unmarshal(&response.payload) { - self.events.push(HandshakeEvent::Failed { + self.events.push(Event::Failed { peer_id, error: HandshakeError::UnmarshalError(e), }); } match self.verify_node_info(&their_info, peer_id) { - Ok(_) => self.events.push(HandshakeEvent::Completed { peer_id, their_info }), - Err(e) => self.events.push(HandshakeEvent::Failed { + Ok(_) => self.events.push(Event::Completed { peer_id, their_info }), + Err(e) => self.events.push(Event::Failed { peer_id, error: e, }), @@ -123,7 +123,7 @@ impl HandshakeBehaviour impl NetworkBehaviour for HandshakeBehaviour { type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; - type ToSwarm = HandshakeEvent; + type ToSwarm = Event; fn handle_established_inbound_connection( &mut self, @@ -187,7 +187,7 @@ impl NetworkBehaviour for HandshakeBehaviour while let Poll::Ready(event) = self.behaviour.poll(cx) { match event { ToSwarm::GenerateEvent(event) => match event { - Event::Message { + RequestResponseEvent::Message { peer, message: request_response::Message::Request { @@ -197,7 +197,7 @@ impl NetworkBehaviour for HandshakeBehaviour debug!("Received handshake request"); self.handle_handshake_request(peer, request, channel); } - Event::Message { + RequestResponseEvent::Message { peer, message: request_response::Message::Response { @@ -209,19 +209,19 @@ impl NetworkBehaviour for HandshakeBehaviour debug!(?response, "Received handshake response"); self.handle_handshake_response(peer, &request_id, &response); } - Event::OutboundFailure { + RequestResponseEvent::OutboundFailure { request_id, peer, error, .. } => { - self.events.push(HandshakeEvent::Failed { + self.events.push(Event::Failed { peer_id: peer, error: HandshakeError::Outbound(error), }); } - Event::InboundFailure { peer, error, .. } => { - self.events.push(HandshakeEvent::Failed { + RequestResponseEvent::InboundFailure { peer, error, .. } => { + self.events.push(Event::Failed { peer_id: peer, error: HandshakeError::Inbound(error), }); From f755ea5cd911181207625e4c57835eb71ea8c7e1 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 12:30:31 +0100 Subject: [PATCH 18/63] rename HandshakeBehaviour --- anchor/network/src/behaviour.rs | 4 ++-- anchor/network/src/handshake/behaviour.rs | 14 +++++++------- anchor/network/src/network.rs | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/anchor/network/src/behaviour.rs b/anchor/network/src/behaviour.rs index acc8e4c5b..13fb0762f 100644 --- a/anchor/network/src/behaviour.rs +++ b/anchor/network/src/behaviour.rs @@ -1,6 +1,6 @@ use crate::discovery::Discovery; use libp2p::request_response::Behaviour; -use crate::handshake::behaviour::HandshakeBehaviour; +use crate::handshake::behaviour::Behaviour; use libp2p::swarm::NetworkBehaviour; use libp2p::{gossipsub, identify, ping}; @@ -15,5 +15,5 @@ pub struct AnchorBehaviour { /// Discv5 Discovery protocol. pub discovery: Discovery, - pub handshake: HandshakeBehaviour, + pub handshake: Behaviour, } diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs index 878f50cb5..0cc33e24b 100644 --- a/anchor/network/src/handshake/behaviour.rs +++ b/anchor/network/src/handshake/behaviour.rs @@ -2,7 +2,7 @@ use discv5::libp2p_identity::Keypair; use discv5::multiaddr::Multiaddr; use libp2p::core::transport::PortUse; use libp2p::core::Endpoint; -use libp2p::request_response::{self, Behaviour, Config, Event as RequestResponseEvent, OutboundRequestId, ProtocolSupport, ResponseChannel}; +use libp2p::request_response::{self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, OutboundRequestId, ProtocolSupport, ResponseChannel}; use libp2p::swarm::{ ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, @@ -33,9 +33,9 @@ pub enum Event { } /// Network behaviour handling the handshake protocol. -pub struct HandshakeBehaviour { +pub struct Behaviour { /// Request-response behaviour for the handshake protocol. - behaviour: Behaviour, + behaviour: RequestResponseBehaviour, /// Keypair for signing envelopes. keypair: Keypair, /// Local node's information provider. @@ -44,7 +44,7 @@ pub struct HandshakeBehaviour { events: Vec, } -impl HandshakeBehaviour +impl Behaviour { pub fn new( keypair: Keypair, @@ -54,7 +54,7 @@ impl HandshakeBehaviour const NODE_INFO_PROTOCOL: &'static str = "/ssv/info/0.0.1"; let protocol = StreamProtocol::new(NODE_INFO_PROTOCOL); - let behaviour = Behaviour::new([(protocol, ProtocolSupport::Full)], Config::default()); + let behaviour = RequestResponseBehaviour::new([(protocol, ProtocolSupport::Full)], Config::default()); Self { behaviour, @@ -120,9 +120,9 @@ impl HandshakeBehaviour } } -impl NetworkBehaviour for HandshakeBehaviour +impl NetworkBehaviour for Behaviour { - type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; + type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; type ToSwarm = Event; fn handle_established_inbound_connection( diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 3f228fd9a..cebfef370 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -23,7 +23,7 @@ use crate::keypair_utils::load_private_key; use crate::transport::build_transport; use crate::Config; -use crate::handshake::behaviour::{HandshakeBehaviour, HandshakeEvent, NodeInfoProvider}; +use crate::handshake::behaviour::{Behaviour, Event, NodeInfoProvider}; use crate::types::ssv_message::SignedSSVMessage; use lighthouse_network::EnrExt; use ssz::Decode; @@ -218,13 +218,13 @@ fn subnet_to_topic(subnet: SubnetId) -> IdentTopic { IdentTopic::new(format!("ssv.{}", *subnet)) } -fn handle_handshake_event(ev: HandshakeEvent) { +fn handle_handshake_event(ev: Event) { match ev { - HandshakeEvent::Completed { peer_id, their_info } => { + Event::Completed { peer_id, their_info } => { debug!(%peer_id, ?their_info, "Handshake completed"); // Update peer store with their_info } - HandshakeEvent::Failed { peer_id, error } => { + Event::Failed { peer_id, error } => { debug!(%peer_id, ?error, "Handshake failed"); } } @@ -295,7 +295,7 @@ async fn build_anchor_behaviour( subnets: "ffffffffffffffffffffffffffffffff".to_string(), }), ); - let handshake = HandshakeBehaviour::new( + let handshake = Behaviour::new( local_keypair.clone(), Box::new(DefaultNodeInfoProvider::new(node_info)), ); From 8f2e5458bc5b416cb87945f0749a0e81cedd0f21 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 12:34:27 +0100 Subject: [PATCH 19/63] move behaviour to mod.rs --- anchor/network/src/handshake/behaviour.rs | 253 --------------------- anchor/network/src/handshake/mod.rs | 255 +++++++++++++++++++++- anchor/network/src/network.rs | 2 +- 3 files changed, 255 insertions(+), 255 deletions(-) delete mode 100644 anchor/network/src/handshake/behaviour.rs diff --git a/anchor/network/src/handshake/behaviour.rs b/anchor/network/src/handshake/behaviour.rs deleted file mode 100644 index 0cc33e24b..000000000 --- a/anchor/network/src/handshake/behaviour.rs +++ /dev/null @@ -1,253 +0,0 @@ -use discv5::libp2p_identity::Keypair; -use discv5::multiaddr::Multiaddr; -use libp2p::core::transport::PortUse; -use libp2p::core::Endpoint; -use libp2p::request_response::{self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, OutboundRequestId, ProtocolSupport, ResponseChannel}; -use libp2p::swarm::{ - ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, - THandlerOutEvent, ToSwarm, -}; -use libp2p::{PeerId, StreamProtocol}; -use prost::Message; -use std::collections::HashMap; -use std::error::Error; -use std::sync::{Arc, Mutex}; -use std::task::{Context, Poll}; -use std::time::Instant; -use tracing::debug; -use crate::handshake::codec::EnvelopeCodec; -use crate::handshake::envelope::Envelope; -use crate::handshake::error::HandshakeError; -use crate::handshake::types::NodeInfo; - -pub trait NodeInfoProvider: Send + Sync { - /// Returns a clone of the current node information. - fn get_node_info(&self) -> NodeInfo; -} - -/// Event emitted on handshake completion or failure. -#[derive(Debug)] -pub enum Event { - Completed { peer_id: PeerId, their_info: NodeInfo }, - Failed { peer_id: PeerId, error: HandshakeError }, -} - -/// Network behaviour handling the handshake protocol. -pub struct Behaviour { - /// Request-response behaviour for the handshake protocol. - behaviour: RequestResponseBehaviour, - /// Keypair for signing envelopes. - keypair: Keypair, - /// Local node's information provider. - node_info_provider: Box, - /// Events to emit. - events: Vec, -} - -impl Behaviour -{ - pub fn new( - keypair: Keypair, - local_node_info: Box, - ) -> Self { - // NodeInfoProtocol is the protocol.ID used for handshake - const NODE_INFO_PROTOCOL: &'static str = "/ssv/info/0.0.1"; - - let protocol = StreamProtocol::new(NODE_INFO_PROTOCOL); - let behaviour = RequestResponseBehaviour::new([(protocol, ProtocolSupport::Full)], Config::default()); - - Self { - behaviour, - keypair, - node_info_provider: local_node_info, - events: Vec::new(), - } - } - - /// Create a signed envelope containing local node info. - fn sealed_node_record(&self) -> Envelope { - let node_info = self.node_info_provider.get_node_info(); - node_info.seal(&self.keypair).unwrap() - } - - /// Verify an incoming envelope and apply filters. - fn verify_node_info( - &mut self, - node_info: &NodeInfo, - peer: PeerId, - ) -> Result<(), HandshakeError> { - let ours = self.node_info_provider.get_node_info().network_id; - if node_info.network_id != *ours { - return Err(HandshakeError::NetworkMismatch { ours, theirs: node_info.network_id.clone()}) - } - Ok(()) - } - - fn handle_handshake_request(&mut self, peer_id: PeerId, request: Envelope, channel: ResponseChannel) { - // Handle incoming request: send response then verify - let response = self.sealed_node_record(); - match self.behaviour.send_response(channel, response.clone()) { - Ok(_) => { - self.unmarshall_and_verify(peer_id, &response); - } - Err(e) => { - // There was an error sending the response. The InboundFailure handler will be called - } - } - } - - fn handle_handshake_response(&mut self, peer_id: PeerId, request_id: &OutboundRequestId, response: &Envelope) { - self.unmarshall_and_verify(peer_id, &response); - } - - fn unmarshall_and_verify(&mut self, peer_id: PeerId, response: &Envelope) { - let mut their_info = NodeInfo::default(); - - if let Err(e) = their_info.unmarshal(&response.payload) { - self.events.push(Event::Failed { - peer_id, - error: HandshakeError::UnmarshalError(e), - }); - } - - match self.verify_node_info(&their_info, peer_id) { - Ok(_) => self.events.push(Event::Completed { peer_id, their_info }), - Err(e) => self.events.push(Event::Failed { - peer_id, - error: e, - }), - } - } -} - -impl NetworkBehaviour for Behaviour -{ - type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; - type ToSwarm = Event; - - fn handle_established_inbound_connection( - &mut self, - connection_id: ConnectionId, - peer: PeerId, - local_addr: &Multiaddr, - remote_addr: &Multiaddr, - ) -> Result, ConnectionDenied> { - self.behaviour.handle_established_inbound_connection( - connection_id, - peer, - local_addr, - remote_addr, - ) - } - - fn handle_established_outbound_connection( - &mut self, - connection_id: ConnectionId, - peer: PeerId, - addr: &Multiaddr, - role_override: Endpoint, - port_use: PortUse, - ) -> Result, ConnectionDenied> { - self.behaviour.handle_established_outbound_connection( - connection_id, - peer, - addr, - role_override, - port_use, - ) - } - - fn on_swarm_event(&mut self, event: FromSwarm) { - // Initiate handshake on new connection - if let FromSwarm::ConnectionEstablished(conn_est) = &event { - let peer = conn_est.peer_id; - let request = self.sealed_node_record(); - self.behaviour.send_request(&peer, request); - } - - // Delegate other events to inner behaviour - self.behaviour.on_swarm_event(event); - } - - fn on_connection_handler_event( - &mut self, - peer_id: PeerId, - connection_id: ConnectionId, - event: THandlerOutEvent, - ) { - self.behaviour - .on_connection_handler_event(peer_id, connection_id, event); - } - - fn poll( - &mut self, - cx: &mut Context<'_>, - ) -> Poll>> { - // Process events from inner request-response behaviour - while let Poll::Ready(event) = self.behaviour.poll(cx) { - match event { - ToSwarm::GenerateEvent(event) => match event { - RequestResponseEvent::Message { - peer, - message: - request_response::Message::Request { - request, channel, .. - }, - } => { - debug!("Received handshake request"); - self.handle_handshake_request(peer, request, channel); - } - RequestResponseEvent::Message { - peer, - message: - request_response::Message::Response { - request_id, - response, - .. - }, - } => { - debug!(?response, "Received handshake response"); - self.handle_handshake_response(peer, &request_id, &response); - } - RequestResponseEvent::OutboundFailure { - request_id, - peer, - error, - .. - } => { - self.events.push(Event::Failed { - peer_id: peer, - error: HandshakeError::Outbound(error), - }); - } - RequestResponseEvent::InboundFailure { peer, error, .. } => { - self.events.push(Event::Failed { - peer_id: peer, - error: HandshakeError::Inbound(error), - }); - } - _ => {} - }, - ToSwarm::NotifyHandler { - peer_id, - handler, - event, - } => { - return Poll::Ready(ToSwarm::NotifyHandler { - peer_id, - handler, - event, - }); - } - _ => {} - } - } - - // Emit queued events - if !self.events.is_empty() { - return Poll::Ready(ToSwarm::GenerateEvent(self.events.remove(0))); - } - - Poll::Pending - } -} diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index d5c6492d7..3979f234e 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -1,5 +1,258 @@ -pub mod behaviour; mod codec; pub mod types; pub mod envelope; mod error; + +use discv5::libp2p_identity::Keypair; +use discv5::multiaddr::Multiaddr; +use libp2p::core::transport::PortUse; +use libp2p::core::Endpoint; +use libp2p::request_response::{self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, OutboundRequestId, ProtocolSupport, ResponseChannel}; +use libp2p::swarm::{ + ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, + THandlerOutEvent, ToSwarm, +}; +use libp2p::{PeerId, StreamProtocol}; +use prost::Message; +use std::collections::HashMap; +use std::error::Error; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; +use std::time::Instant; +use tracing::debug; +use crate::handshake::codec::EnvelopeCodec; +use crate::handshake::envelope::Envelope; +use crate::handshake::error::HandshakeError; +use crate::handshake::types::NodeInfo; + +pub trait NodeInfoProvider: Send + Sync { + /// Returns a clone of the current node information. + fn get_node_info(&self) -> NodeInfo; +} + +/// Event emitted on handshake completion or failure. +#[derive(Debug)] +pub enum Event { + Completed { peer_id: PeerId, their_info: NodeInfo }, + Failed { peer_id: PeerId, error: HandshakeError }, +} + +/// Network behaviour handling the handshake protocol. +pub struct Behaviour { + /// Request-response behaviour for the handshake protocol. + behaviour: RequestResponseBehaviour, + /// Keypair for signing envelopes. + keypair: Keypair, + /// Local node's information provider. + node_info_provider: Box, + /// Events to emit. + events: Vec, +} + +impl Behaviour +{ + pub fn new( + keypair: Keypair, + local_node_info: Box, + ) -> Self { + // NodeInfoProtocol is the protocol.ID used for handshake + const NODE_INFO_PROTOCOL: &'static str = "/ssv/info/0.0.1"; + + let protocol = StreamProtocol::new(NODE_INFO_PROTOCOL); + let behaviour = RequestResponseBehaviour::new([(protocol, ProtocolSupport::Full)], Config::default()); + + Self { + behaviour, + keypair, + node_info_provider: local_node_info, + events: Vec::new(), + } + } + + /// Create a signed envelope containing local node info. + fn sealed_node_record(&self) -> Envelope { + let node_info = self.node_info_provider.get_node_info(); + node_info.seal(&self.keypair).unwrap() + } + + /// Verify an incoming envelope and apply filters. + fn verify_node_info( + &mut self, + node_info: &NodeInfo, + peer: PeerId, + ) -> Result<(), HandshakeError> { + let ours = self.node_info_provider.get_node_info().network_id; + if node_info.network_id != *ours { + return Err(HandshakeError::NetworkMismatch { ours, theirs: node_info.network_id.clone()}) + } + Ok(()) + } + + fn handle_handshake_request(&mut self, peer_id: PeerId, request: Envelope, channel: ResponseChannel) { + // Handle incoming request: send response then verify + let response = self.sealed_node_record(); + match self.behaviour.send_response(channel, response.clone()) { + Ok(_) => { + self.unmarshall_and_verify(peer_id, &response); + } + Err(e) => { + // There was an error sending the response. The InboundFailure handler will be called + } + } + } + + fn handle_handshake_response(&mut self, peer_id: PeerId, request_id: &OutboundRequestId, response: &Envelope) { + self.unmarshall_and_verify(peer_id, &response); + } + + fn unmarshall_and_verify(&mut self, peer_id: PeerId, response: &Envelope) { + let mut their_info = NodeInfo::default(); + + if let Err(e) = their_info.unmarshal(&response.payload) { + self.events.push(Event::Failed { + peer_id, + error: HandshakeError::UnmarshalError(e), + }); + } + + match self.verify_node_info(&their_info, peer_id) { + Ok(_) => self.events.push(Event::Completed { peer_id, their_info }), + Err(e) => self.events.push(Event::Failed { + peer_id, + error: e, + }), + } + } +} + +impl NetworkBehaviour for Behaviour +{ + type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; + type ToSwarm = Event; + + fn handle_established_inbound_connection( + &mut self, + connection_id: ConnectionId, + peer: PeerId, + local_addr: &Multiaddr, + remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + self.behaviour.handle_established_inbound_connection( + connection_id, + peer, + local_addr, + remote_addr, + ) + } + + fn handle_established_outbound_connection( + &mut self, + connection_id: ConnectionId, + peer: PeerId, + addr: &Multiaddr, + role_override: Endpoint, + port_use: PortUse, + ) -> Result, ConnectionDenied> { + self.behaviour.handle_established_outbound_connection( + connection_id, + peer, + addr, + role_override, + port_use, + ) + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + // Initiate handshake on new connection + if let FromSwarm::ConnectionEstablished(conn_est) = &event { + let peer = conn_est.peer_id; + let request = self.sealed_node_record(); + self.behaviour.send_request(&peer, request); + } + + // Delegate other events to inner behaviour + self.behaviour.on_swarm_event(event); + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: THandlerOutEvent, + ) { + self.behaviour + .on_connection_handler_event(peer_id, connection_id, event); + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + // Process events from inner request-response behaviour + while let Poll::Ready(event) = self.behaviour.poll(cx) { + match event { + ToSwarm::GenerateEvent(event) => match event { + RequestResponseEvent::Message { + peer, + message: + request_response::Message::Request { + request, channel, .. + }, + } => { + debug!("Received handshake request"); + self.handle_handshake_request(peer, request, channel); + } + RequestResponseEvent::Message { + peer, + message: + request_response::Message::Response { + request_id, + response, + .. + }, + } => { + debug!(?response, "Received handshake response"); + self.handle_handshake_response(peer, &request_id, &response); + } + RequestResponseEvent::OutboundFailure { + request_id, + peer, + error, + .. + } => { + self.events.push(Event::Failed { + peer_id: peer, + error: HandshakeError::Outbound(error), + }); + } + RequestResponseEvent::InboundFailure { peer, error, .. } => { + self.events.push(Event::Failed { + peer_id: peer, + error: HandshakeError::Inbound(error), + }); + } + _ => {} + }, + ToSwarm::NotifyHandler { + peer_id, + handler, + event, + } => { + return Poll::Ready(ToSwarm::NotifyHandler { + peer_id, + handler, + event, + }); + } + _ => {} + } + } + + // Emit queued events + if !self.events.is_empty() { + return Poll::Ready(ToSwarm::GenerateEvent(self.events.remove(0))); + } + + Poll::Pending + } +} diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index cebfef370..b78eee38a 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -23,7 +23,7 @@ use crate::keypair_utils::load_private_key; use crate::transport::build_transport; use crate::Config; -use crate::handshake::behaviour::{Behaviour, Event, NodeInfoProvider}; +use crate::handshake::{Behaviour, Event, NodeInfoProvider}; use crate::types::ssv_message::SignedSSVMessage; use lighthouse_network::EnrExt; use ssz::Decode; From d0ab73846a98701d5dcef922809effaa2d1a9ab4 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 13:12:34 +0100 Subject: [PATCH 20/63] create envelope mod --- anchor/network/src/handshake/{ => envelope}/codec.rs | 8 ++++---- .../src/handshake/{envelope.rs => envelope/mod.rs} | 5 ++++- anchor/network/src/handshake/mod.rs | 11 +++++------ 3 files changed, 13 insertions(+), 11 deletions(-) rename anchor/network/src/handshake/{ => envelope}/codec.rs (95%) rename anchor/network/src/handshake/{envelope.rs => envelope/mod.rs} (98%) diff --git a/anchor/network/src/handshake/codec.rs b/anchor/network/src/handshake/envelope/codec.rs similarity index 95% rename from anchor/network/src/handshake/codec.rs rename to anchor/network/src/handshake/envelope/codec.rs index d3f3443ae..cedeb4c4f 100644 --- a/anchor/network/src/handshake/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -1,7 +1,7 @@ use crate::handshake::envelope::{parse_envelope, Envelope}; use futures::{AsyncReadExt, AsyncWriteExt}; use libp2p::futures::{AsyncRead, AsyncWrite}; -use libp2p::request_response::Codec; +use libp2p::request_response::Codec as RequestResponseCodec; use std::io; use async_trait::async_trait; use libp2p::StreamProtocol; @@ -13,10 +13,10 @@ use crate::handshake::types::NodeInfo; /// A `Codec` that reads/writes an **`Envelope`** #[derive(Clone, Debug, Default)] -pub struct EnvelopeCodec; +pub struct Codec; #[async_trait] -impl Codec for EnvelopeCodec { +impl RequestResponseCodec for Codec { type Protocol = StreamProtocol; type Request = Envelope; type Response = Envelope; @@ -55,7 +55,7 @@ impl Codec for EnvelopeCodec { let env = parse_envelope(&msg_buf).unwrap(); debug!(?env, "decoded handshake response"); - Ok(env) + Ok(env) } async fn write_request( diff --git a/anchor/network/src/handshake/envelope.rs b/anchor/network/src/handshake/envelope/mod.rs similarity index 98% rename from anchor/network/src/handshake/envelope.rs rename to anchor/network/src/handshake/envelope/mod.rs index 7cdad45a9..ab07bc9f2 100644 --- a/anchor/network/src/handshake/envelope.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -1,7 +1,8 @@ +mod codec; + use std::error::Error; use discv5::libp2p_identity::PublicKey; use prost::Message; -use strum::Display; use crate::handshake::types::NodeInfo; /// The Envelope structure exactly matching Go's Envelope fields and tags: @@ -75,3 +76,5 @@ pub fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec< out } + +pub use codec::Codec; \ No newline at end of file diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 3979f234e..b383af3ca 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -1,6 +1,5 @@ -mod codec; pub mod types; -pub mod envelope; +mod envelope; mod error; use discv5::libp2p_identity::Keypair; @@ -20,8 +19,8 @@ use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use std::time::Instant; use tracing::debug; -use crate::handshake::codec::EnvelopeCodec; -use crate::handshake::envelope::Envelope; +use crate::handshake::envelope::{Envelope}; +use crate::handshake::envelope::Codec; use crate::handshake::error::HandshakeError; use crate::handshake::types::NodeInfo; @@ -40,7 +39,7 @@ pub enum Event { /// Network behaviour handling the handshake protocol. pub struct Behaviour { /// Request-response behaviour for the handshake protocol. - behaviour: RequestResponseBehaviour, + behaviour: RequestResponseBehaviour, /// Keypair for signing envelopes. keypair: Keypair, /// Local node's information provider. @@ -127,7 +126,7 @@ impl Behaviour impl NetworkBehaviour for Behaviour { - type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; + type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; type ToSwarm = Event; fn handle_established_inbound_connection( From 88e8a925cecc5efa1d022353ad270ec97b7aca5e Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 13:51:32 +0100 Subject: [PATCH 21/63] improve error handling --- .../network/src/handshake/envelope/codec.rs | 13 +++- anchor/network/src/handshake/envelope/mod.rs | 74 ++++++++++++++++--- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/anchor/network/src/handshake/envelope/codec.rs b/anchor/network/src/handshake/envelope/codec.rs index cedeb4c4f..6b564298c 100644 --- a/anchor/network/src/handshake/envelope/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -9,8 +9,16 @@ use prost::bytes::BytesMut; use prost::encoding::{decode_varint, encode_varint, encoded_len_varint}; use prost::Message; use tracing::debug; +use crate::handshake::envelope; use crate::handshake::types::NodeInfo; + +impl From for io::Error { + fn from(err: envelope::Error) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, err) + } +} + /// A `Codec` that reads/writes an **`Envelope`** #[derive(Clone, Debug, Default)] pub struct Codec; @@ -33,8 +41,7 @@ impl RequestResponseCodec for Codec { let mut msg_buf = Vec::new(); let num_bytes_read = io.read_to_end(&mut msg_buf).await?; debug!(?num_bytes_read, "read handshake request"); - let env = Envelope::decode_from_slice(&msg_buf) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + let env = Envelope::decode_from_slice(&msg_buf)?; debug!(?env, "decoded handshake request"); Ok(env) } @@ -52,7 +59,7 @@ impl RequestResponseCodec for Codec { let num_bytes_read = io.read_to_end(&mut msg_buf).await?; debug!(?num_bytes_read, "read handshake response"); - let env = parse_envelope(&msg_buf).unwrap(); + let env = parse_envelope(&msg_buf)?; debug!(?env, "decoded handshake response"); Ok(env) diff --git a/anchor/network/src/handshake/envelope/mod.rs b/anchor/network/src/handshake/envelope/mod.rs index ab07bc9f2..0026a151d 100644 --- a/anchor/network/src/handshake/envelope/mod.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -1,9 +1,61 @@ mod codec; -use std::error::Error; use discv5::libp2p_identity::PublicKey; -use prost::Message; -use crate::handshake::types::NodeInfo; +use libp2p::identity::DecodingError; +use prost::{DecodeError, EncodeError, Message}; +use crate::handshake::types::{NodeInfo, UnmarshalError}; + +use std::error::Error as StdError; +use std::fmt; + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Decode(e) => write!(f, "Decode error: {}", e), + Error::Encode(e) => write!(f, "Encode error: {}", e), + Error::PublicKeyDecoding(msg) => write!(f, "Public Key decoding error: {}", msg), + Error::SignatureVerification(msg) => write!(f, "Signature verification error: {}", msg), + } + } +} + +impl StdError for Error { + // Optionally, override `source` if any variant wraps an underlying error. + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Error::Decode(e) => Some(e), + Error::Encode(e) => Some(e), + Error::PublicKeyDecoding(_) => None, + Error::SignatureVerification(_) => None, + } + } +} + +#[derive(Debug)] +pub enum Error { + Decode(DecodeError), + Encode(EncodeError), + PublicKeyDecoding(DecodingError), + SignatureVerification(String), +} + +impl From for Error { + fn from(error: DecodeError) -> Self { + Error::Decode(error) + } +} + +impl From for Error { + fn from(error: EncodeError) -> Self { + Error::Encode(error) + } +} + +impl From for Error { + fn from(error: DecodingError) -> Self { + Error::PublicKeyDecoding(error) + } +} /// The Envelope structure exactly matching Go's Envelope fields and tags: /// 1 => public_key @@ -27,24 +79,25 @@ pub struct Envelope { pub signature: Vec, } + impl Envelope { /// Encode the Envelope to a Protobuf byte array (like `proto.Marshal` in Go). - pub fn encode_to_vec(&self) -> Result, prost::EncodeError> { + pub fn encode_to_vec(&self) -> Result, Error> { let mut buf = Vec::with_capacity(self.encoded_len()); self.encode(&mut buf)?; Ok(buf) } /// Decode an Envelope from a Protobuf byte array (like `proto.Unmarshal` in Go). - pub fn decode_from_slice(data: &[u8]) -> Result { - Envelope::decode(data) + pub fn decode_from_slice(data: &[u8]) -> Result { + Envelope::decode(data).map_err(Error::from) } } /// Consumes an Envelope => verify signature => parse the record. pub fn parse_envelope( bytes: &[u8], -) -> Result<(Envelope), Box> { +) -> Result { let env = Envelope::decode_from_slice(bytes)?; let domain = NodeInfo::DOMAIN; @@ -52,10 +105,10 @@ pub fn parse_envelope( let unsigned = make_unsigned(domain.as_bytes(), payload_type, &env.payload); - let pk = PublicKey::try_decode_protobuf(&*env.public_key.to_vec()).unwrap(); + let pk = PublicKey::try_decode_protobuf(&*env.public_key.to_vec())?; if !pk.verify(&unsigned, &env.signature) { - return Err("signature verification failed".into()); + return Err(SignatureVerification("signature verification failed".into())); } Ok(env) @@ -77,4 +130,5 @@ pub fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec< out } -pub use codec::Codec; \ No newline at end of file +pub use codec::Codec; +use crate::handshake::envelope::Error::SignatureVerification; \ No newline at end of file From 0c42c26cfdba6907cae3568bab0fa06a5adb5fca Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 14:01:14 +0100 Subject: [PATCH 22/63] simplify error with thiserror --- anchor/network/Cargo.toml | 1 + anchor/network/src/handshake/envelope/mod.rs | 58 ++++---------------- 2 files changed, 13 insertions(+), 46 deletions(-) diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index 9d737a78e..768c3bc17 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -38,6 +38,7 @@ thiserror = "1.0.69" prost = "0.13.4" async-trait = "0.1.85" rand = "0.8.5" +thiserror = "1.0.69" [dev-dependencies] async-channel = { workspace = true } diff --git a/anchor/network/src/handshake/envelope/mod.rs b/anchor/network/src/handshake/envelope/mod.rs index 0026a151d..31b1f709a 100644 --- a/anchor/network/src/handshake/envelope/mod.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -1,60 +1,26 @@ mod codec; +use crate::handshake::types::NodeInfo; use discv5::libp2p_identity::PublicKey; use libp2p::identity::DecodingError; use prost::{DecodeError, EncodeError, Message}; -use crate::handshake::types::{NodeInfo, UnmarshalError}; use std::error::Error as StdError; -use std::fmt; - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::Decode(e) => write!(f, "Decode error: {}", e), - Error::Encode(e) => write!(f, "Encode error: {}", e), - Error::PublicKeyDecoding(msg) => write!(f, "Public Key decoding error: {}", msg), - Error::SignatureVerification(msg) => write!(f, "Signature verification error: {}", msg), - } - } -} +use thiserror::Error; -impl StdError for Error { - // Optionally, override `source` if any variant wraps an underlying error. - fn source(&self) -> Option<&(dyn StdError + 'static)> { - match self { - Error::Decode(e) => Some(e), - Error::Encode(e) => Some(e), - Error::PublicKeyDecoding(_) => None, - Error::SignatureVerification(_) => None, - } - } -} - -#[derive(Debug)] +#[derive(Debug, Error)] pub enum Error { - Decode(DecodeError), - Encode(EncodeError), - PublicKeyDecoding(DecodingError), - SignatureVerification(String), -} + #[error("Decode error: {0}")] + Decode(#[from] DecodeError), // Automatically implements `From for Error` -impl From for Error { - fn from(error: DecodeError) -> Self { - Error::Decode(error) - } -} + #[error("Encode error: {0}")] + Encode(#[from] EncodeError), -impl From for Error { - fn from(error: EncodeError) -> Self { - Error::Encode(error) - } -} + #[error("Public Key Decoding error: {0}")] + PublicKeyDecoding(#[from] DecodingError), -impl From for Error { - fn from(error: DecodingError) -> Self { - Error::PublicKeyDecoding(error) - } + #[error("Signature Verification error: {0}")] + SignatureVerification(String), } /// The Envelope structure exactly matching Go's Envelope fields and tags: @@ -130,5 +96,5 @@ pub fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec< out } +use crate::handshake::envelope::Error::SignatureVerification; pub use codec::Codec; -use crate::handshake::envelope::Error::SignatureVerification; \ No newline at end of file From c0966352835edbfb31e6d3a1e5b933caf6d90db1 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 14:46:03 +0100 Subject: [PATCH 23/63] improve error handling --- .../network/src/handshake/envelope/codec.rs | 2 +- anchor/network/src/handshake/envelope/mod.rs | 2 +- anchor/network/src/handshake/error.rs | 16 +------ anchor/network/src/handshake/mod.rs | 4 +- .../src/handshake/{types.rs => node_info.rs} | 48 ++++++++++--------- anchor/network/src/network.rs | 2 +- 6 files changed, 33 insertions(+), 41 deletions(-) rename anchor/network/src/handshake/{types.rs => node_info.rs} (85%) diff --git a/anchor/network/src/handshake/envelope/codec.rs b/anchor/network/src/handshake/envelope/codec.rs index 6b564298c..39888b75d 100644 --- a/anchor/network/src/handshake/envelope/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -10,7 +10,7 @@ use prost::encoding::{decode_varint, encode_varint, encoded_len_varint}; use prost::Message; use tracing::debug; use crate::handshake::envelope; -use crate::handshake::types::NodeInfo; +use crate::handshake::node_info::NodeInfo; impl From for io::Error { diff --git a/anchor/network/src/handshake/envelope/mod.rs b/anchor/network/src/handshake/envelope/mod.rs index 31b1f709a..427aed197 100644 --- a/anchor/network/src/handshake/envelope/mod.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -1,6 +1,6 @@ mod codec; -use crate::handshake::types::NodeInfo; +use crate::handshake::node_info::NodeInfo; use discv5::libp2p_identity::PublicKey; use libp2p::identity::DecodingError; use prost::{DecodeError, EncodeError, Message}; diff --git a/anchor/network/src/handshake/error.rs b/anchor/network/src/handshake/error.rs index 1168ab855..a204f156d 100644 --- a/anchor/network/src/handshake/error.rs +++ b/anchor/network/src/handshake/error.rs @@ -1,22 +1,10 @@ use libp2p::request_response::{InboundFailure, OutboundFailure}; -use crate::handshake::types::UnmarshalError; +use crate::handshake::node_info::Error; #[derive(Debug)] pub enum HandshakeError { - InvalidSignature, - NetworkMismatch { ours: String, theirs: String }, - - SubnetsFormat, - - PeerRejected, - - Crypto(String), - - InvalidMessageFormat, - ResponseFailed, - - UnmarshalError(UnmarshalError), + UnmarshalError(Error), Inbound(InboundFailure), Outbound(OutboundFailure), } \ No newline at end of file diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index b383af3ca..928c8da58 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -1,4 +1,4 @@ -pub mod types; +pub mod node_info; mod envelope; mod error; @@ -22,7 +22,7 @@ use tracing::debug; use crate::handshake::envelope::{Envelope}; use crate::handshake::envelope::Codec; use crate::handshake::error::HandshakeError; -use crate::handshake::types::NodeInfo; +use crate::handshake::node_info::NodeInfo; pub trait NodeInfoProvider: Send + Sync { /// Returns a clone of the current node information. diff --git a/anchor/network/src/handshake/types.rs b/anchor/network/src/handshake/node_info.rs similarity index 85% rename from anchor/network/src/handshake/types.rs rename to anchor/network/src/handshake/node_info.rs index 0bbe3b3c5..8d577fd7b 100644 --- a/anchor/network/src/handshake/types.rs +++ b/anchor/network/src/handshake/node_info.rs @@ -1,9 +1,25 @@ use serde::{Deserialize, Serialize}; use serde_json; -use std::error::Error; -use discv5::libp2p_identity::Keypair; +use discv5::libp2p_identity::{Keypair, SigningError}; use crate::handshake::envelope::{make_unsigned, Envelope}; -use crate::handshake::types::UnmarshalError::ValidationError; + +use thiserror::Error; +use crate::handshake::node_info::Error::Validation; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("UTF-8 conversion error: {0}")] + Utf8(#[from] std::string::FromUtf8Error), + + #[error("Seal error: {0}")] + Seal(#[from] SigningError), + + #[error("Validation error: {0}")] + Validation(String), +} #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct NodeMetadata { @@ -30,18 +46,6 @@ struct Serializable { entries: Vec, } -#[derive(Debug)] -pub enum UnmarshalError { - SerializationError(String), - ValidationError(String), -} - -impl From for UnmarshalError { - fn from(error: serde_json::Error) -> Self { - UnmarshalError::SerializationError(error.to_string()) - } -} - impl NodeInfo { pub fn new(network_id: String, metadata: Option) -> Self { NodeInfo { @@ -55,7 +59,7 @@ impl NodeInfo { pub(crate) const CODEC: &'static [u8] = b"ssv/nodeinfo"; /// Serialize `NodeInfo` to JSON bytes. - fn marshal(&self) -> Result, Box> { + fn marshal(&self) -> Result, Error> { let mut entries = vec![ "".to_string(), // formerly forkVersion, now deprecated self.network_id.clone(), // network id @@ -73,10 +77,10 @@ impl NodeInfo { } /// Deserialize `NodeInfo` from JSON bytes, replacing `self`. - pub fn unmarshal(&mut self, data: &[u8]) -> Result<(), UnmarshalError> { + pub fn unmarshal(&mut self, data: &[u8]) -> Result<(), Error> { let ser: Serializable = serde_json::from_slice(data)?; if ser.entries.len() < 2 { - return Err(ValidationError("node info must have at least 2 entries".into())); + return Err(Validation("node info must have at least 2 entries".into())); } // skip ser.entries[0]: old forkVersion self.network_id = ser.entries[1].clone(); @@ -92,14 +96,14 @@ impl NodeInfo { /// 2) building "unsigned" data (domain + codec + payload), /// 3) signing with ed25519, /// 4) storing into `Envelope`. - pub fn seal(&self, keypair: &Keypair) -> Result> { + pub fn seal(&self, keypair: &Keypair) -> Result { let domain = Self::DOMAIN; if domain.is_empty() { - return Err("domain must not be empty".into()); + return Err(Validation("domain must not be empty".into())); } let payload_type = Self::CODEC; if payload_type.is_empty() { - return Err("payload_type must not be empty".into()); + return Err(Validation("payload_type must not be empty".into())); } // 1) marshal @@ -126,7 +130,7 @@ impl NodeInfo { mod tests { use libp2p::identity::Keypair; use crate::handshake::envelope::parse_envelope; - use crate::handshake::types::{NodeInfo, NodeMetadata}; + use crate::handshake::node_info::{NodeInfo, NodeMetadata}; #[test] fn test_node_info_seal_consume() { diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index b78eee38a..c73903773 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -27,7 +27,7 @@ use crate::handshake::{Behaviour, Event, NodeInfoProvider}; use crate::types::ssv_message::SignedSSVMessage; use lighthouse_network::EnrExt; use ssz::Decode; -use crate::handshake::types::{NodeInfo, NodeMetadata}; +use crate::handshake::node_info::{NodeInfo, NodeMetadata}; use subnet_tracker::{SubnetEvent, SubnetId}; use tokio::sync::mpsc; From 71f0de50d3d3438c4588a7f7054331ac5bf99b6f Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 14:47:54 +0100 Subject: [PATCH 24/63] rename HandshakeError --- anchor/network/src/handshake/error.rs | 10 --------- anchor/network/src/handshake/mod.rs | 31 ++++++++++++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) delete mode 100644 anchor/network/src/handshake/error.rs diff --git a/anchor/network/src/handshake/error.rs b/anchor/network/src/handshake/error.rs deleted file mode 100644 index a204f156d..000000000 --- a/anchor/network/src/handshake/error.rs +++ /dev/null @@ -1,10 +0,0 @@ -use libp2p::request_response::{InboundFailure, OutboundFailure}; -use crate::handshake::node_info::Error; - -#[derive(Debug)] -pub enum HandshakeError { - NetworkMismatch { ours: String, theirs: String }, - UnmarshalError(Error), - Inbound(InboundFailure), - Outbound(OutboundFailure), -} \ No newline at end of file diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 928c8da58..8678f29f6 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -1,12 +1,14 @@ pub mod node_info; mod envelope; -mod error; +use crate::handshake::envelope::Codec; +use crate::handshake::envelope::Envelope; +use crate::handshake::node_info::NodeInfo; use discv5::libp2p_identity::Keypair; use discv5::multiaddr::Multiaddr; use libp2p::core::transport::PortUse; use libp2p::core::Endpoint; -use libp2p::request_response::{self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, OutboundRequestId, ProtocolSupport, ResponseChannel}; +use libp2p::request_response::{self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, InboundFailure, OutboundFailure, OutboundRequestId, ProtocolSupport, ResponseChannel}; use libp2p::swarm::{ ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, @@ -14,15 +16,18 @@ use libp2p::swarm::{ use libp2p::{PeerId, StreamProtocol}; use prost::Message; use std::collections::HashMap; -use std::error::Error; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use std::time::Instant; use tracing::debug; -use crate::handshake::envelope::{Envelope}; -use crate::handshake::envelope::Codec; -use crate::handshake::error::HandshakeError; -use crate::handshake::node_info::NodeInfo; + +#[derive(Debug)] +pub enum Error { + NetworkMismatch { ours: String, theirs: String }, + UnmarshalError(crate::handshake::node_info::Error), + Inbound(InboundFailure), + Outbound(OutboundFailure), +} pub trait NodeInfoProvider: Send + Sync { /// Returns a clone of the current node information. @@ -33,7 +38,7 @@ pub trait NodeInfoProvider: Send + Sync { #[derive(Debug)] pub enum Event { Completed { peer_id: PeerId, their_info: NodeInfo }, - Failed { peer_id: PeerId, error: HandshakeError }, + Failed { peer_id: PeerId, error: Error }, } /// Network behaviour handling the handshake protocol. @@ -79,10 +84,10 @@ impl Behaviour &mut self, node_info: &NodeInfo, peer: PeerId, - ) -> Result<(), HandshakeError> { + ) -> Result<(), Error> { let ours = self.node_info_provider.get_node_info().network_id; if node_info.network_id != *ours { - return Err(HandshakeError::NetworkMismatch { ours, theirs: node_info.network_id.clone()}) + return Err(Error::NetworkMismatch { ours, theirs: node_info.network_id.clone()}) } Ok(()) } @@ -110,7 +115,7 @@ impl Behaviour if let Err(e) = their_info.unmarshal(&response.payload) { self.events.push(Event::Failed { peer_id, - error: HandshakeError::UnmarshalError(e), + error: Error::UnmarshalError(e), }); } @@ -221,13 +226,13 @@ impl NetworkBehaviour for Behaviour } => { self.events.push(Event::Failed { peer_id: peer, - error: HandshakeError::Outbound(error), + error: Error::Outbound(error), }); } RequestResponseEvent::InboundFailure { peer, error, .. } => { self.events.push(Event::Failed { peer_id: peer, - error: HandshakeError::Inbound(error), + error: Error::Inbound(error), }); } _ => {} From 7f47b7dab2f2e5f86d72e0e0605ba40f5e5f5a33 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 14:59:21 +0100 Subject: [PATCH 25/63] cargo clippy --- .../network/src/handshake/envelope/codec.rs | 5 ---- anchor/network/src/handshake/envelope/mod.rs | 3 +-- anchor/network/src/handshake/mod.rs | 25 +++++++------------ anchor/network/src/handshake/node_info.rs | 6 ----- 4 files changed, 10 insertions(+), 29 deletions(-) diff --git a/anchor/network/src/handshake/envelope/codec.rs b/anchor/network/src/handshake/envelope/codec.rs index 39888b75d..3469933b2 100644 --- a/anchor/network/src/handshake/envelope/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -5,13 +5,8 @@ use libp2p::request_response::Codec as RequestResponseCodec; use std::io; use async_trait::async_trait; use libp2p::StreamProtocol; -use prost::bytes::BytesMut; -use prost::encoding::{decode_varint, encode_varint, encoded_len_varint}; -use prost::Message; use tracing::debug; use crate::handshake::envelope; -use crate::handshake::node_info::NodeInfo; - impl From for io::Error { fn from(err: envelope::Error) -> io::Error { diff --git a/anchor/network/src/handshake/envelope/mod.rs b/anchor/network/src/handshake/envelope/mod.rs index 427aed197..e227327d1 100644 --- a/anchor/network/src/handshake/envelope/mod.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -5,7 +5,6 @@ use discv5::libp2p_identity::PublicKey; use libp2p::identity::DecodingError; use prost::{DecodeError, EncodeError, Message}; -use std::error::Error as StdError; use thiserror::Error; #[derive(Debug, Error)] @@ -71,7 +70,7 @@ pub fn parse_envelope( let unsigned = make_unsigned(domain.as_bytes(), payload_type, &env.payload); - let pk = PublicKey::try_decode_protobuf(&*env.public_key.to_vec())?; + let pk = PublicKey::try_decode_protobuf(&env.public_key.to_vec())?; if !pk.verify(&unsigned, &env.signature) { return Err(SignatureVerification("signature verification failed".into())); diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 8678f29f6..1ba7e5a4c 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -8,23 +8,19 @@ use discv5::libp2p_identity::Keypair; use discv5::multiaddr::Multiaddr; use libp2p::core::transport::PortUse; use libp2p::core::Endpoint; -use libp2p::request_response::{self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, InboundFailure, OutboundFailure, OutboundRequestId, ProtocolSupport, ResponseChannel}; +use libp2p::request_response::{self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, InboundFailure, OutboundFailure, ProtocolSupport, ResponseChannel}; use libp2p::swarm::{ ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, }; use libp2p::{PeerId, StreamProtocol}; -use prost::Message; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; -use std::time::Instant; use tracing::debug; #[derive(Debug)] pub enum Error { NetworkMismatch { ours: String, theirs: String }, - UnmarshalError(crate::handshake::node_info::Error), + NodeInfo(crate::handshake::node_info::Error), Inbound(InboundFailure), Outbound(OutboundFailure), } @@ -60,7 +56,7 @@ impl Behaviour local_node_info: Box, ) -> Self { // NodeInfoProtocol is the protocol.ID used for handshake - const NODE_INFO_PROTOCOL: &'static str = "/ssv/info/0.0.1"; + const NODE_INFO_PROTOCOL: &str = "/ssv/info/0.0.1"; let protocol = StreamProtocol::new(NODE_INFO_PROTOCOL); let behaviour = RequestResponseBehaviour::new([(protocol, ProtocolSupport::Full)], Config::default()); @@ -83,7 +79,6 @@ impl Behaviour fn verify_node_info( &mut self, node_info: &NodeInfo, - peer: PeerId, ) -> Result<(), Error> { let ours = self.node_info_provider.get_node_info().network_id; if node_info.network_id != *ours { @@ -99,14 +94,14 @@ impl Behaviour Ok(_) => { self.unmarshall_and_verify(peer_id, &response); } - Err(e) => { + Err(_) => { // There was an error sending the response. The InboundFailure handler will be called } } } - fn handle_handshake_response(&mut self, peer_id: PeerId, request_id: &OutboundRequestId, response: &Envelope) { - self.unmarshall_and_verify(peer_id, &response); + fn handle_handshake_response(&mut self, peer_id: PeerId, response: &Envelope) { + self.unmarshall_and_verify(peer_id, response); } fn unmarshall_and_verify(&mut self, peer_id: PeerId, response: &Envelope) { @@ -115,11 +110,11 @@ impl Behaviour if let Err(e) = their_info.unmarshal(&response.payload) { self.events.push(Event::Failed { peer_id, - error: Error::UnmarshalError(e), + error: Error::NodeInfo(e), }); } - match self.verify_node_info(&their_info, peer_id) { + match self.verify_node_info(&their_info) { Ok(_) => self.events.push(Event::Completed { peer_id, their_info }), Err(e) => self.events.push(Event::Failed { peer_id, @@ -210,16 +205,14 @@ impl NetworkBehaviour for Behaviour peer, message: request_response::Message::Response { - request_id, response, .. }, } => { debug!(?response, "Received handshake response"); - self.handle_handshake_response(peer, &request_id, &response); + self.handle_handshake_response(peer, &response); } RequestResponseEvent::OutboundFailure { - request_id, peer, error, .. diff --git a/anchor/network/src/handshake/node_info.rs b/anchor/network/src/handshake/node_info.rs index 8d577fd7b..3899ac539 100644 --- a/anchor/network/src/handshake/node_info.rs +++ b/anchor/network/src/handshake/node_info.rs @@ -98,13 +98,7 @@ impl NodeInfo { /// 4) storing into `Envelope`. pub fn seal(&self, keypair: &Keypair) -> Result { let domain = Self::DOMAIN; - if domain.is_empty() { - return Err(Validation("domain must not be empty".into())); - } let payload_type = Self::CODEC; - if payload_type.is_empty() { - return Err(Validation("payload_type must not be empty".into())); - } // 1) marshal let raw_payload = self.marshal()?; From 6411d4cb13006ed5794ab52f34e3dd5057c6fa83 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 15:13:50 +0100 Subject: [PATCH 26/63] fix request handling --- anchor/network/src/handshake/mod.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 1ba7e5a4c..dae335e19 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -90,14 +90,9 @@ impl Behaviour fn handle_handshake_request(&mut self, peer_id: PeerId, request: Envelope, channel: ResponseChannel) { // Handle incoming request: send response then verify let response = self.sealed_node_record(); - match self.behaviour.send_response(channel, response.clone()) { - Ok(_) => { - self.unmarshall_and_verify(peer_id, &response); - } - Err(_) => { - // There was an error sending the response. The InboundFailure handler will be called - } - } + let _ = self.behaviour.send_response(channel, response.clone()); // Any error here is handled by the InboundFailure handler + + self.unmarshall_and_verify(peer_id, &request); } fn handle_handshake_response(&mut self, peer_id: PeerId, response: &Envelope) { From 288e0a2c005542150592fbff50243ec6334c9d64 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 7 Feb 2025 16:39:20 +0100 Subject: [PATCH 27/63] fix problems after rebase --- anchor/network/Cargo.toml | 1 - anchor/network/src/behaviour.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index 768c3bc17..9d737a78e 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -38,7 +38,6 @@ thiserror = "1.0.69" prost = "0.13.4" async-trait = "0.1.85" rand = "0.8.5" -thiserror = "1.0.69" [dev-dependencies] async-channel = { workspace = true } diff --git a/anchor/network/src/behaviour.rs b/anchor/network/src/behaviour.rs index 13fb0762f..29afd102d 100644 --- a/anchor/network/src/behaviour.rs +++ b/anchor/network/src/behaviour.rs @@ -1,6 +1,5 @@ use crate::discovery::Discovery; -use libp2p::request_response::Behaviour; -use crate::handshake::behaviour::Behaviour; +use crate::handshake::Behaviour; use libp2p::swarm::NetworkBehaviour; use libp2p::{gossipsub, identify, ping}; From 4be67aa22ba9e590d8e72f0ba090556f5e4c0e91 Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 11:45:40 +0100 Subject: [PATCH 28/63] use quick-protobuf --- Cargo.lock | 25 +------ anchor/network/Cargo.toml | 4 +- .../network/src/handshake/envelope/codec.rs | 2 + .../src/handshake/envelope/envelope.proto | 8 +++ .../src/handshake/envelope/envelope.rs | 57 +++++++++++++++ anchor/network/src/handshake/envelope/mod.rs | 69 ++++++------------- anchor/network/src/handshake/node_info.rs | 13 +++- 7 files changed, 103 insertions(+), 75 deletions(-) create mode 100644 anchor/network/src/handshake/envelope/envelope.proto create mode 100644 anchor/network/src/handshake/envelope/envelope.rs diff --git a/Cargo.lock b/Cargo.lock index f9dca6443..782106b9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5171,7 +5171,7 @@ dependencies = [ "hex", "libp2p", "lighthouse_network", - "prost", + "quick-protobuf", "rand", "serde", "serde_json", @@ -5911,29 +5911,6 @@ dependencies = [ "syn 2.0.98", ] -[[package]] -name = "prost" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" -dependencies = [ - "anyhow", - "itertools 0.13.0", - "proc-macro2", - "quote", - "syn 2.0.98", -] - [[package]] name = "proto_array" version = "0.2.0" diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index 9d737a78e..31ea17b52 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -34,10 +34,10 @@ tracing = { workspace = true } types = { workspace = true } version = { workspace = true } serde_json = "1.0.137" -thiserror = "1.0.69" -prost = "0.13.4" async-trait = "0.1.85" rand = "0.8.5" +thiserror = "1.0.69" +quick-protobuf = "0.8.1" [dev-dependencies] async-channel = { workspace = true } diff --git a/anchor/network/src/handshake/envelope/codec.rs b/anchor/network/src/handshake/envelope/codec.rs index 3469933b2..a073cdb65 100644 --- a/anchor/network/src/handshake/envelope/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -51,6 +51,8 @@ impl RequestResponseCodec for Codec { { debug!("reading handshake response"); let mut msg_buf = Vec::new(); + // We don't need a varint here because we always read only one message in protocol. + // In this way we can just read until the end of the stream. let num_bytes_read = io.read_to_end(&mut msg_buf).await?; debug!(?num_bytes_read, "read handshake response"); diff --git a/anchor/network/src/handshake/envelope/envelope.proto b/anchor/network/src/handshake/envelope/envelope.proto new file mode 100644 index 000000000..2e1f668f2 --- /dev/null +++ b/anchor/network/src/handshake/envelope/envelope.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +message Envelope { + bytes public_key = 1; + bytes payload_type = 2; + bytes payload = 3; + bytes signature = 5; +} diff --git a/anchor/network/src/handshake/envelope/envelope.rs b/anchor/network/src/handshake/envelope/envelope.rs new file mode 100644 index 000000000..9f7746dee --- /dev/null +++ b/anchor/network/src/handshake/envelope/envelope.rs @@ -0,0 +1,57 @@ +// Automatically generated rust module for 'envelope.proto' file + +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(unused_imports)] +#![allow(unknown_lints)] +#![allow(clippy::all)] +#![cfg_attr(rustfmt, rustfmt_skip)] + +use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; +use quick_protobuf::sizeofs::*; +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Envelope { + pub public_key: Vec, + pub payload_type: Vec, + pub payload: Vec, + pub signature: Vec, +} + +impl<'a> MessageRead<'a> for Envelope { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.public_key = r.read_bytes(bytes)?.to_owned(), + Ok(18) => msg.payload_type = r.read_bytes(bytes)?.to_owned(), + Ok(26) => msg.payload = r.read_bytes(bytes)?.to_owned(), + Ok(42) => msg.signature = r.read_bytes(bytes)?.to_owned(), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for Envelope { + fn write_message(&self, w: &mut Writer) -> Result<()> { + if !self.public_key.is_empty() { w.write_with_tag(10, |w| w.write_bytes(&**&self.public_key))?; } + if !self.payload_type.is_empty() { w.write_with_tag(18, |w| w.write_bytes(&**&self.payload_type))?; } + if !self.payload.is_empty() { w.write_with_tag(26, |w| w.write_bytes(&**&self.payload))?; } + if !self.signature.is_empty() { w.write_with_tag(42, |w| w.write_bytes(&**&self.signature))?; } + Ok(()) + } + + fn get_size(&self) -> usize { + 0 + + if self.public_key.is_empty() { 0 } else { 1 + sizeof_len((&self.public_key).len()) } + + if self.payload_type.is_empty() { 0 } else { 1 + sizeof_len((&self.payload_type).len()) } + + if self.payload.is_empty() { 0 } else { 1 + sizeof_len((&self.payload).len()) } + + if self.signature.is_empty() { 0 } else { 1 + sizeof_len((&self.signature).len()) } + } +} diff --git a/anchor/network/src/handshake/envelope/mod.rs b/anchor/network/src/handshake/envelope/mod.rs index e227327d1..5cb09ddde 100644 --- a/anchor/network/src/handshake/envelope/mod.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -1,19 +1,16 @@ mod codec; +mod envelope; use crate::handshake::node_info::NodeInfo; use discv5::libp2p_identity::PublicKey; use libp2p::identity::DecodingError; -use prost::{DecodeError, EncodeError, Message}; - +use quick_protobuf::{Writer, Error as ProtoError, BytesReader, MessageRead, MessageWrite}; use thiserror::Error; #[derive(Debug, Error)] pub enum Error { - #[error("Decode error: {0}")] - Decode(#[from] DecodeError), // Automatically implements `From for Error` - - #[error("Encode error: {0}")] - Encode(#[from] EncodeError), + #[error("Coding error: {0}")] + Coding(#[from] ProtoError), // Automatically implements `From for Error` #[error("Public Key Decoding error: {0}")] PublicKeyDecoding(#[from] DecodingError), @@ -22,40 +19,20 @@ pub enum Error { SignatureVerification(String), } -/// The Envelope structure exactly matching Go's Envelope fields and tags: -/// 1 => public_key -/// 2 => payload_type -/// 3 => payload -/// 4 => signature -/// -/// All are `bytes`, just like in Go. -#[derive(Clone, PartialEq, Message)] -pub struct Envelope { - #[prost(bytes = "vec", tag = "1")] - pub public_key: Vec, - - #[prost(bytes = "vec", tag = "2")] - pub payload_type: Vec, - - #[prost(bytes = "vec", tag = "3")] - pub payload: Vec, - - #[prost(bytes = "vec", tag = "5")] - pub signature: Vec, -} - - impl Envelope { /// Encode the Envelope to a Protobuf byte array (like `proto.Marshal` in Go). pub fn encode_to_vec(&self) -> Result, Error> { - let mut buf = Vec::with_capacity(self.encoded_len()); - self.encode(&mut buf)?; + let mut buf = Vec::new(); + let mut writer = Writer::new(&mut buf); + self.write_message(&mut writer)?; Ok(buf) } /// Decode an Envelope from a Protobuf byte array (like `proto.Unmarshal` in Go). pub fn decode_from_slice(data: &[u8]) -> Result { - Envelope::decode(data).map_err(Error::from) + let mut reader = BytesReader::from_bytes(&data); + let env = Envelope::from_reader(&mut reader, &data).map_err(Error::Coding)?; + Ok(env) } } @@ -72,28 +49,24 @@ pub fn parse_envelope( let pk = PublicKey::try_decode_protobuf(&env.public_key.to_vec())?; - if !pk.verify(&unsigned, &env.signature) { + if !pk.verify(&unsigned?, &env.signature) { return Err(SignatureVerification("signature verification failed".into())); } Ok(env) } -pub fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Vec { - use prost::encoding::encode_varint; - let mut out = Vec::new(); - - encode_varint(domain.len() as u64, &mut out); - out.extend_from_slice(domain); - - encode_varint(payload_type.len() as u64, &mut out); - out.extend_from_slice(payload_type); - - encode_varint(payload.len() as u64, &mut out); - out.extend_from_slice(payload); - - out +pub fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Result, ProtoError> { + let mut buf = Vec::new(); + { + let mut writer = Writer::new(&mut buf); + writer.write_bytes(domain)?; + writer.write_bytes(payload_type)?; + writer.write_bytes(payload)?; + } + Ok(buf) } use crate::handshake::envelope::Error::SignatureVerification; pub use codec::Codec; +pub use envelope::Envelope; diff --git a/anchor/network/src/handshake/node_info.rs b/anchor/network/src/handshake/node_info.rs index 3899ac539..51309f86c 100644 --- a/anchor/network/src/handshake/node_info.rs +++ b/anchor/network/src/handshake/node_info.rs @@ -104,7 +104,7 @@ impl NodeInfo { let raw_payload = self.marshal()?; // 2) build the "unsigned" data - let unsigned = make_unsigned(domain.as_bytes(), payload_type, &raw_payload); + let unsigned = make_unsigned(domain.as_bytes(), payload_type, &raw_payload).unwrap(); // 3) sign let sig = keypair.sign(&unsigned)?; @@ -149,6 +149,17 @@ mod tests { parsed_node_info.unmarshal(&parsed_env.payload).expect("TODO: panic message"); assert_eq!(node_info, parsed_node_info); + + let encoded= + hex::decode("0a250802122102ba6a707dcec6c60ba2793d52123d34b22556964fc798d4aa88ffc41\ + a00e42407120c7373762f6e6f6465696e666f1aa5017b22456e7472696573223a5b22222c22686f6c65736b7\ + 9222c227b5c224e6f646556657273696f6e5c223a5c22676574682f785c222c5c22457865637574696f6e4e6f64655c223a5c22676574682f785c222c5c22436f6e73656e7375734e6f64655c223a5c22707279736d2f785c222c5c225375626e6574735c223a5c2230303030303030303030303030303030303030303030303030303030303030305c227d225d7d2a473045022100b8a2a668113330369e74b86ec818a87009e2a351f7ee4c0e431e1f659dd1bc3f02202b1ebf418efa7fb0541f77703bea8563234a1b70b8391d43daa40b6e7c3fcc84").unwrap(); + + let parsed_env = parse_envelope(&encoded).expect("Consume failed"); + let mut parsed_node_info = NodeInfo::default(); + parsed_node_info.unmarshal(&parsed_env.payload).expect("TODO: panic message"); + + assert_eq!(node_info, parsed_node_info); } #[test] From cb9363cf9b76c36efde667102ed765cc9160eb02 Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 11:52:49 +0100 Subject: [PATCH 29/63] cargo fmt --- .../network/src/handshake/envelope/codec.rs | 6 +- anchor/network/src/handshake/envelope/mod.rs | 16 ++-- anchor/network/src/handshake/mod.rs | 75 ++++++++++--------- anchor/network/src/handshake/node_info.rs | 37 +++++---- anchor/network/src/network.rs | 8 +- 5 files changed, 80 insertions(+), 62 deletions(-) diff --git a/anchor/network/src/handshake/envelope/codec.rs b/anchor/network/src/handshake/envelope/codec.rs index a073cdb65..ecb518037 100644 --- a/anchor/network/src/handshake/envelope/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -1,12 +1,12 @@ +use crate::handshake::envelope; use crate::handshake::envelope::{parse_envelope, Envelope}; +use async_trait::async_trait; use futures::{AsyncReadExt, AsyncWriteExt}; use libp2p::futures::{AsyncRead, AsyncWrite}; use libp2p::request_response::Codec as RequestResponseCodec; -use std::io; -use async_trait::async_trait; use libp2p::StreamProtocol; +use std::io; use tracing::debug; -use crate::handshake::envelope; impl From for io::Error { fn from(err: envelope::Error) -> io::Error { diff --git a/anchor/network/src/handshake/envelope/mod.rs b/anchor/network/src/handshake/envelope/mod.rs index 5cb09ddde..018091171 100644 --- a/anchor/network/src/handshake/envelope/mod.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -4,7 +4,7 @@ mod envelope; use crate::handshake::node_info::NodeInfo; use discv5::libp2p_identity::PublicKey; use libp2p::identity::DecodingError; -use quick_protobuf::{Writer, Error as ProtoError, BytesReader, MessageRead, MessageWrite}; +use quick_protobuf::{BytesReader, Error as ProtoError, MessageRead, MessageWrite, Writer}; use thiserror::Error; #[derive(Debug, Error)] @@ -37,9 +37,7 @@ impl Envelope { } /// Consumes an Envelope => verify signature => parse the record. -pub fn parse_envelope( - bytes: &[u8], -) -> Result { +pub fn parse_envelope(bytes: &[u8]) -> Result { let env = Envelope::decode_from_slice(bytes)?; let domain = NodeInfo::DOMAIN; @@ -50,13 +48,19 @@ pub fn parse_envelope( let pk = PublicKey::try_decode_protobuf(&env.public_key.to_vec())?; if !pk.verify(&unsigned?, &env.signature) { - return Err(SignatureVerification("signature verification failed".into())); + return Err(SignatureVerification( + "signature verification failed".into(), + )); } Ok(env) } -pub fn make_unsigned(domain: &[u8], payload_type: &[u8], payload: &[u8]) -> Result, ProtoError> { +pub fn make_unsigned( + domain: &[u8], + payload_type: &[u8], + payload: &[u8], +) -> Result, ProtoError> { let mut buf = Vec::new(); { let mut writer = Writer::new(&mut buf); diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index dae335e19..92dce6cba 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -1,5 +1,5 @@ -pub mod node_info; mod envelope; +pub mod node_info; use crate::handshake::envelope::Codec; use crate::handshake::envelope::Envelope; @@ -8,7 +8,10 @@ use discv5::libp2p_identity::Keypair; use discv5::multiaddr::Multiaddr; use libp2p::core::transport::PortUse; use libp2p::core::Endpoint; -use libp2p::request_response::{self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, InboundFailure, OutboundFailure, ProtocolSupport, ResponseChannel}; +use libp2p::request_response::{ + self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, + InboundFailure, OutboundFailure, ProtocolSupport, ResponseChannel, +}; use libp2p::swarm::{ ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, @@ -33,8 +36,14 @@ pub trait NodeInfoProvider: Send + Sync { /// Event emitted on handshake completion or failure. #[derive(Debug)] pub enum Event { - Completed { peer_id: PeerId, their_info: NodeInfo }, - Failed { peer_id: PeerId, error: Error }, + Completed { + peer_id: PeerId, + their_info: NodeInfo, + }, + Failed { + peer_id: PeerId, + error: Error, + }, } /// Network behaviour handling the handshake protocol. @@ -49,17 +58,14 @@ pub struct Behaviour { events: Vec, } -impl Behaviour -{ - pub fn new( - keypair: Keypair, - local_node_info: Box, - ) -> Self { +impl Behaviour { + pub fn new(keypair: Keypair, local_node_info: Box) -> Self { // NodeInfoProtocol is the protocol.ID used for handshake const NODE_INFO_PROTOCOL: &str = "/ssv/info/0.0.1"; let protocol = StreamProtocol::new(NODE_INFO_PROTOCOL); - let behaviour = RequestResponseBehaviour::new([(protocol, ProtocolSupport::Full)], Config::default()); + let behaviour = + RequestResponseBehaviour::new([(protocol, ProtocolSupport::Full)], Config::default()); Self { behaviour, @@ -76,18 +82,23 @@ impl Behaviour } /// Verify an incoming envelope and apply filters. - fn verify_node_info( - &mut self, - node_info: &NodeInfo, - ) -> Result<(), Error> { + fn verify_node_info(&mut self, node_info: &NodeInfo) -> Result<(), Error> { let ours = self.node_info_provider.get_node_info().network_id; if node_info.network_id != *ours { - return Err(Error::NetworkMismatch { ours, theirs: node_info.network_id.clone()}) + return Err(Error::NetworkMismatch { + ours, + theirs: node_info.network_id.clone(), + }); } Ok(()) } - fn handle_handshake_request(&mut self, peer_id: PeerId, request: Envelope, channel: ResponseChannel) { + fn handle_handshake_request( + &mut self, + peer_id: PeerId, + request: Envelope, + channel: ResponseChannel, + ) { // Handle incoming request: send response then verify let response = self.sealed_node_record(); let _ = self.behaviour.send_response(channel, response.clone()); // Any error here is handled by the InboundFailure handler @@ -110,18 +121,18 @@ impl Behaviour } match self.verify_node_info(&their_info) { - Ok(_) => self.events.push(Event::Completed { peer_id, their_info }), - Err(e) => self.events.push(Event::Failed { + Ok(_) => self.events.push(Event::Completed { peer_id, - error: e, + their_info, }), + Err(e) => self.events.push(Event::Failed { peer_id, error: e }), } } } -impl NetworkBehaviour for Behaviour -{ - type ConnectionHandler = as NetworkBehaviour>::ConnectionHandler; +impl NetworkBehaviour for Behaviour { + type ConnectionHandler = + as NetworkBehaviour>::ConnectionHandler; type ToSwarm = Event; fn handle_established_inbound_connection( @@ -189,29 +200,21 @@ impl NetworkBehaviour for Behaviour RequestResponseEvent::Message { peer, message: - request_response::Message::Request { - request, channel, .. - }, + request_response::Message::Request { + request, channel, .. + }, } => { debug!("Received handshake request"); self.handle_handshake_request(peer, request, channel); } RequestResponseEvent::Message { peer, - message: - request_response::Message::Response { - response, - .. - }, + message: request_response::Message::Response { response, .. }, } => { debug!(?response, "Received handshake response"); self.handle_handshake_response(peer, &response); } - RequestResponseEvent::OutboundFailure { - peer, - error, - .. - } => { + RequestResponseEvent::OutboundFailure { peer, error, .. } => { self.events.push(Event::Failed { peer_id: peer, error: Error::Outbound(error), diff --git a/anchor/network/src/handshake/node_info.rs b/anchor/network/src/handshake/node_info.rs index 51309f86c..88b4d90e0 100644 --- a/anchor/network/src/handshake/node_info.rs +++ b/anchor/network/src/handshake/node_info.rs @@ -1,10 +1,10 @@ +use crate::handshake::envelope::{make_unsigned, Envelope}; +use discv5::libp2p_identity::{Keypair, SigningError}; use serde::{Deserialize, Serialize}; use serde_json; -use discv5::libp2p_identity::{Keypair, SigningError}; -use crate::handshake::envelope::{make_unsigned, Envelope}; -use thiserror::Error; use crate::handshake::node_info::Error::Validation; +use thiserror::Error; #[derive(Debug, Error)] pub enum Error { @@ -61,8 +61,8 @@ impl NodeInfo { /// Serialize `NodeInfo` to JSON bytes. fn marshal(&self) -> Result, Error> { let mut entries = vec![ - "".to_string(), // formerly forkVersion, now deprecated - self.network_id.clone(), // network id + "".to_string(), // formerly forkVersion, now deprecated + self.network_id.clone(), // network id ]; if let Some(meta) = &self.metadata { @@ -96,7 +96,7 @@ impl NodeInfo { /// 2) building "unsigned" data (domain + codec + payload), /// 3) signing with ed25519, /// 4) storing into `Envelope`. - pub fn seal(&self, keypair: &Keypair) -> Result { + pub fn seal(&self, keypair: &Keypair) -> Result { let domain = Self::DOMAIN; let payload_type = Self::CODEC; @@ -122,9 +122,9 @@ impl NodeInfo { #[cfg(test)] mod tests { - use libp2p::identity::Keypair; use crate::handshake::envelope::parse_envelope; use crate::handshake::node_info::{NodeInfo, NodeMetadata}; + use libp2p::identity::Keypair; #[test] fn test_node_info_seal_consume() { @@ -140,13 +140,17 @@ mod tests { ); // Marshal the NodeInfo into bytes - let envelope = node_info.seal(&Keypair::generate_secp256k1()).expect("Seal failed"); + let envelope = node_info + .seal(&Keypair::generate_secp256k1()) + .expect("Seal failed"); let data = envelope.encode_to_vec().unwrap(); let parsed_env = parse_envelope(&data).expect("Consume failed"); let mut parsed_node_info = NodeInfo::default(); - parsed_node_info.unmarshal(&parsed_env.payload).expect("TODO: panic message"); + parsed_node_info + .unmarshal(&parsed_env.payload) + .expect("TODO: panic message"); assert_eq!(node_info, parsed_node_info); @@ -157,7 +161,9 @@ mod tests { let parsed_env = parse_envelope(&encoded).expect("Consume failed"); let mut parsed_node_info = NodeInfo::default(); - parsed_node_info.unmarshal(&parsed_env.payload).expect("TODO: panic message"); + parsed_node_info + .unmarshal(&parsed_env.payload) + .expect("TODO: panic message"); assert_eq!(node_info, parsed_node_info); } @@ -180,16 +186,19 @@ mod tests { }; // 1) Marshal current_data - let data = current_data.marshal() + let data = current_data + .marshal() .expect("marshal_record should succeed"); // 2) Unmarshal into parsed_rec let mut parsed_rec = NodeInfo::default(); - parsed_rec.unmarshal(&data) + parsed_rec + .unmarshal(&data) .expect("unmarshal_record should succeed"); // 3) Now unmarshal the old format data into the same struct - parsed_rec.unmarshal(old_serialized_data) + parsed_rec + .unmarshal(old_serialized_data) .expect("unmarshal old data should succeed"); // 4) Compare @@ -197,4 +206,4 @@ mod tests { // We can do the same in Rust using assert_eq. assert_eq!(current_data, parsed_rec); } -} \ No newline at end of file +} diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index c73903773..a93518835 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -23,11 +23,11 @@ use crate::keypair_utils::load_private_key; use crate::transport::build_transport; use crate::Config; +use crate::handshake::node_info::{NodeInfo, NodeMetadata}; use crate::handshake::{Behaviour, Event, NodeInfoProvider}; use crate::types::ssv_message::SignedSSVMessage; use lighthouse_network::EnrExt; use ssz::Decode; -use crate::handshake::node_info::{NodeInfo, NodeMetadata}; use subnet_tracker::{SubnetEvent, SubnetId}; use tokio::sync::mpsc; @@ -220,7 +220,10 @@ fn subnet_to_topic(subnet: SubnetId) -> IdentTopic { fn handle_handshake_event(ev: Event) { match ev { - Event::Completed { peer_id, their_info } => { + Event::Completed { + peer_id, + their_info, + } => { debug!(%peer_id, ?their_info, "Handshake completed"); // Update peer store with their_info } @@ -362,7 +365,6 @@ fn build_swarm( .build() } - pub struct DefaultNodeInfoProvider { node_info: Arc>, } From 399499cdbb49b07571e54faf7bcd7601c570f75b Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 11:53:06 +0100 Subject: [PATCH 30/63] cargo sort --- anchor/network/Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index 31ea17b52..e0aa6923c 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -5,6 +5,7 @@ edition = { workspace = true } authors = ["Sigma Prime "] [dependencies] +async-trait = "0.1.85" dirs = { workspace = true } discv5 = { workspace = true } ethereum_ssz = "0.8.1" @@ -25,19 +26,18 @@ libp2p = { version = "0.54", default-features = false, features = [ "request-response", ] } lighthouse_network = { workspace = true } +quick-protobuf = "0.8.1" +rand = "0.8.5" serde = { workspace = true } +serde_json = "1.0.137" ssz_types = "0.10" subnet_tracker = { workspace = true } task_executor = { workspace = true } +thiserror = "1.0.69" tokio = { workspace = true } tracing = { workspace = true } types = { workspace = true } version = { workspace = true } -serde_json = "1.0.137" -async-trait = "0.1.85" -rand = "0.8.5" -thiserror = "1.0.69" -quick-protobuf = "0.8.1" [dev-dependencies] async-channel = { workspace = true } From 3ac485b1b0d30919064ebc473b1bbb022984adba Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 11:54:08 +0100 Subject: [PATCH 31/63] cargo clippy --- anchor/network/src/handshake/envelope/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anchor/network/src/handshake/envelope/mod.rs b/anchor/network/src/handshake/envelope/mod.rs index 018091171..afa91fc0b 100644 --- a/anchor/network/src/handshake/envelope/mod.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -30,8 +30,8 @@ impl Envelope { /// Decode an Envelope from a Protobuf byte array (like `proto.Unmarshal` in Go). pub fn decode_from_slice(data: &[u8]) -> Result { - let mut reader = BytesReader::from_bytes(&data); - let env = Envelope::from_reader(&mut reader, &data).map_err(Error::Coding)?; + let mut reader = BytesReader::from_bytes(data); + let env = Envelope::from_reader(&mut reader, data).map_err(Error::Coding)?; Ok(env) } } From 2a47eb0c49c16f5530fd5a9b2d7e12721647bb84 Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 13:56:58 +0100 Subject: [PATCH 32/63] limit the msg payload while reading --- anchor/network/src/handshake/envelope/codec.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/anchor/network/src/handshake/envelope/codec.rs b/anchor/network/src/handshake/envelope/codec.rs index ecb518037..7312ac7c6 100644 --- a/anchor/network/src/handshake/envelope/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -8,6 +8,8 @@ use libp2p::StreamProtocol; use std::io; use tracing::debug; +const MAXIMUM_SIZE: u64 = 1024; + impl From for io::Error { fn from(err: envelope::Error) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, err) @@ -34,7 +36,8 @@ impl RequestResponseCodec for Codec { { debug!("reading handsake request"); let mut msg_buf = Vec::new(); - let num_bytes_read = io.read_to_end(&mut msg_buf).await?; + let num_bytes_read = io.take(MAXIMUM_SIZE).read_to_end(&mut msg_buf).await?; + // TODO potentially try to read one more byte here and create a "message too large error" debug!(?num_bytes_read, "read handshake request"); let env = Envelope::decode_from_slice(&msg_buf)?; debug!(?env, "decoded handshake request"); @@ -53,7 +56,7 @@ impl RequestResponseCodec for Codec { let mut msg_buf = Vec::new(); // We don't need a varint here because we always read only one message in protocol. // In this way we can just read until the end of the stream. - let num_bytes_read = io.read_to_end(&mut msg_buf).await?; + let num_bytes_read = io.take(MAXIMUM_SIZE).read_to_end(&mut msg_buf).await?; debug!(?num_bytes_read, "read handshake response"); let env = parse_envelope(&msg_buf)?; From a2611e15941f44118d43b9df4d75abec2e2a126f Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 13:57:05 +0100 Subject: [PATCH 33/63] protocol spec --- anchor/network/src/handshake/README.md | 272 +++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 anchor/network/src/handshake/README.md diff --git a/anchor/network/src/handshake/README.md b/anchor/network/src/handshake/README.md new file mode 100644 index 000000000..0bdc0c8ec --- /dev/null +++ b/anchor/network/src/handshake/README.md @@ -0,0 +1,272 @@ +# SSV NodeInfo Handshake Protocol Specification + +This document specifies the **SSV NodeInfo Handshake Protocol**. The protocol is used by SSV-based nodes to exchange basic node metadata and validate each other's identity when establishing a connection over Libp2p under a dedicated protocol ID. + +--- + +## Table of Contents + +- [1. Introduction](#1-introduction) +- [2. Definitions](#2-definitions) + - [2.1 Terminology](#21-terminology) + - [2.2 Domain Separation](#22-domain-separation) +- [3. Protocol Constants](#3-protocol-constants) +- [4. Data Structures](#4-data-structures) + - [4.1 Envelope](#41-envelope) + - [4.2 NodeInfo](#42-nodeinfo) + - [4.3 NodeMetadata](#43-nodemetadata) +- [5. Serialization and Signing](#5-serialization-and-signing) + - [5.1 Envelope Fields](#51-envelope-fields) + - [5.2 NodeInfo JSON Layout](#52-nodeinfo-json-layout) + - [5.3 Signature Preparation](#53-signature-preparation) +- [6. Handshake Protocol Flows](#6-handshake-protocol-flows) + - [6.1 Protocol ID](#61-protocol-id) + - [6.2 Request Phase](#62-request-phase) + - [6.3 Response Phase](#63-response-phase) + - [6.4 Network Mismatch Checks](#64-network-mismatch-checks) +- [7. Security Considerations](#7-security-considerations) +- [8. Rationale and Notes](#8-rationale-and-notes) +- [9. Examples](#9-examples) + +--- + +## 1. Introduction + +The SSV NodeInfo Handshake Protocol defines how two SSV nodes exchange, sign, and verify each other's **NodeInfo**, which includes a `network_id` (such as "holesky", "prater", etc.) and optional metadata about node software versions or subnets. The protocol uses a request-response style handshake over Libp2p under a dedicated protocol ID. + +The high-level handshake steps are: + +1. **Requester** sends an Envelope (containing its NodeInfo) to the peer. +2. **Responder** verifies this Envelope, checks the `network_id`, and replies with its own Envelope. +3. **Requester** verifies the responder's Envelope. +4. Both sides proceed if verification succeeds; otherwise, the handshake is considered failed. + +--- + +## 2. Definitions + +### 2.1 Terminology + +| **Term** | **Definition** | +|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Envelope** | A Protobuf-encoded message containing a `public_key`, `payload_type`, `payload`, and `signature` (covering a domain-separated concatenation of fields). | +| **NodeInfo** | A JSON-based structure holding key node attributes like `network_id` plus optional metadata. | +| **Handshake**| The request-response exchange of Envelopes between two nodes at connection time. | + +### 2.2 Domain Separation + +- **Domain**: `"ssv"`. + Used to separate signatures for different contexts or protocols. + +--- + +## 3. Protocol Constants + +| **Name** | **Value** | **Description** | +|----------------|--------------------|------------------------------------------------------| +| `DOMAIN` | `ssv` | Fixed ASCII text used during signature generation. | +| `PAYLOAD_TYPE` | `ssv/nodeinfo` | Identifies the payload as an SSV NodeInfo structure. | +| `PROTOCOL_ID` | `/ssv/info/0.0.1` | Libp2p protocol ID used for the handshake. | + +--- + +## 4. Data Structures + +### 4.1 Envelope + +The Envelope is a Protobuf message: + +```protobuf +message Envelope { + bytes public_key = 1; + bytes payload_type = 2; + bytes payload = 3; + bytes signature = 5; +} +``` + +### 4.2 NodeInfo + +``` +NodeInfo: +- network_id: String +- metadata: NodeMetadata (optional) +``` + +### 4.3 NodeMetadata + +``` +NodeMetadata: +- node_version: String +- execution_node: String +- consensus_node: String +- subnets: String +``` + +--- + +## 5. Serialization and Signing + +### 5.1 Envelope Fields + +1. **public_key** + - Sender’s public key in serialized form (e.g., compressed Secp256k1 or raw Ed25519 bytes). + - The public key is encoded and decoded using Protobuf. + - For reference, Libp2p has a [Peer Ids and Keys](https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md), which may be consulted for consistent handling across implementations. + +2. **payload_type** + - MUST be `"ssv/nodeinfo"` in this protocol. + - Used to identify how to interpret `payload`. + +3. **payload** + - Contains `NodeInfo` data in JSON (described below). + +4. **signature** + - A cryptographic signature covering `DOMAIN || payload_type || payload`. + +### 5.2 NodeInfo JSON Layout + +Internally, the protocol uses a “legacy” layout for `NodeInfo` serialization, with a top-level JSON structure: + +```json +{ + "Entries": [ + "", // (Index 0) Old forkVersion, not used + "", // (Index 1) The NodeInfo.network_id + "" // (Index 2) if NodeMetadata is present + ] +} +``` + +- If the array has fewer than 2 entries, the payload is invalid. +- If the array has 3 entries, the 3rd entry is a JSON object for metadata, for example: + +```json +{ + "NodeVersion": "...", + "ExecutionNode": "...", + "ConsensusNode": "...", + "Subnets": "..." +} +``` + +### 5.3 Signature Preparation + +To **sign** an Envelope, implementations: + +1. Construct the unsigned message: + ``` + unsigned_message = DOMAIN || payload_type || payload + ``` +2. Sign `unsigned_message` using the node’s private key. +3. Write the resulting signature to `signature`. + +To **verify** an Envelope: + +1. Recompute the `unsigned_message`. +2. Verify using `public_key` against `signature`. + +If verification fails, the handshake **MUST** abort. + +--- + +## 6. Handshake Protocol Flows + +### 6.1 Protocol ID + +Both peers must speak the protocol identified by: + +``` +/ssv/info/0.0.1 +``` + +### 6.2 Request Phase + +1. **Build Envelope** + - The initiating node (Requester) serializes its `NodeInfo` into JSON (the `payload`). + - Sets `payload_type = "ssv/nodeinfo"`. + - Prepends `DOMAIN = "ssv"` when computing the signature. + - Places the resulting `public_key` and `signature` into the Envelope. + +2. **Send Request** + - The requester sends this Envelope as the request. + +3. **Wait for Response** + - The requester awaits the single response from the Responder. + +### 6.3 Response Phase + +1. **Receive & Verify** + - The responder verifies the incoming Envelope: + - Check signature correctness. + - Extract `NodeInfo`. + - Validate `network_id` if necessary (see [6.4](#64-network-mismatch-checks)). + +2. **Build Response** + - If valid, the responder builds and signs its own Envelope containing its `NodeInfo`. + +3. **Send Response** + - The responder sends the Envelope back to the requester. + +4. **Requester Verifies** + - The requester verifies the signature, parses `NodeInfo`, and checks `network_id`. + +### 6.4 Network Mismatch Checks + +- Implementations **MUST** check whether the received `NodeInfo`’s `network_id` matches their local `network_id`. +- If they mismatch, the implementation **SHOULD** reject the connection. + +--- + +## 7. Security Considerations + +- **Signature Validation** is mandatory. Any failure to verify the Envelope’s signature indicates an invalid handshake. +- **Public Key Authenticity**: The Envelope’s `public_key` is not implicitly trusted. It must match the verified signature. +- **Network Mismatch**: Avoid bridging distinct SSV or Ethereum networks. Peers claiming the wrong `network_id` should be rejected. +- **Payload Size**: Although `NodeInfo` is generally small, implementations **SHOULD** impose a maximum bound for payload. Any request or response exceeding this size limit **SHOULD** be rejected. + +--- + +## 8. Rationale and Notes + +- Using a Protobuf-based Envelope simplifies cross-language interoperability. +- The domain separation string (`"ssv"`) prevents signature reuse in other contexts. +- The “legacy” `Entries` layout ensures backward-compatibility with older SSV implementations. + +--- + +## 9. Examples + +### 9.1 Example Envelope in Hex + +An example Envelope could be hex-encoded as: + +``` +0a250802122102ba6a707dcec6c60ba2793d52123d34b22556964fc798d4aa88ffc41a00e42407120c7373762f6e6f6465696e666f1aa5017b22456e7472696573223a5b22222c22686f6c65736b79222c227b5c224e6f646556657273696f6e5c223a5c22676574682f785c222c5c22457865637574696f6e4e6f64655c223a5c22676574682f785c222c5c22436f6e73656e7375734e6f64655c223a5c22707279736d2f785c222c5c225375626e6574735c223a5c2230303030303030303030303030303030303030303030303030303030303030303030305c227d225d7d2a473045022100b8a2a668113330369e74b86ec818a87009e2a351f7ee4c0e431e1f659dd1bc3f02202b1ebf418efa7fb0541f77703bea8563234a1b70b8391d43daa40b6e7c3fcc84 +``` + +Decoding reveals (high-level view): + +``` +Envelope { + public_key = , + payload_type = "ssv/nodeinfo", + payload = { + "Entries": [ + "", + "holesky", + "{\"NodeVersion\":\"geth/x\",\"ExecutionNode\":\"geth/x\",\"ConsensusNode\":\"prysm/x\",\"Subnets\":\"00000000000000000000000000000000\"}" + ] + }, + signature = +} +``` + +### 9.2 Verifying the Envelope + +1. Recompute: `domain = "ssv"` + ``` + unsigned_message = "ssv" || "ssv/nodeinfo" || payload_bytes + ``` +2. Verify signature with `public_key`. +3. Parse payload JSON => parse `NodeInfo` => check `network_id`. From b53d49e47a49ba16b0619bfa4c57e2c57c2e28ec Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 14:04:08 +0100 Subject: [PATCH 34/63] remove deps --- Cargo.lock | 2 -- anchor/network/Cargo.toml | 2 -- 2 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 782106b9b..d07a8bad2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5172,7 +5172,6 @@ dependencies = [ "libp2p", "lighthouse_network", "quick-protobuf", - "rand", "serde", "serde_json", "ssz_types 0.10.0", @@ -5181,7 +5180,6 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tracing", - "types", "version", ] diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index e0aa6923c..52b146eda 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -27,7 +27,6 @@ libp2p = { version = "0.54", default-features = false, features = [ ] } lighthouse_network = { workspace = true } quick-protobuf = "0.8.1" -rand = "0.8.5" serde = { workspace = true } serde_json = "1.0.137" ssz_types = "0.10" @@ -36,7 +35,6 @@ task_executor = { workspace = true } thiserror = "1.0.69" tokio = { workspace = true } tracing = { workspace = true } -types = { workspace = true } version = { workspace = true } [dev-dependencies] From aab349e0e2aa84b37a945ef69b078eb648d53bc5 Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 14:16:51 +0100 Subject: [PATCH 35/63] remove println! --- anchor/network/src/network.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index a93518835..20a762dde 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -288,7 +288,6 @@ async fn build_anchor_behaviour( }; let domain = format!("0x{}", hex::encode(vec![0x0, 0x0, 0x5, 0x2])); - println!("Domain: {}", domain); let node_info = NodeInfo::new( domain, Some(NodeMetadata { From cc5d0e3fdcef6aa56eb2f4824a1e1b3f0990e1e4 Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 14:25:41 +0100 Subject: [PATCH 36/63] update comments --- anchor/network/src/handshake/envelope/codec.rs | 2 +- anchor/network/src/handshake/envelope/mod.rs | 4 ++-- anchor/network/src/handshake/mod.rs | 1 - anchor/network/src/handshake/node_info.rs | 8 ++------ anchor/network/src/network.rs | 2 +- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/anchor/network/src/handshake/envelope/codec.rs b/anchor/network/src/handshake/envelope/codec.rs index 7312ac7c6..175d5397d 100644 --- a/anchor/network/src/handshake/envelope/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -54,7 +54,7 @@ impl RequestResponseCodec for Codec { { debug!("reading handshake response"); let mut msg_buf = Vec::new(); - // We don't need a varint here because we always read only one message in protocol. + // We don't need a varint here because we always read only one message in the protocol. // In this way we can just read until the end of the stream. let num_bytes_read = io.take(MAXIMUM_SIZE).read_to_end(&mut msg_buf).await?; debug!(?num_bytes_read, "read handshake response"); diff --git a/anchor/network/src/handshake/envelope/mod.rs b/anchor/network/src/handshake/envelope/mod.rs index afa91fc0b..e9d458179 100644 --- a/anchor/network/src/handshake/envelope/mod.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -10,7 +10,7 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum Error { #[error("Coding error: {0}")] - Coding(#[from] ProtoError), // Automatically implements `From for Error` + Coding(#[from] ProtoError), // Automatically implements `From for Error` #[error("Public Key Decoding error: {0}")] PublicKeyDecoding(#[from] DecodingError), @@ -36,7 +36,7 @@ impl Envelope { } } -/// Consumes an Envelope => verify signature => parse the record. +/// Decodes an Envelope and verify signature. pub fn parse_envelope(bytes: &[u8]) -> Result { let env = Envelope::decode_from_slice(bytes)?; diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 92dce6cba..adecc975f 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -81,7 +81,6 @@ impl Behaviour { node_info.seal(&self.keypair).unwrap() } - /// Verify an incoming envelope and apply filters. fn verify_node_info(&mut self, node_info: &NodeInfo) -> Result<(), Error> { let ours = self.node_info_provider.get_node_info().network_id; if node_info.network_id != *ours { diff --git a/anchor/network/src/handshake/node_info.rs b/anchor/network/src/handshake/node_info.rs index 88b4d90e0..9ba53bddb 100644 --- a/anchor/network/src/handshake/node_info.rs +++ b/anchor/network/src/handshake/node_info.rs @@ -94,22 +94,18 @@ impl NodeInfo { /// Seals a `Record` into an Envelope by: /// 1) marshalling record to bytes, /// 2) building "unsigned" data (domain + codec + payload), - /// 3) signing with ed25519, + /// 3) signing, /// 4) storing into `Envelope`. pub fn seal(&self, keypair: &Keypair) -> Result { let domain = Self::DOMAIN; let payload_type = Self::CODEC; - // 1) marshal let raw_payload = self.marshal()?; - // 2) build the "unsigned" data let unsigned = make_unsigned(domain.as_bytes(), payload_type, &raw_payload).unwrap(); - // 3) sign let sig = keypair.sign(&unsigned)?; - // 4) build Envelope let env = Envelope { public_key: keypair.public().encode_protobuf(), payload_type: payload_type.to_vec(), @@ -139,7 +135,7 @@ mod tests { }), ); - // Marshal the NodeInfo into bytes + // Marshal the NodeInfo into bytes and wrap it into an Envelope let envelope = node_info .seal(&Keypair::generate_secp256k1()) .expect("Seal failed"); diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 20a762dde..4a94c7c21 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -378,7 +378,7 @@ impl DefaultNodeInfoProvider { impl NodeInfoProvider for DefaultNodeInfoProvider { fn get_node_info(&self) -> NodeInfo { - // In a real implementation, consider handling lock poisoning. + // TODO consider handling lock poisoning. self.node_info.lock().unwrap().clone() } } From c26750e215a342bd74a5017baf6e449e50dcff4c Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 14:54:55 +0100 Subject: [PATCH 37/63] update wordlist.txt --- .github/wordlist.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 097487156..2a2be478a 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -47,4 +47,14 @@ Holesky Mainnet lifecycle Syncer - +JSON +Protobuf +Responder +responder +Prepends +Secp +NodeMetadata +NodeInfo +subnets +holesky +responder's \ No newline at end of file From c75a225c00391eb9b712899fe8c4376e044c7796 Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 15:01:05 +0100 Subject: [PATCH 38/63] ignore case in spellcheck --- .github/spellcheck.yml | 1 + .github/wordlist.txt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/spellcheck.yml b/.github/spellcheck.yml index f786756cf..58bb1abd0 100644 --- a/.github/spellcheck.yml +++ b/.github/spellcheck.yml @@ -2,6 +2,7 @@ matrix: - name: Markdown aspell: lang: en + ignore-case: true dictionary: wordlists: - .github/wordlist.txt diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 2a2be478a..9c479cfe3 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -49,7 +49,6 @@ lifecycle Syncer JSON Protobuf -Responder responder Prepends Secp From d9791ce2c2572286e666c91e283974e585c888c7 Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 15:27:07 +0100 Subject: [PATCH 39/63] Revert "ignore case in spellcheck" This reverts commit c75a225c00391eb9b712899fe8c4376e044c7796. --- .github/spellcheck.yml | 1 - .github/wordlist.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/spellcheck.yml b/.github/spellcheck.yml index 58bb1abd0..f786756cf 100644 --- a/.github/spellcheck.yml +++ b/.github/spellcheck.yml @@ -2,7 +2,6 @@ matrix: - name: Markdown aspell: lang: en - ignore-case: true dictionary: wordlists: - .github/wordlist.txt diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 9c479cfe3..2a2be478a 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -49,6 +49,7 @@ lifecycle Syncer JSON Protobuf +Responder responder Prepends Secp From 40dc739cfa11538be67f6feaaf7b9ffd988a61e7 Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 16:05:04 +0100 Subject: [PATCH 40/63] changes after review --- anchor/network/src/behaviour.rs | 4 ++-- anchor/network/src/handshake/mod.rs | 2 +- anchor/network/src/handshake/node_info.rs | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/anchor/network/src/behaviour.rs b/anchor/network/src/behaviour.rs index 29afd102d..7f5364b00 100644 --- a/anchor/network/src/behaviour.rs +++ b/anchor/network/src/behaviour.rs @@ -1,5 +1,5 @@ use crate::discovery::Discovery; -use crate::handshake::Behaviour; +use crate::handshake; use libp2p::swarm::NetworkBehaviour; use libp2p::{gossipsub, identify, ping}; @@ -14,5 +14,5 @@ pub struct AnchorBehaviour { /// Discv5 Discovery protocol. pub discovery: Discovery, - pub handshake: Behaviour, + pub handshake: handshake::Behaviour, } diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index adecc975f..82e3ea03a 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -23,7 +23,7 @@ use tracing::debug; #[derive(Debug)] pub enum Error { NetworkMismatch { ours: String, theirs: String }, - NodeInfo(crate::handshake::node_info::Error), + NodeInfo(node_info::Error), Inbound(InboundFailure), Outbound(OutboundFailure), } diff --git a/anchor/network/src/handshake/node_info.rs b/anchor/network/src/handshake/node_info.rs index 9ba53bddb..78a8ba0b8 100644 --- a/anchor/network/src/handshake/node_info.rs +++ b/anchor/network/src/handshake/node_info.rs @@ -11,9 +11,6 @@ pub enum Error { #[error("Serialization error: {0}")] Serialization(#[from] serde_json::Error), - #[error("UTF-8 conversion error: {0}")] - Utf8(#[from] std::string::FromUtf8Error), - #[error("Seal error: {0}")] Seal(#[from] SigningError), @@ -66,8 +63,7 @@ impl NodeInfo { ]; if let Some(meta) = &self.metadata { - let raw_meta = serde_json::to_vec(meta)?; - entries.push(String::from_utf8(raw_meta)?); + entries.push(serde_json::to_string(meta)?); } // Serialize as JSON From ca67c05bcd03b10d538c654a15e0ba468e8f269c Mon Sep 17 00:00:00 2001 From: diego Date: Mon, 10 Feb 2025 18:00:30 +0100 Subject: [PATCH 41/63] use ssv_network_config --- anchor/client/src/config.rs | 4 +++- .../built_in_network_configs/holesky/ssv_domain_type.txt | 1 + .../built_in_network_configs/mainnet/ssv_domain_type.txt | 1 + anchor/common/ssv_network_config/src/lib.rs | 8 +++++++- anchor/network/src/config.rs | 3 +++ anchor/network/src/network.rs | 4 ++-- anchor/src/main.rs | 3 ++- 7 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 anchor/common/ssv_network_config/built_in_network_configs/holesky/ssv_domain_type.txt create mode 100644 anchor/common/ssv_network_config/built_in_network_configs/mainnet/ssv_domain_type.txt diff --git a/anchor/client/src/config.rs b/anchor/client/src/config.rs index 29c811634..644f5e746 100644 --- a/anchor/client/src/config.rs +++ b/anchor/client/src/config.rs @@ -16,7 +16,7 @@ pub const DEFAULT_EXECUTION_NODE_WS: &str = "ws://localhost:8545/"; /// The default Data directory, relative to the users home directory pub const DEFAULT_ROOT_DIR: &str = ".anchor"; /// Default network, used to partition the data storage -pub const DEFAULT_HARDCODED_NETWORK: &str = "mainnet"; +pub const DEFAULT_HARDCODED_NETWORK: &str = "holesky"; /// Base directory name for unnamed testnets passed through the --testnet-dir flag pub const CUSTOM_TESTNET_DIR: &str = "custom"; @@ -103,8 +103,10 @@ impl Config { /// `cli_args`. pub fn from_cli(cli_args: &Anchor) -> Result { let eth2_network = if let Some(testnet_dir) = &cli_args.testnet_dir { + println!("Loading testnet from {:?}", testnet_dir); SsvNetworkConfig::load(testnet_dir.clone()) } else { + println!("Loading default network {:?}", cli_args.network); SsvNetworkConfig::constant(&cli_args.network) .and_then(|net| net.ok_or_else(|| format!("Unknown network {}", cli_args.network))) }?; diff --git a/anchor/common/ssv_network_config/built_in_network_configs/holesky/ssv_domain_type.txt b/anchor/common/ssv_network_config/built_in_network_configs/holesky/ssv_domain_type.txt new file mode 100644 index 000000000..ec919db56 --- /dev/null +++ b/anchor/common/ssv_network_config/built_in_network_configs/holesky/ssv_domain_type.txt @@ -0,0 +1 @@ +0x00000502 \ No newline at end of file diff --git a/anchor/common/ssv_network_config/built_in_network_configs/mainnet/ssv_domain_type.txt b/anchor/common/ssv_network_config/built_in_network_configs/mainnet/ssv_domain_type.txt new file mode 100644 index 000000000..3ee928e4d --- /dev/null +++ b/anchor/common/ssv_network_config/built_in_network_configs/mainnet/ssv_domain_type.txt @@ -0,0 +1 @@ +0x00000001 \ No newline at end of file diff --git a/anchor/common/ssv_network_config/src/lib.rs b/anchor/common/ssv_network_config/src/lib.rs index 7ef0463ab..10d592291 100644 --- a/anchor/common/ssv_network_config/src/lib.rs +++ b/anchor/common/ssv_network_config/src/lib.rs @@ -22,6 +22,7 @@ macro_rules! get_hardcoded { include_str_for_net!($network, "ssv_boot_enr.yaml"), include_str_for_net!($network, "ssv_contract_address.txt"), include_str_for_net!($network, "ssv_contract_block.txt"), + include_str_for_net!($network, "ssv_domain_type.txt"), ) }; } @@ -32,11 +33,12 @@ pub struct SsvNetworkConfig { pub ssv_boot_nodes: Option>>, pub ssv_contract: Address, pub ssv_contract_block: u64, + pub ssv_domain_type: String } impl SsvNetworkConfig { pub fn constant(name: &str) -> Result, String> { - let (enr_yaml, address, block) = match name { + let (enr_yaml, address, block, domain_type) = match name { "mainnet" => get_hardcoded!(mainnet), "holesky" => get_hardcoded!(holesky), _ => return Ok(None), @@ -55,6 +57,9 @@ impl SsvNetworkConfig { ssv_contract_block: block .parse() .map_err(|_| "Unable to parse built-in block!")?, + ssv_domain_type: domain_type + .parse() + .map_err(|_| "Unable to parse built-in domain type!")?, })) } @@ -76,6 +81,7 @@ impl SsvNetworkConfig { ssv_boot_nodes, ssv_contract: read(&base_dir.join("ssv_contract_address.txt"))?, ssv_contract_block: read(&base_dir.join("ssv_contract_block.txt"))?, + ssv_domain_type: read(&base_dir.join("domain_type.txt"))?, eth2_network: Eth2NetworkConfig::load(base_dir)?, }) } diff --git a/anchor/network/src/config.rs b/anchor/network/src/config.rs index 3587da760..160c59020 100644 --- a/anchor/network/src/config.rs +++ b/anchor/network/src/config.rs @@ -66,6 +66,8 @@ pub struct Config { /// Target number of connected peers. pub target_peers: usize, + + pub domain_type: String, } impl Default for Config { @@ -100,6 +102,7 @@ impl Default for Config { disable_discovery: false, disable_quic_support: false, topics: vec![], + domain_type: "".to_string(), } } } diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 4a94c7c21..9872a35d1 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -287,9 +287,9 @@ async fn build_anchor_behaviour( discovery }; - let domain = format!("0x{}", hex::encode(vec![0x0, 0x0, 0x5, 0x2])); + print!("Domain: {}", network_config.domain_type); let node_info = NodeInfo::new( - domain, + network_config.domain_type.clone(), Some(NodeMetadata { node_version: "1.0.0".to_string(), execution_node: "geth/v1.10.8".to_string(), diff --git a/anchor/src/main.rs b/anchor/src/main.rs index dd4cb8266..d38b648b3 100644 --- a/anchor/src/main.rs +++ b/anchor/src/main.rs @@ -22,13 +22,14 @@ fn main() { // Currently the only binary is the client. We build the client config, but later this will // generalise to other sub commands // Build the client config - let config = match config::from_cli(&anchor_config) { + let mut config = match config::from_cli(&anchor_config) { Ok(config) => config, Err(e) => { error!(e, "Unable to initialize configuration"); return; } }; + config.network.domain_type = config.ssv_network.ssv_domain_type.clone(); // Build the core task executor let core_executor = environment.executor(); From d38776de9bc1ceb5ce861481d42ee34871c587f1 Mon Sep 17 00:00:00 2001 From: diego Date: Tue, 11 Feb 2025 19:33:25 +0100 Subject: [PATCH 42/63] create and use DomainType --- Cargo.lock | 1 + anchor/common/ssv_network_config/src/lib.rs | 34 +++++++++++++++++++-- anchor/network/Cargo.toml | 1 + anchor/network/src/config.rs | 8 ++--- anchor/network/src/network.rs | 5 +-- 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d07a8bad2..4d0cc2d76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5174,6 +5174,7 @@ dependencies = [ "quick-protobuf", "serde", "serde_json", + "ssv_network_config", "ssz_types 0.10.0", "subnet_tracker", "task_executor", diff --git a/anchor/common/ssv_network_config/src/lib.rs b/anchor/common/ssv_network_config/src/lib.rs index 10d592291..ad4fbb6cb 100644 --- a/anchor/common/ssv_network_config/src/lib.rs +++ b/anchor/common/ssv_network_config/src/lib.rs @@ -4,6 +4,7 @@ use eth2_network_config::Eth2NetworkConfig; use std::fs::File; use std::path::{Path, PathBuf}; use std::str::FromStr; +use alloy::hex; macro_rules! include_str_for_net { ($network:ident, $file:literal) => { @@ -27,13 +28,42 @@ macro_rules! get_hardcoded { }; } +#[derive(Clone, Debug)] +pub struct DomainType(pub [u8; 4]); + +impl FromStr for DomainType { + type Err = String; + + fn from_str(hex_str: &str) -> Result { + let bytes = hex::decode(hex_str).map_err(|_| "Invalid domain type hex")?; + if bytes.len() != 4 { + return Err("Domain type must be 4 bytes".into()); + } + let mut domain_type = [0; 4]; + domain_type.copy_from_slice(&bytes); + Ok(Self(domain_type)) + } +} + +impl From for String { + fn from(domain_type: DomainType) -> Self { + hex::encode(&domain_type.0) + } +} + +impl Default for DomainType { + fn default() -> Self { + Self([0; 4]) + } +} + #[derive(Clone, Debug)] pub struct SsvNetworkConfig { pub eth2_network: Eth2NetworkConfig, pub ssv_boot_nodes: Option>>, pub ssv_contract: Address, pub ssv_contract_block: u64, - pub ssv_domain_type: String + pub ssv_domain_type: DomainType, } impl SsvNetworkConfig { @@ -81,7 +111,7 @@ impl SsvNetworkConfig { ssv_boot_nodes, ssv_contract: read(&base_dir.join("ssv_contract_address.txt"))?, ssv_contract_block: read(&base_dir.join("ssv_contract_block.txt"))?, - ssv_domain_type: read(&base_dir.join("domain_type.txt"))?, + ssv_domain_type: read(&base_dir.join("ssv_domain_type.txt"))?, eth2_network: Eth2NetworkConfig::load(base_dir)?, }) } diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index 52b146eda..49035f6e9 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -36,6 +36,7 @@ thiserror = "1.0.69" tokio = { workspace = true } tracing = { workspace = true } version = { workspace = true } +ssv_network_config = { workspace = true } [dev-dependencies] async-channel = { workspace = true } diff --git a/anchor/network/src/config.rs b/anchor/network/src/config.rs index 160c59020..fc6acbc83 100644 --- a/anchor/network/src/config.rs +++ b/anchor/network/src/config.rs @@ -2,10 +2,10 @@ use discv5::Enr; use libp2p::Multiaddr; use lighthouse_network::types::GossipKind; use lighthouse_network::{ListenAddr, ListenAddress}; -use serde::{Deserialize, Serialize}; use std::net::{Ipv4Addr, Ipv6Addr}; use std::num::NonZeroU16; use std::path::PathBuf; +use ssv_network_config::DomainType; /// This is a default network directory, but it will be overridden by the cli defaults. const DEFAULT_NETWORK_DIR: &str = ".anchor/network"; @@ -16,7 +16,7 @@ pub const DEFAULT_DISC_PORT: u16 = 9100u16; pub const DEFAULT_QUIC_PORT: u16 = 9101u16; /// Configuration for setting up the p2p network. -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone)] pub struct Config { /// Data directory where node's keyfile is stored pub network_dir: PathBuf, @@ -67,7 +67,7 @@ pub struct Config { /// Target number of connected peers. pub target_peers: usize, - pub domain_type: String, + pub domain_type: DomainType, } impl Default for Config { @@ -102,7 +102,7 @@ impl Default for Config { disable_discovery: false, disable_quic_support: false, topics: vec![], - domain_type: "".to_string(), + domain_type: DomainType::default(), } } } diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 9872a35d1..456704ba3 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -287,9 +287,10 @@ async fn build_anchor_behaviour( discovery }; - print!("Domain: {}", network_config.domain_type); + let domain_type: String = network_config.clone().domain_type.into(); + print!("Domain: {}", domain_type); let node_info = NodeInfo::new( - network_config.domain_type.clone(), + domain_type, Some(NodeMetadata { node_version: "1.0.0".to_string(), execution_node: "geth/v1.10.8".to_string(), From db25bae4ff4351d3a68437975fbc600c5f83d3ea Mon Sep 17 00:00:00 2001 From: diego Date: Tue, 11 Feb 2025 19:54:27 +0100 Subject: [PATCH 43/63] remove print --- anchor/client/src/config.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/anchor/client/src/config.rs b/anchor/client/src/config.rs index 644f5e746..f972a11d4 100644 --- a/anchor/client/src/config.rs +++ b/anchor/client/src/config.rs @@ -103,10 +103,8 @@ impl Config { /// `cli_args`. pub fn from_cli(cli_args: &Anchor) -> Result { let eth2_network = if let Some(testnet_dir) = &cli_args.testnet_dir { - println!("Loading testnet from {:?}", testnet_dir); SsvNetworkConfig::load(testnet_dir.clone()) } else { - println!("Loading default network {:?}", cli_args.network); SsvNetworkConfig::constant(&cli_args.network) .and_then(|net| net.ok_or_else(|| format!("Unknown network {}", cli_args.network))) }?; From 5fd4866fbb8c12dc35a432414ada857b57b7f151 Mon Sep 17 00:00:00 2001 From: diego Date: Tue, 11 Feb 2025 20:09:26 +0100 Subject: [PATCH 44/63] cargo fmt and clippy --- anchor/common/ssv_network_config/src/lib.rs | 12 +++--------- anchor/network/src/config.rs | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/anchor/common/ssv_network_config/src/lib.rs b/anchor/common/ssv_network_config/src/lib.rs index ad4fbb6cb..6d33b4b96 100644 --- a/anchor/common/ssv_network_config/src/lib.rs +++ b/anchor/common/ssv_network_config/src/lib.rs @@ -1,10 +1,10 @@ +use alloy::hex; use alloy::primitives::Address; use enr::{CombinedKey, Enr}; use eth2_network_config::Eth2NetworkConfig; use std::fs::File; use std::path::{Path, PathBuf}; use std::str::FromStr; -use alloy::hex; macro_rules! include_str_for_net { ($network:ident, $file:literal) => { @@ -28,7 +28,7 @@ macro_rules! get_hardcoded { }; } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct DomainType(pub [u8; 4]); impl FromStr for DomainType { @@ -47,13 +47,7 @@ impl FromStr for DomainType { impl From for String { fn from(domain_type: DomainType) -> Self { - hex::encode(&domain_type.0) - } -} - -impl Default for DomainType { - fn default() -> Self { - Self([0; 4]) + hex::encode(domain_type.0) } } diff --git a/anchor/network/src/config.rs b/anchor/network/src/config.rs index fc6acbc83..ec67e1efb 100644 --- a/anchor/network/src/config.rs +++ b/anchor/network/src/config.rs @@ -2,10 +2,10 @@ use discv5::Enr; use libp2p::Multiaddr; use lighthouse_network::types::GossipKind; use lighthouse_network::{ListenAddr, ListenAddress}; +use ssv_network_config::DomainType; use std::net::{Ipv4Addr, Ipv6Addr}; use std::num::NonZeroU16; use std::path::PathBuf; -use ssv_network_config::DomainType; /// This is a default network directory, but it will be overridden by the cli defaults. const DEFAULT_NETWORK_DIR: &str = ".anchor/network"; From 026d99fe0d9dbb8ceefa395877be5440c2342f83 Mon Sep 17 00:00:00 2001 From: diego Date: Tue, 11 Feb 2025 20:48:51 +0100 Subject: [PATCH 45/63] cargo sort --- anchor/network/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index 49035f6e9..5c6d4105b 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -29,6 +29,7 @@ lighthouse_network = { workspace = true } quick-protobuf = "0.8.1" serde = { workspace = true } serde_json = "1.0.137" +ssv_network_config = { workspace = true } ssz_types = "0.10" subnet_tracker = { workspace = true } task_executor = { workspace = true } @@ -36,7 +37,6 @@ thiserror = "1.0.69" tokio = { workspace = true } tracing = { workspace = true } version = { workspace = true } -ssv_network_config = { workspace = true } [dev-dependencies] async-channel = { workspace = true } From cb4b102ae1f96e93412c4ad631bf2c129350d2ed Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 12 Feb 2025 14:05:15 +0100 Subject: [PATCH 46/63] only send handshake request if we initiated the connection --- anchor/network/src/handshake/mod.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 82e3ea03a..9592ce45e 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -7,7 +7,7 @@ use crate::handshake::node_info::NodeInfo; use discv5::libp2p_identity::Keypair; use discv5::multiaddr::Multiaddr; use libp2p::core::transport::PortUse; -use libp2p::core::Endpoint; +use libp2p::core::{ConnectedPoint, Endpoint}; use libp2p::request_response::{ self, Behaviour as RequestResponseBehaviour, Config, Event as RequestResponseEvent, InboundFailure, OutboundFailure, ProtocolSupport, ResponseChannel, @@ -169,9 +169,12 @@ impl NetworkBehaviour for Behaviour { fn on_swarm_event(&mut self, event: FromSwarm) { // Initiate handshake on new connection if let FromSwarm::ConnectionEstablished(conn_est) = &event { - let peer = conn_est.peer_id; - let request = self.sealed_node_record(); - self.behaviour.send_request(&peer, request); + // Only send handshake request if we initiated the connection (outbound) + if let ConnectedPoint::Dialer { .. } = conn_est.endpoint { + let peer = conn_est.peer_id; + let request = self.sealed_node_record(); + self.behaviour.send_request(&peer, request); + } } // Delegate other events to inner behaviour From 195e7423662468cdd6677d9810572a84ba5d591e Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 12 Feb 2025 14:10:32 +0100 Subject: [PATCH 47/63] remove trait and create NodeInfoManager --- anchor/network/src/handshake/mod.rs | 16 ++++++---------- anchor/network/src/network.rs | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 9592ce45e..a0dabebe6 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -19,6 +19,7 @@ use libp2p::swarm::{ use libp2p::{PeerId, StreamProtocol}; use std::task::{Context, Poll}; use tracing::debug; +use crate::network::NodeInfoManager; #[derive(Debug)] pub enum Error { @@ -28,11 +29,6 @@ pub enum Error { Outbound(OutboundFailure), } -pub trait NodeInfoProvider: Send + Sync { - /// Returns a clone of the current node information. - fn get_node_info(&self) -> NodeInfo; -} - /// Event emitted on handshake completion or failure. #[derive(Debug)] pub enum Event { @@ -53,13 +49,13 @@ pub struct Behaviour { /// Keypair for signing envelopes. keypair: Keypair, /// Local node's information provider. - node_info_provider: Box, + node_info_manager: NodeInfoManager, /// Events to emit. events: Vec, } impl Behaviour { - pub fn new(keypair: Keypair, local_node_info: Box) -> Self { + pub fn new(keypair: Keypair, local_node_info: NodeInfoManager) -> Self { // NodeInfoProtocol is the protocol.ID used for handshake const NODE_INFO_PROTOCOL: &str = "/ssv/info/0.0.1"; @@ -70,19 +66,19 @@ impl Behaviour { Self { behaviour, keypair, - node_info_provider: local_node_info, + node_info_manager: local_node_info, events: Vec::new(), } } /// Create a signed envelope containing local node info. fn sealed_node_record(&self) -> Envelope { - let node_info = self.node_info_provider.get_node_info(); + let node_info = self.node_info_manager.get_node_info(); node_info.seal(&self.keypair).unwrap() } fn verify_node_info(&mut self, node_info: &NodeInfo) -> Result<(), Error> { - let ours = self.node_info_provider.get_node_info().network_id; + let ours = self.node_info_manager.get_node_info().network_id; if node_info.network_id != *ours { return Err(Error::NetworkMismatch { ours, diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 456704ba3..641e21748 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -24,7 +24,7 @@ use crate::transport::build_transport; use crate::Config; use crate::handshake::node_info::{NodeInfo, NodeMetadata}; -use crate::handshake::{Behaviour, Event, NodeInfoProvider}; +use crate::handshake::{Behaviour, Event}; use crate::types::ssv_message::SignedSSVMessage; use lighthouse_network::EnrExt; use ssz::Decode; @@ -300,7 +300,7 @@ async fn build_anchor_behaviour( ); let handshake = Behaviour::new( local_keypair.clone(), - Box::new(DefaultNodeInfoProvider::new(node_info)), + NodeInfoManager::new(node_info), ); AnchorBehaviour { @@ -365,23 +365,25 @@ fn build_swarm( .build() } -pub struct DefaultNodeInfoProvider { +pub struct NodeInfoManager { node_info: Arc>, } -impl DefaultNodeInfoProvider { +impl NodeInfoManager { pub fn new(node_info: NodeInfo) -> Self { Self { node_info: Arc::new(Mutex::new(node_info)), } } -} - -impl NodeInfoProvider for DefaultNodeInfoProvider { - fn get_node_info(&self) -> NodeInfo { + pub fn get_node_info(&self) -> NodeInfo { // TODO consider handling lock poisoning. self.node_info.lock().unwrap().clone() } + + pub fn set_node_info(&self, node_info: NodeInfo) { + // TODO consider handling lock poisoning. + *self.node_info.lock().unwrap() = node_info; + } } #[cfg(test)] From 2954f08bdd058572c84596a8fb7b11fd864d60ed Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 12 Feb 2025 14:53:07 +0100 Subject: [PATCH 48/63] cargo fmt --- anchor/network/src/handshake/mod.rs | 2 +- anchor/network/src/network.rs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index a0dabebe6..264cea52c 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -4,6 +4,7 @@ pub mod node_info; use crate::handshake::envelope::Codec; use crate::handshake::envelope::Envelope; use crate::handshake::node_info::NodeInfo; +use crate::network::NodeInfoManager; use discv5::libp2p_identity::Keypair; use discv5::multiaddr::Multiaddr; use libp2p::core::transport::PortUse; @@ -19,7 +20,6 @@ use libp2p::swarm::{ use libp2p::{PeerId, StreamProtocol}; use std::task::{Context, Poll}; use tracing::debug; -use crate::network::NodeInfoManager; #[derive(Debug)] pub enum Error { diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 641e21748..a35d5e29e 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -298,10 +298,7 @@ async fn build_anchor_behaviour( subnets: "ffffffffffffffffffffffffffffffff".to_string(), }), ); - let handshake = Behaviour::new( - local_keypair.clone(), - NodeInfoManager::new(node_info), - ); + let handshake = Behaviour::new(local_keypair.clone(), NodeInfoManager::new(node_info)); AnchorBehaviour { identify, From 4e12f5ed213dc04f60b25cb9111e34c481075d29 Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 12 Feb 2025 16:05:58 +0100 Subject: [PATCH 49/63] Bubble up all other ToSwarm events --- anchor/network/src/handshake/mod.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 264cea52c..343d35ad6 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -226,18 +226,10 @@ impl NetworkBehaviour for Behaviour { } _ => {} }, - ToSwarm::NotifyHandler { - peer_id, - handler, - event, - } => { - return Poll::Ready(ToSwarm::NotifyHandler { - peer_id, - handler, - event, - }); + other => { + // Bubble up all other ToSwarm events. The closure is unreachable because we already handled GenerateEvent + return Poll::Ready(other.map_out(|_| { unreachable!("We already handled GenerateEvent") })); } - _ => {} } } From 04cc5bfc2709a49bb5e9d0d192e427f803d82f9f Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 12 Feb 2025 16:40:31 +0100 Subject: [PATCH 50/63] use RwLock instead of Mutex --- anchor/network/src/network.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index a35d5e29e..705b7152e 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -1,6 +1,6 @@ use std::num::{NonZeroU8, NonZeroUsize}; use std::pin::Pin; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; use futures::StreamExt; @@ -363,23 +363,25 @@ fn build_swarm( } pub struct NodeInfoManager { - node_info: Arc>, + node_info: Arc>, } impl NodeInfoManager { pub fn new(node_info: NodeInfo) -> Self { Self { - node_info: Arc::new(Mutex::new(node_info)), + node_info: Arc::new(RwLock::new(node_info)), } } + pub fn get_node_info(&self) -> NodeInfo { - // TODO consider handling lock poisoning. - self.node_info.lock().unwrap().clone() + // Using unwrap() here will panic if the lock is poisoned. + // We might choose to handle the error more gracefully. + self.node_info.read().unwrap().clone() } pub fn set_node_info(&self, node_info: NodeInfo) { - // TODO consider handling lock poisoning. - *self.node_info.lock().unwrap() = node_info; + // Using unwrap() here will panic if the lock is poisoned. + *self.node_info.write().unwrap() = node_info; } } From 9626f47c2db8e24ea3b009cf8952f9c32c71c069 Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 12 Feb 2025 19:33:25 +0100 Subject: [PATCH 51/63] cargo fmt --- anchor/network/src/handshake/mod.rs | 4 +++- anchor/network/src/network.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index 343d35ad6..fa76868d3 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -228,7 +228,9 @@ impl NetworkBehaviour for Behaviour { }, other => { // Bubble up all other ToSwarm events. The closure is unreachable because we already handled GenerateEvent - return Poll::Ready(other.map_out(|_| { unreachable!("We already handled GenerateEvent") })); + return Poll::Ready( + other.map_out(|_| unreachable!("We already handled GenerateEvent")), + ); } } } diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 705b7152e..f37be1be3 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -1,6 +1,6 @@ use std::num::{NonZeroU8, NonZeroUsize}; use std::pin::Pin; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Arc, RwLock}; use std::time::Duration; use futures::StreamExt; From 0c09ca4522e1ae8c6405c0f7f95e4e4b1de2d427 Mon Sep 17 00:00:00 2001 From: diegomrsantos Date: Wed, 12 Feb 2025 19:49:44 +0100 Subject: [PATCH 52/63] Update anchor/network/src/handshake/envelope/codec.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Oliveira --- anchor/network/src/handshake/envelope/codec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anchor/network/src/handshake/envelope/codec.rs b/anchor/network/src/handshake/envelope/codec.rs index 175d5397d..190858b61 100644 --- a/anchor/network/src/handshake/envelope/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -16,7 +16,7 @@ impl From for io::Error { } } -/// A `Codec` that reads/writes an **`Envelope`** +/// A `Codec` that reads/writes an **`Envelope`**. #[derive(Clone, Debug, Default)] pub struct Codec; From 495b2ea4d900eb1d622be4aa908c0c6a94da1436 Mon Sep 17 00:00:00 2001 From: diego Date: Wed, 12 Feb 2025 23:14:10 +0100 Subject: [PATCH 53/63] follow libp2p pattern for proto files --- anchor/network/src/handshake/envelope/codec.rs | 5 ++--- .../envelope/{envelope.proto => generated/message.proto} | 0 .../network/src/handshake/envelope/generated/message/mod.rs | 1 + .../envelope/{envelope.rs => generated/message/pb.rs} | 0 anchor/network/src/handshake/envelope/generated/mod.rs | 1 + anchor/network/src/handshake/envelope/mod.rs | 4 ++-- 6 files changed, 6 insertions(+), 5 deletions(-) rename anchor/network/src/handshake/envelope/{envelope.proto => generated/message.proto} (100%) create mode 100644 anchor/network/src/handshake/envelope/generated/message/mod.rs rename anchor/network/src/handshake/envelope/{envelope.rs => generated/message/pb.rs} (100%) create mode 100644 anchor/network/src/handshake/envelope/generated/mod.rs diff --git a/anchor/network/src/handshake/envelope/codec.rs b/anchor/network/src/handshake/envelope/codec.rs index 190858b61..7c7e7bf49 100644 --- a/anchor/network/src/handshake/envelope/codec.rs +++ b/anchor/network/src/handshake/envelope/codec.rs @@ -3,8 +3,7 @@ use crate::handshake::envelope::{parse_envelope, Envelope}; use async_trait::async_trait; use futures::{AsyncReadExt, AsyncWriteExt}; use libp2p::futures::{AsyncRead, AsyncWrite}; -use libp2p::request_response::Codec as RequestResponseCodec; -use libp2p::StreamProtocol; +use libp2p::{request_response, StreamProtocol}; use std::io; use tracing::debug; @@ -21,7 +20,7 @@ impl From for io::Error { pub struct Codec; #[async_trait] -impl RequestResponseCodec for Codec { +impl request_response::Codec for Codec { type Protocol = StreamProtocol; type Request = Envelope; type Response = Envelope; diff --git a/anchor/network/src/handshake/envelope/envelope.proto b/anchor/network/src/handshake/envelope/generated/message.proto similarity index 100% rename from anchor/network/src/handshake/envelope/envelope.proto rename to anchor/network/src/handshake/envelope/generated/message.proto diff --git a/anchor/network/src/handshake/envelope/generated/message/mod.rs b/anchor/network/src/handshake/envelope/generated/message/mod.rs new file mode 100644 index 000000000..ffaf2083e --- /dev/null +++ b/anchor/network/src/handshake/envelope/generated/message/mod.rs @@ -0,0 +1 @@ +pub mod pb; diff --git a/anchor/network/src/handshake/envelope/envelope.rs b/anchor/network/src/handshake/envelope/generated/message/pb.rs similarity index 100% rename from anchor/network/src/handshake/envelope/envelope.rs rename to anchor/network/src/handshake/envelope/generated/message/pb.rs diff --git a/anchor/network/src/handshake/envelope/generated/mod.rs b/anchor/network/src/handshake/envelope/generated/mod.rs new file mode 100644 index 000000000..e216a5018 --- /dev/null +++ b/anchor/network/src/handshake/envelope/generated/mod.rs @@ -0,0 +1 @@ +pub mod message; diff --git a/anchor/network/src/handshake/envelope/mod.rs b/anchor/network/src/handshake/envelope/mod.rs index e9d458179..96d8a1d51 100644 --- a/anchor/network/src/handshake/envelope/mod.rs +++ b/anchor/network/src/handshake/envelope/mod.rs @@ -1,5 +1,5 @@ mod codec; -mod envelope; +mod generated; use crate::handshake::node_info::NodeInfo; use discv5::libp2p_identity::PublicKey; @@ -73,4 +73,4 @@ pub fn make_unsigned( use crate::handshake::envelope::Error::SignatureVerification; pub use codec::Codec; -pub use envelope::Envelope; +pub use generated::message::pb::Envelope; From 2eb6b3e5bd34c36d642ec55369d0afb201a8dbd2 Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 13 Feb 2025 09:40:57 +0100 Subject: [PATCH 54/63] remove print --- anchor/network/src/network.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index f37be1be3..32c49343b 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -288,7 +288,6 @@ async fn build_anchor_behaviour( }; let domain_type: String = network_config.clone().domain_type.into(); - print!("Domain: {}", domain_type); let node_info = NodeInfo::new( domain_type, Some(NodeMetadata { From 908d4a9f51021ab9537d9b081ea7f7a71f5fd731 Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 13 Feb 2025 13:53:21 +0100 Subject: [PATCH 55/63] move domain_type.rs --- Cargo.lock | 3 ++- anchor/common/ssv_network_config/Cargo.toml | 1 + anchor/common/ssv_network_config/src/lib.rs | 25 +-------------------- anchor/common/ssv_types/src/domain_type.rs | 24 ++++++++++++++++++++ anchor/common/ssv_types/src/lib.rs | 1 + anchor/network/Cargo.toml | 2 +- anchor/network/src/config.rs | 2 +- 7 files changed, 31 insertions(+), 27 deletions(-) create mode 100644 anchor/common/ssv_types/src/domain_type.rs diff --git a/Cargo.lock b/Cargo.lock index a8faa4255..f8a05d6dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5176,7 +5176,7 @@ dependencies = [ "quick-protobuf", "serde", "serde_json", - "ssv_network_config", + "ssv_types", "ssz_types 0.10.0", "subnet_tracker", "task_executor", @@ -7234,6 +7234,7 @@ dependencies = [ "enr", "eth2_network_config", "serde_yaml", + "ssv_types", ] [[package]] diff --git a/anchor/common/ssv_network_config/Cargo.toml b/anchor/common/ssv_network_config/Cargo.toml index e3d9a6c32..858eb9159 100644 --- a/anchor/common/ssv_network_config/Cargo.toml +++ b/anchor/common/ssv_network_config/Cargo.toml @@ -9,3 +9,4 @@ alloy = { workspace = true } enr = { workspace = true } eth2_network_config = { workspace = true } serde_yaml = { workspace = true } +ssv_types = { workspace = true } diff --git a/anchor/common/ssv_network_config/src/lib.rs b/anchor/common/ssv_network_config/src/lib.rs index 6d33b4b96..f9224f767 100644 --- a/anchor/common/ssv_network_config/src/lib.rs +++ b/anchor/common/ssv_network_config/src/lib.rs @@ -1,7 +1,7 @@ -use alloy::hex; use alloy::primitives::Address; use enr::{CombinedKey, Enr}; use eth2_network_config::Eth2NetworkConfig; +use ssv_types::domain_type::DomainType; use std::fs::File; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -28,29 +28,6 @@ macro_rules! get_hardcoded { }; } -#[derive(Clone, Debug, Default)] -pub struct DomainType(pub [u8; 4]); - -impl FromStr for DomainType { - type Err = String; - - fn from_str(hex_str: &str) -> Result { - let bytes = hex::decode(hex_str).map_err(|_| "Invalid domain type hex")?; - if bytes.len() != 4 { - return Err("Domain type must be 4 bytes".into()); - } - let mut domain_type = [0; 4]; - domain_type.copy_from_slice(&bytes); - Ok(Self(domain_type)) - } -} - -impl From for String { - fn from(domain_type: DomainType) -> Self { - hex::encode(domain_type.0) - } -} - #[derive(Clone, Debug)] pub struct SsvNetworkConfig { pub eth2_network: Eth2NetworkConfig, diff --git a/anchor/common/ssv_types/src/domain_type.rs b/anchor/common/ssv_types/src/domain_type.rs new file mode 100644 index 000000000..6e94d6a58 --- /dev/null +++ b/anchor/common/ssv_types/src/domain_type.rs @@ -0,0 +1,24 @@ +use std::str::FromStr; + +#[derive(Clone, Debug, Default)] +pub struct DomainType(pub [u8; 4]); + +impl FromStr for DomainType { + type Err = String; + + fn from_str(hex_str: &str) -> Result { + let bytes = hex::decode(hex_str).map_err(|_| "Invalid domain type hex")?; + if bytes.len() != 4 { + return Err("Domain type must be 4 bytes".into()); + } + let mut domain_type = [0; 4]; + domain_type.copy_from_slice(&bytes); + Ok(Self(domain_type)) + } +} + +impl From for String { + fn from(domain_type: DomainType) -> Self { + hex::encode(domain_type.0) + } +} diff --git a/anchor/common/ssv_types/src/lib.rs b/anchor/common/ssv_types/src/lib.rs index 31b8a9900..f3acc55c1 100644 --- a/anchor/common/ssv_types/src/lib.rs +++ b/anchor/common/ssv_types/src/lib.rs @@ -3,6 +3,7 @@ pub use operator::{Operator, OperatorId}; pub use share::Share; mod cluster; pub mod consensus; +pub mod domain_type; pub mod message; pub mod msgid; mod operator; diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index 5c6d4105b..e377b411b 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -29,7 +29,7 @@ lighthouse_network = { workspace = true } quick-protobuf = "0.8.1" serde = { workspace = true } serde_json = "1.0.137" -ssv_network_config = { workspace = true } +ssv_types = { workspace = true } ssz_types = "0.10" subnet_tracker = { workspace = true } task_executor = { workspace = true } diff --git a/anchor/network/src/config.rs b/anchor/network/src/config.rs index ec67e1efb..99a87d2e8 100644 --- a/anchor/network/src/config.rs +++ b/anchor/network/src/config.rs @@ -2,7 +2,7 @@ use discv5::Enr; use libp2p::Multiaddr; use lighthouse_network::types::GossipKind; use lighthouse_network::{ListenAddr, ListenAddress}; -use ssv_network_config::DomainType; +use ssv_types::domain_type::DomainType; use std::net::{Ipv4Addr, Ipv6Addr}; use std::num::NonZeroU16; use std::path::PathBuf; From 266c4e561919b15ed4e9986b9e79a5134d27cc11 Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 13 Feb 2025 15:25:19 +0100 Subject: [PATCH 56/63] create handshake_success test --- Cargo.lock | 245 +++++++++++++++++++++++++++- anchor/network/Cargo.toml | 3 + anchor/network/src/handshake/mod.rs | 76 ++++++++- 3 files changed, 314 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8a05d6dd..ec44e8df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,7 +711,7 @@ dependencies = [ name = "anchor" version = "0.1.0" dependencies = [ - "async-channel", + "async-channel 1.9.0", "bls", "clap", "client", @@ -1046,6 +1046,57 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + [[package]] name = "async-io" version = "2.4.0" @@ -1076,6 +1127,80 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel 2.3.1", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.0", + "futures-lite", + "rustix 0.38.44", + "tracing", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.44", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-std" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1098,6 +1223,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.86" @@ -1362,6 +1493,19 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bls" version = "0.2.0" @@ -2148,7 +2292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.98", ] [[package]] @@ -3026,7 +3170,10 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ + "fastrand", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -3193,12 +3340,24 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gossipsub" version = "0.5.0" source = "git+https://github.com/sigp/lighthouse?rev=1a77f7a0#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ - "async-channel", + "async-channel 1.9.0", "asynchronous-codec", "base64 0.21.7", "byteorder", @@ -3914,6 +4073,7 @@ dependencies = [ "netlink-proto", "netlink-sys", "rtnetlink", + "smol", "system-configuration 0.6.1", "tokio", "windows", @@ -4118,6 +4278,15 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "kzg" version = "0.1.0" @@ -4529,6 +4698,7 @@ version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7dd6741793d2c1fb2088f67f82cf07261f25272ebe3c0b0c311e0c6b50e851a" dependencies = [ + "async-std", "either", "fnv", "futures", @@ -4559,12 +4729,32 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "libp2p-swarm-test" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4e1d1d92421dc4c90cad42e3cd24f50fd210191c9f126d41bd483a09567f67" +dependencies = [ + "async-trait", + "futures", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-plaintext", + "libp2p-swarm", + "libp2p-tcp", + "libp2p-yamux", + "rand", + "tracing", +] + [[package]] name = "libp2p-tcp" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad964f312c59dcfcac840acd8c555de8403e295d39edf96f5240048b5fcaa314" dependencies = [ + "async-io", "futures", "futures-timer", "if-watch", @@ -4793,6 +4983,9 @@ name = "log" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +dependencies = [ + "value-bag", +] [[package]] name = "logging" @@ -5152,6 +5345,7 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" dependencies = [ + "async-io", "bytes", "futures", "libc", @@ -5163,7 +5357,7 @@ dependencies = [ name = "network" version = "0.1.0" dependencies = [ - "async-channel", + "async-channel 1.9.0", "async-trait", "dirs 6.0.0", "discv5", @@ -5172,6 +5366,8 @@ dependencies = [ "futures", "hex", "libp2p", + "libp2p-swarm", + "libp2p-swarm-test", "lighthouse_network", "quick-protobuf", "serde", @@ -5670,6 +5866,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -5818,7 +6025,7 @@ dependencies = [ name = "processor" version = "0.1.0" dependencies = [ - "async-channel", + "async-channel 1.9.0", "futures", "metrics", "num_cpus", @@ -5970,7 +6177,7 @@ dependencies = [ name = "qbft_manager" version = "0.1.0" dependencies = [ - "async-channel", + "async-channel 1.9.0", "dashmap", "ethereum_ssz 0.7.1", "ethereum_ssz_derive 0.7.1", @@ -6439,6 +6646,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" dependencies = [ + "async-global-executor", "futures", "log", "netlink-packet-core", @@ -7168,6 +7376,23 @@ dependencies = [ "serde", ] +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + [[package]] name = "snap" version = "1.1.1" @@ -7537,7 +7762,7 @@ name = "task_executor" version = "0.1.0" source = "git+https://github.com/sigp/lighthouse?rev=1a77f7a0#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ - "async-channel", + "async-channel 1.9.0", "futures", "logging", "metrics", @@ -8319,6 +8544,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index e377b411b..79357f152 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -40,3 +40,6 @@ version = { workspace = true } [dev-dependencies] async-channel = { workspace = true } +libp2p-swarm-test = { version = "0.4.0" } +libp2p-swarm = { version = "0.45.1", features = ["macros"] } +tokio = { workspace = true, features = ["rt", "macros", "time"] } diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index fa76868d3..cf5898980 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -45,7 +45,7 @@ pub enum Event { /// Network behaviour handling the handshake protocol. pub struct Behaviour { /// Request-response behaviour for the handshake protocol. - behaviour: RequestResponseBehaviour, + pub(crate) behaviour: RequestResponseBehaviour, /// Keypair for signing envelopes. keypair: Keypair, /// Local node's information provider. @@ -105,10 +105,10 @@ impl Behaviour { self.unmarshall_and_verify(peer_id, response); } - fn unmarshall_and_verify(&mut self, peer_id: PeerId, response: &Envelope) { + fn unmarshall_and_verify(&mut self, peer_id: PeerId, envelope: &Envelope) { let mut their_info = NodeInfo::default(); - if let Err(e) = their_info.unmarshal(&response.payload) { + if let Err(e) = their_info.unmarshal(&envelope.payload) { self.events.push(Event::Failed { peer_id, error: Error::NodeInfo(e), @@ -243,3 +243,73 @@ impl NetworkBehaviour for Behaviour { Poll::Pending } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::handshake::node_info::NodeMetadata; + use discv5::libp2p_identity::Keypair; + use libp2p_swarm::Swarm; + use libp2p_swarm_test::{drive, SwarmExt}; + + fn node_info(version: &str) -> NodeInfo { + NodeInfo { + network_id: "test".to_string(), + metadata: Some(NodeMetadata { + node_version: version.to_string(), + execution_node: "".to_string(), + consensus_node: "".to_string(), + subnets: "".to_string(), + }), + } + } + + fn test_behaviour(version: &str, keypair: Keypair) -> Behaviour { + let node_info_manager = NodeInfoManager::new(node_info(version)); + Behaviour::new(keypair, node_info_manager) + } + + #[tokio::test] + async fn handshake_success() { + let local_key = Keypair::generate_ed25519(); + let remote_key = Keypair::generate_ed25519(); + + let mut local_swarm = Swarm::new_ephemeral(|_| test_behaviour("local", local_key)); + let mut remote_swarm = + Swarm::new_ephemeral(|_| test_behaviour("remote", remote_key.clone())); + + tokio::spawn(async move { + local_swarm.listen().with_memory_addr_external().await; + + remote_swarm.connect(&mut local_swarm).await; + + // Drive the swarm until the handshake completes + let ([local_event], [remote_event]): ([Event; 1], [Event; 1]) = + drive(&mut local_swarm, &mut remote_swarm).await; + + match local_event { + Event::Completed { + peer_id, + their_info, + } => { + assert_eq!(peer_id, *remote_swarm.local_peer_id()); + assert_eq!(their_info.metadata.unwrap().node_version, "remote"); + } + _ => panic!("Unexpected event for local swarm"), + } + + match remote_event { + Event::Completed { + peer_id, + their_info, + } => { + assert_eq!(peer_id, *local_swarm.local_peer_id()); + assert_eq!(their_info.metadata.unwrap().node_version, "local"); + } + _ => panic!("Unexpected event for remote swarm"), + } + }) + .await + .expect("tokio runtime failed"); + } +} From b6666c7857c46a23dd8ab6bda6737de9916a8591 Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 13 Feb 2025 16:19:44 +0100 Subject: [PATCH 57/63] add mismatched_networks_handshake_failed test --- anchor/network/Cargo.toml | 2 +- anchor/network/src/handshake/mod.rs | 71 ++++++++++++++++++----------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index 79357f152..e3cf96026 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -40,6 +40,6 @@ version = { workspace = true } [dev-dependencies] async-channel = { workspace = true } -libp2p-swarm-test = { version = "0.4.0" } libp2p-swarm = { version = "0.45.1", features = ["macros"] } +libp2p-swarm-test = { version = "0.4.0" } tokio = { workspace = true, features = ["rt", "macros", "time"] } diff --git a/anchor/network/src/handshake/mod.rs b/anchor/network/src/handshake/mod.rs index cf5898980..e4029fa60 100644 --- a/anchor/network/src/handshake/mod.rs +++ b/anchor/network/src/handshake/mod.rs @@ -252,9 +252,9 @@ mod tests { use libp2p_swarm::Swarm; use libp2p_swarm_test::{drive, SwarmExt}; - fn node_info(version: &str) -> NodeInfo { + fn node_info(network: &str, version: &str) -> NodeInfo { NodeInfo { - network_id: "test".to_string(), + network_id: network.to_string(), metadata: Some(NodeMetadata { node_version: version.to_string(), execution_node: "".to_string(), @@ -264,8 +264,8 @@ mod tests { } } - fn test_behaviour(version: &str, keypair: Keypair) -> Behaviour { - let node_info_manager = NodeInfoManager::new(node_info(version)); + fn test_behaviour(network: &str, version: &str, keypair: Keypair) -> Behaviour { + let node_info_manager = NodeInfoManager::new(node_info(network, version)); Behaviour::new(keypair, node_info_manager) } @@ -274,9 +274,9 @@ mod tests { let local_key = Keypair::generate_ed25519(); let remote_key = Keypair::generate_ed25519(); - let mut local_swarm = Swarm::new_ephemeral(|_| test_behaviour("local", local_key)); + let mut local_swarm = Swarm::new_ephemeral(|_| test_behaviour("test", "local", local_key)); let mut remote_swarm = - Swarm::new_ephemeral(|_| test_behaviour("remote", remote_key.clone())); + Swarm::new_ephemeral(|_| test_behaviour("test", "remote", remote_key.clone())); tokio::spawn(async move { local_swarm.listen().with_memory_addr_external().await; @@ -287,27 +287,46 @@ mod tests { let ([local_event], [remote_event]): ([Event; 1], [Event; 1]) = drive(&mut local_swarm, &mut remote_swarm).await; - match local_event { - Event::Completed { - peer_id, - their_info, - } => { - assert_eq!(peer_id, *remote_swarm.local_peer_id()); - assert_eq!(their_info.metadata.unwrap().node_version, "remote"); - } - _ => panic!("Unexpected event for local swarm"), - } + assert!(matches!(local_event, + Event::Completed { peer_id, ref their_info } if peer_id == *remote_swarm.local_peer_id() && their_info.metadata.as_ref().unwrap().node_version == "remote") + ); - match remote_event { - Event::Completed { - peer_id, - their_info, - } => { - assert_eq!(peer_id, *local_swarm.local_peer_id()); - assert_eq!(their_info.metadata.unwrap().node_version, "local"); - } - _ => panic!("Unexpected event for remote swarm"), - } + assert!(matches!(remote_event, + Event::Completed { peer_id, ref their_info } if peer_id == *local_swarm.local_peer_id() && their_info.metadata.as_ref().unwrap().node_version == "local") + ); + }) + .await + .expect("tokio runtime failed"); + } + + #[tokio::test] + async fn mismatched_networks_handshake_failed() { + let local_key = Keypair::generate_ed25519(); + let remote_key = Keypair::generate_ed25519(); + + let mut local_swarm = Swarm::new_ephemeral(|_| test_behaviour("test1", "local", local_key)); + let mut remote_swarm = + Swarm::new_ephemeral(|_| test_behaviour("test2", "remote", remote_key.clone())); + + tokio::spawn(async move { + local_swarm.listen().with_memory_addr_external().await; + + remote_swarm.connect(&mut local_swarm).await; + + // Drive the swarm until the handshake completes + let ([local_event], [remote_event]): ([Event; 1], [Event; 1]) = + drive(&mut local_swarm, &mut remote_swarm).await; + + assert!(matches!(remote_event, + Event::Failed { peer_id, error } if peer_id == *local_swarm.local_peer_id() + && matches!( + error, Error::NetworkMismatch { ref ours , ref theirs } if ours.as_str() == "test2" && theirs.as_str() == "test1")) + ); + + assert!(matches!(local_event, + Event::Failed { peer_id, error } if peer_id == *remote_swarm.local_peer_id() + && matches!(error, Error::NetworkMismatch { ref ours, ref theirs } if ours.as_str() == "test1" && theirs.as_str() == "test2")) + ); }) .await .expect("tokio runtime failed"); From edfdd780316f4f712a4bf2aa33f9193f93c1ae80 Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 13 Feb 2025 17:23:09 +0100 Subject: [PATCH 58/63] fix test --- .../built_in_network_configs/holesky/ssv_domain_type.txt | 2 +- .../built_in_network_configs/mainnet/ssv_domain_type.txt | 2 +- anchor/common/ssv_network_config/src/lib.rs | 2 +- anchor/common/ssv_types/src/domain_type.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/anchor/common/ssv_network_config/built_in_network_configs/holesky/ssv_domain_type.txt b/anchor/common/ssv_network_config/built_in_network_configs/holesky/ssv_domain_type.txt index ec919db56..1e9292108 100644 --- a/anchor/common/ssv_network_config/built_in_network_configs/holesky/ssv_domain_type.txt +++ b/anchor/common/ssv_network_config/built_in_network_configs/holesky/ssv_domain_type.txt @@ -1 +1 @@ -0x00000502 \ No newline at end of file +00000502 \ No newline at end of file diff --git a/anchor/common/ssv_network_config/built_in_network_configs/mainnet/ssv_domain_type.txt b/anchor/common/ssv_network_config/built_in_network_configs/mainnet/ssv_domain_type.txt index 3ee928e4d..b86234fa1 100644 --- a/anchor/common/ssv_network_config/built_in_network_configs/mainnet/ssv_domain_type.txt +++ b/anchor/common/ssv_network_config/built_in_network_configs/mainnet/ssv_domain_type.txt @@ -1 +1 @@ -0x00000001 \ No newline at end of file +00000001 \ No newline at end of file diff --git a/anchor/common/ssv_network_config/src/lib.rs b/anchor/common/ssv_network_config/src/lib.rs index f9224f767..a5b52ad3e 100644 --- a/anchor/common/ssv_network_config/src/lib.rs +++ b/anchor/common/ssv_network_config/src/lib.rs @@ -60,7 +60,7 @@ impl SsvNetworkConfig { .map_err(|_| "Unable to parse built-in block!")?, ssv_domain_type: domain_type .parse() - .map_err(|_| "Unable to parse built-in domain type!")?, + .map_err(|e| format!("Unable to parse built-in domain type: {}", e))?, })) } diff --git a/anchor/common/ssv_types/src/domain_type.rs b/anchor/common/ssv_types/src/domain_type.rs index 6e94d6a58..b4a276782 100644 --- a/anchor/common/ssv_types/src/domain_type.rs +++ b/anchor/common/ssv_types/src/domain_type.rs @@ -7,7 +7,7 @@ impl FromStr for DomainType { type Err = String; fn from_str(hex_str: &str) -> Result { - let bytes = hex::decode(hex_str).map_err(|_| "Invalid domain type hex")?; + let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid domain type hex: {}", e))?; if bytes.len() != 4 { return Err("Domain type must be 4 bytes".into()); } From dcf972730df56238af42b92874d6cf29e31b6c3a Mon Sep 17 00:00:00 2001 From: diego Date: Thu, 13 Feb 2025 18:43:04 +0100 Subject: [PATCH 59/63] use RwLock from parking_lot --- Cargo.lock | 1 + anchor/network/Cargo.toml | 1 + anchor/network/src/network.rs | 10 ++++------ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec44e8df5..f7fea0e25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5369,6 +5369,7 @@ dependencies = [ "libp2p-swarm", "libp2p-swarm-test", "lighthouse_network", + "parking_lot", "quick-protobuf", "serde", "serde_json", diff --git a/anchor/network/Cargo.toml b/anchor/network/Cargo.toml index e3cf96026..0450fdf96 100644 --- a/anchor/network/Cargo.toml +++ b/anchor/network/Cargo.toml @@ -26,6 +26,7 @@ libp2p = { version = "0.54", default-features = false, features = [ "request-response", ] } lighthouse_network = { workspace = true } +parking_lot = { workspace = true } quick-protobuf = "0.8.1" serde = { workspace = true } serde_json = "1.0.137" diff --git a/anchor/network/src/network.rs b/anchor/network/src/network.rs index 32c49343b..1be3adbd6 100644 --- a/anchor/network/src/network.rs +++ b/anchor/network/src/network.rs @@ -1,6 +1,6 @@ use std::num::{NonZeroU8, NonZeroUsize}; use std::pin::Pin; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use std::time::Duration; use futures::StreamExt; @@ -27,6 +27,7 @@ use crate::handshake::node_info::{NodeInfo, NodeMetadata}; use crate::handshake::{Behaviour, Event}; use crate::types::ssv_message::SignedSSVMessage; use lighthouse_network::EnrExt; +use parking_lot::RwLock; use ssz::Decode; use subnet_tracker::{SubnetEvent, SubnetId}; use tokio::sync::mpsc; @@ -373,14 +374,11 @@ impl NodeInfoManager { } pub fn get_node_info(&self) -> NodeInfo { - // Using unwrap() here will panic if the lock is poisoned. - // We might choose to handle the error more gracefully. - self.node_info.read().unwrap().clone() + self.node_info.read().clone() } pub fn set_node_info(&self, node_info: NodeInfo) { - // Using unwrap() here will panic if the lock is poisoned. - *self.node_info.write().unwrap() = node_info; + *self.node_info.write() = node_info; } } From 7d3f32dda5ac5e0da633e3691e199f90285b79b4 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 14 Feb 2025 11:57:39 +0100 Subject: [PATCH 60/63] add handshake spec to the book --- book/src/developers.md | 4 + book/src/handshake.md | 272 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 book/src/handshake.md diff --git a/book/src/developers.md b/book/src/developers.md index 819edcaa7..a8a562830 100644 --- a/book/src/developers.md +++ b/book/src/developers.md @@ -4,3 +4,7 @@ _Documentation for protocol developers._ This section lists Anchor-specific decisions that are not strictly spec'd and may be useful for other protocol developers wishing to interact with Anchor. + +## SSV NodeInfo Handshake Protocol + +The protocol is used by SSV-based nodes to exchange basic node metadata and validate each other's identity when establishing a connection over Libp2p under a dedicated protocol ID. The spec is define here in the [SSV NodeInfo Handshake Protocol](./handshake.md). diff --git a/book/src/handshake.md b/book/src/handshake.md new file mode 100644 index 000000000..0bdc0c8ec --- /dev/null +++ b/book/src/handshake.md @@ -0,0 +1,272 @@ +# SSV NodeInfo Handshake Protocol Specification + +This document specifies the **SSV NodeInfo Handshake Protocol**. The protocol is used by SSV-based nodes to exchange basic node metadata and validate each other's identity when establishing a connection over Libp2p under a dedicated protocol ID. + +--- + +## Table of Contents + +- [1. Introduction](#1-introduction) +- [2. Definitions](#2-definitions) + - [2.1 Terminology](#21-terminology) + - [2.2 Domain Separation](#22-domain-separation) +- [3. Protocol Constants](#3-protocol-constants) +- [4. Data Structures](#4-data-structures) + - [4.1 Envelope](#41-envelope) + - [4.2 NodeInfo](#42-nodeinfo) + - [4.3 NodeMetadata](#43-nodemetadata) +- [5. Serialization and Signing](#5-serialization-and-signing) + - [5.1 Envelope Fields](#51-envelope-fields) + - [5.2 NodeInfo JSON Layout](#52-nodeinfo-json-layout) + - [5.3 Signature Preparation](#53-signature-preparation) +- [6. Handshake Protocol Flows](#6-handshake-protocol-flows) + - [6.1 Protocol ID](#61-protocol-id) + - [6.2 Request Phase](#62-request-phase) + - [6.3 Response Phase](#63-response-phase) + - [6.4 Network Mismatch Checks](#64-network-mismatch-checks) +- [7. Security Considerations](#7-security-considerations) +- [8. Rationale and Notes](#8-rationale-and-notes) +- [9. Examples](#9-examples) + +--- + +## 1. Introduction + +The SSV NodeInfo Handshake Protocol defines how two SSV nodes exchange, sign, and verify each other's **NodeInfo**, which includes a `network_id` (such as "holesky", "prater", etc.) and optional metadata about node software versions or subnets. The protocol uses a request-response style handshake over Libp2p under a dedicated protocol ID. + +The high-level handshake steps are: + +1. **Requester** sends an Envelope (containing its NodeInfo) to the peer. +2. **Responder** verifies this Envelope, checks the `network_id`, and replies with its own Envelope. +3. **Requester** verifies the responder's Envelope. +4. Both sides proceed if verification succeeds; otherwise, the handshake is considered failed. + +--- + +## 2. Definitions + +### 2.1 Terminology + +| **Term** | **Definition** | +|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Envelope** | A Protobuf-encoded message containing a `public_key`, `payload_type`, `payload`, and `signature` (covering a domain-separated concatenation of fields). | +| **NodeInfo** | A JSON-based structure holding key node attributes like `network_id` plus optional metadata. | +| **Handshake**| The request-response exchange of Envelopes between two nodes at connection time. | + +### 2.2 Domain Separation + +- **Domain**: `"ssv"`. + Used to separate signatures for different contexts or protocols. + +--- + +## 3. Protocol Constants + +| **Name** | **Value** | **Description** | +|----------------|--------------------|------------------------------------------------------| +| `DOMAIN` | `ssv` | Fixed ASCII text used during signature generation. | +| `PAYLOAD_TYPE` | `ssv/nodeinfo` | Identifies the payload as an SSV NodeInfo structure. | +| `PROTOCOL_ID` | `/ssv/info/0.0.1` | Libp2p protocol ID used for the handshake. | + +--- + +## 4. Data Structures + +### 4.1 Envelope + +The Envelope is a Protobuf message: + +```protobuf +message Envelope { + bytes public_key = 1; + bytes payload_type = 2; + bytes payload = 3; + bytes signature = 5; +} +``` + +### 4.2 NodeInfo + +``` +NodeInfo: +- network_id: String +- metadata: NodeMetadata (optional) +``` + +### 4.3 NodeMetadata + +``` +NodeMetadata: +- node_version: String +- execution_node: String +- consensus_node: String +- subnets: String +``` + +--- + +## 5. Serialization and Signing + +### 5.1 Envelope Fields + +1. **public_key** + - Sender’s public key in serialized form (e.g., compressed Secp256k1 or raw Ed25519 bytes). + - The public key is encoded and decoded using Protobuf. + - For reference, Libp2p has a [Peer Ids and Keys](https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md), which may be consulted for consistent handling across implementations. + +2. **payload_type** + - MUST be `"ssv/nodeinfo"` in this protocol. + - Used to identify how to interpret `payload`. + +3. **payload** + - Contains `NodeInfo` data in JSON (described below). + +4. **signature** + - A cryptographic signature covering `DOMAIN || payload_type || payload`. + +### 5.2 NodeInfo JSON Layout + +Internally, the protocol uses a “legacy” layout for `NodeInfo` serialization, with a top-level JSON structure: + +```json +{ + "Entries": [ + "", // (Index 0) Old forkVersion, not used + "", // (Index 1) The NodeInfo.network_id + "" // (Index 2) if NodeMetadata is present + ] +} +``` + +- If the array has fewer than 2 entries, the payload is invalid. +- If the array has 3 entries, the 3rd entry is a JSON object for metadata, for example: + +```json +{ + "NodeVersion": "...", + "ExecutionNode": "...", + "ConsensusNode": "...", + "Subnets": "..." +} +``` + +### 5.3 Signature Preparation + +To **sign** an Envelope, implementations: + +1. Construct the unsigned message: + ``` + unsigned_message = DOMAIN || payload_type || payload + ``` +2. Sign `unsigned_message` using the node’s private key. +3. Write the resulting signature to `signature`. + +To **verify** an Envelope: + +1. Recompute the `unsigned_message`. +2. Verify using `public_key` against `signature`. + +If verification fails, the handshake **MUST** abort. + +--- + +## 6. Handshake Protocol Flows + +### 6.1 Protocol ID + +Both peers must speak the protocol identified by: + +``` +/ssv/info/0.0.1 +``` + +### 6.2 Request Phase + +1. **Build Envelope** + - The initiating node (Requester) serializes its `NodeInfo` into JSON (the `payload`). + - Sets `payload_type = "ssv/nodeinfo"`. + - Prepends `DOMAIN = "ssv"` when computing the signature. + - Places the resulting `public_key` and `signature` into the Envelope. + +2. **Send Request** + - The requester sends this Envelope as the request. + +3. **Wait for Response** + - The requester awaits the single response from the Responder. + +### 6.3 Response Phase + +1. **Receive & Verify** + - The responder verifies the incoming Envelope: + - Check signature correctness. + - Extract `NodeInfo`. + - Validate `network_id` if necessary (see [6.4](#64-network-mismatch-checks)). + +2. **Build Response** + - If valid, the responder builds and signs its own Envelope containing its `NodeInfo`. + +3. **Send Response** + - The responder sends the Envelope back to the requester. + +4. **Requester Verifies** + - The requester verifies the signature, parses `NodeInfo`, and checks `network_id`. + +### 6.4 Network Mismatch Checks + +- Implementations **MUST** check whether the received `NodeInfo`’s `network_id` matches their local `network_id`. +- If they mismatch, the implementation **SHOULD** reject the connection. + +--- + +## 7. Security Considerations + +- **Signature Validation** is mandatory. Any failure to verify the Envelope’s signature indicates an invalid handshake. +- **Public Key Authenticity**: The Envelope’s `public_key` is not implicitly trusted. It must match the verified signature. +- **Network Mismatch**: Avoid bridging distinct SSV or Ethereum networks. Peers claiming the wrong `network_id` should be rejected. +- **Payload Size**: Although `NodeInfo` is generally small, implementations **SHOULD** impose a maximum bound for payload. Any request or response exceeding this size limit **SHOULD** be rejected. + +--- + +## 8. Rationale and Notes + +- Using a Protobuf-based Envelope simplifies cross-language interoperability. +- The domain separation string (`"ssv"`) prevents signature reuse in other contexts. +- The “legacy” `Entries` layout ensures backward-compatibility with older SSV implementations. + +--- + +## 9. Examples + +### 9.1 Example Envelope in Hex + +An example Envelope could be hex-encoded as: + +``` +0a250802122102ba6a707dcec6c60ba2793d52123d34b22556964fc798d4aa88ffc41a00e42407120c7373762f6e6f6465696e666f1aa5017b22456e7472696573223a5b22222c22686f6c65736b79222c227b5c224e6f646556657273696f6e5c223a5c22676574682f785c222c5c22457865637574696f6e4e6f64655c223a5c22676574682f785c222c5c22436f6e73656e7375734e6f64655c223a5c22707279736d2f785c222c5c225375626e6574735c223a5c2230303030303030303030303030303030303030303030303030303030303030303030305c227d225d7d2a473045022100b8a2a668113330369e74b86ec818a87009e2a351f7ee4c0e431e1f659dd1bc3f02202b1ebf418efa7fb0541f77703bea8563234a1b70b8391d43daa40b6e7c3fcc84 +``` + +Decoding reveals (high-level view): + +``` +Envelope { + public_key = , + payload_type = "ssv/nodeinfo", + payload = { + "Entries": [ + "", + "holesky", + "{\"NodeVersion\":\"geth/x\",\"ExecutionNode\":\"geth/x\",\"ConsensusNode\":\"prysm/x\",\"Subnets\":\"00000000000000000000000000000000\"}" + ] + }, + signature = +} +``` + +### 9.2 Verifying the Envelope + +1. Recompute: `domain = "ssv"` + ``` + unsigned_message = "ssv" || "ssv/nodeinfo" || payload_bytes + ``` +2. Verify signature with `public_key`. +3. Parse payload JSON => parse `NodeInfo` => check `network_id`. From b155c6497965376a0b020daa2053eab2303f9075 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 14 Feb 2025 12:52:33 +0100 Subject: [PATCH 61/63] fix handshake.md --- book/src/handshake.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/book/src/handshake.md b/book/src/handshake.md index 0bdc0c8ec..4adeda71f 100644 --- a/book/src/handshake.md +++ b/book/src/handshake.md @@ -8,22 +8,22 @@ This document specifies the **SSV NodeInfo Handshake Protocol**. The protocol is - [1. Introduction](#1-introduction) - [2. Definitions](#2-definitions) - - [2.1 Terminology](#21-terminology) - - [2.2 Domain Separation](#22-domain-separation) + - [2.1 Terminology](#21-terminology) + - [2.2 Domain Separation](#22-domain-separation) - [3. Protocol Constants](#3-protocol-constants) - [4. Data Structures](#4-data-structures) - - [4.1 Envelope](#41-envelope) - - [4.2 NodeInfo](#42-nodeinfo) - - [4.3 NodeMetadata](#43-nodemetadata) + - [4.1 Envelope](#41-envelope) + - [4.2 NodeInfo](#42-nodeinfo) + - [4.3 NodeMetadata](#43-nodemetadata) - [5. Serialization and Signing](#5-serialization-and-signing) - - [5.1 Envelope Fields](#51-envelope-fields) - - [5.2 NodeInfo JSON Layout](#52-nodeinfo-json-layout) - - [5.3 Signature Preparation](#53-signature-preparation) + - [5.1 Envelope Fields](#51-envelope-fields) + - [5.2 NodeInfo JSON Layout](#52-nodeinfo-json-layout) + - [5.3 Signature Preparation](#53-signature-preparation) - [6. Handshake Protocol Flows](#6-handshake-protocol-flows) - - [6.1 Protocol ID](#61-protocol-id) - - [6.2 Request Phase](#62-request-phase) - - [6.3 Response Phase](#63-response-phase) - - [6.4 Network Mismatch Checks](#64-network-mismatch-checks) + - [6.1 Protocol ID](#61-protocol-id) + - [6.2 Request Phase](#62-request-phase) + - [6.3 Response Phase](#63-response-phase) + - [6.4 Network Mismatch Checks](#64-network-mismatch-checks) - [7. Security Considerations](#7-security-considerations) - [8. Rationale and Notes](#8-rationale-and-notes) - [9. Examples](#9-examples) @@ -87,7 +87,7 @@ message Envelope { ### 4.2 NodeInfo -``` +```text NodeInfo: - network_id: String - metadata: NodeMetadata (optional) @@ -95,7 +95,7 @@ NodeInfo: ### 4.3 NodeMetadata -``` +```text NodeMetadata: - node_version: String - execution_node: String @@ -155,9 +155,11 @@ Internally, the protocol uses a “legacy” layout for `NodeInfo` serialization To **sign** an Envelope, implementations: 1. Construct the unsigned message: - ``` + + ```text unsigned_message = DOMAIN || payload_type || payload ``` + 2. Sign `unsigned_message` using the node’s private key. 3. Write the resulting signature to `signature`. @@ -176,7 +178,7 @@ If verification fails, the handshake **MUST** abort. Both peers must speak the protocol identified by: -``` +```text /ssv/info/0.0.1 ``` @@ -241,13 +243,13 @@ Both peers must speak the protocol identified by: An example Envelope could be hex-encoded as: -``` +```text 0a250802122102ba6a707dcec6c60ba2793d52123d34b22556964fc798d4aa88ffc41a00e42407120c7373762f6e6f6465696e666f1aa5017b22456e7472696573223a5b22222c22686f6c65736b79222c227b5c224e6f646556657273696f6e5c223a5c22676574682f785c222c5c22457865637574696f6e4e6f64655c223a5c22676574682f785c222c5c22436f6e73656e7375734e6f64655c223a5c22707279736d2f785c222c5c225375626e6574735c223a5c2230303030303030303030303030303030303030303030303030303030303030303030305c227d225d7d2a473045022100b8a2a668113330369e74b86ec818a87009e2a351f7ee4c0e431e1f659dd1bc3f02202b1ebf418efa7fb0541f77703bea8563234a1b70b8391d43daa40b6e7c3fcc84 ``` Decoding reveals (high-level view): -``` +```text Envelope { public_key = , payload_type = "ssv/nodeinfo", @@ -265,8 +267,10 @@ Envelope { ### 9.2 Verifying the Envelope 1. Recompute: `domain = "ssv"` - ``` + + ```text unsigned_message = "ssv" || "ssv/nodeinfo" || payload_bytes ``` + 2. Verify signature with `public_key`. 3. Parse payload JSON => parse `NodeInfo` => check `network_id`. From c250765fcd3950e53440ef4347daf712b52abb37 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 14 Feb 2025 13:08:09 +0100 Subject: [PATCH 62/63] add handshake.md to summary --- book/src/SUMMARY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 4843bd51d..ce6d933c7 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -7,4 +7,5 @@ - [Development Environment](./setup.md) - [Contributing](./contributing.md) - [Protocol Developers](./developers.md) + - [SSV Handshake Protocol](./handshake.md) - [Architecture](./architecture.md) From f72e3db8b63990de0a7684ea7cfced1360df3c29 Mon Sep 17 00:00:00 2001 From: diego Date: Fri, 14 Feb 2025 13:12:23 +0100 Subject: [PATCH 63/63] remove readme from handshake --- anchor/network/src/handshake/README.md | 272 ------------------------- 1 file changed, 272 deletions(-) delete mode 100644 anchor/network/src/handshake/README.md diff --git a/anchor/network/src/handshake/README.md b/anchor/network/src/handshake/README.md deleted file mode 100644 index 0bdc0c8ec..000000000 --- a/anchor/network/src/handshake/README.md +++ /dev/null @@ -1,272 +0,0 @@ -# SSV NodeInfo Handshake Protocol Specification - -This document specifies the **SSV NodeInfo Handshake Protocol**. The protocol is used by SSV-based nodes to exchange basic node metadata and validate each other's identity when establishing a connection over Libp2p under a dedicated protocol ID. - ---- - -## Table of Contents - -- [1. Introduction](#1-introduction) -- [2. Definitions](#2-definitions) - - [2.1 Terminology](#21-terminology) - - [2.2 Domain Separation](#22-domain-separation) -- [3. Protocol Constants](#3-protocol-constants) -- [4. Data Structures](#4-data-structures) - - [4.1 Envelope](#41-envelope) - - [4.2 NodeInfo](#42-nodeinfo) - - [4.3 NodeMetadata](#43-nodemetadata) -- [5. Serialization and Signing](#5-serialization-and-signing) - - [5.1 Envelope Fields](#51-envelope-fields) - - [5.2 NodeInfo JSON Layout](#52-nodeinfo-json-layout) - - [5.3 Signature Preparation](#53-signature-preparation) -- [6. Handshake Protocol Flows](#6-handshake-protocol-flows) - - [6.1 Protocol ID](#61-protocol-id) - - [6.2 Request Phase](#62-request-phase) - - [6.3 Response Phase](#63-response-phase) - - [6.4 Network Mismatch Checks](#64-network-mismatch-checks) -- [7. Security Considerations](#7-security-considerations) -- [8. Rationale and Notes](#8-rationale-and-notes) -- [9. Examples](#9-examples) - ---- - -## 1. Introduction - -The SSV NodeInfo Handshake Protocol defines how two SSV nodes exchange, sign, and verify each other's **NodeInfo**, which includes a `network_id` (such as "holesky", "prater", etc.) and optional metadata about node software versions or subnets. The protocol uses a request-response style handshake over Libp2p under a dedicated protocol ID. - -The high-level handshake steps are: - -1. **Requester** sends an Envelope (containing its NodeInfo) to the peer. -2. **Responder** verifies this Envelope, checks the `network_id`, and replies with its own Envelope. -3. **Requester** verifies the responder's Envelope. -4. Both sides proceed if verification succeeds; otherwise, the handshake is considered failed. - ---- - -## 2. Definitions - -### 2.1 Terminology - -| **Term** | **Definition** | -|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Envelope** | A Protobuf-encoded message containing a `public_key`, `payload_type`, `payload`, and `signature` (covering a domain-separated concatenation of fields). | -| **NodeInfo** | A JSON-based structure holding key node attributes like `network_id` plus optional metadata. | -| **Handshake**| The request-response exchange of Envelopes between two nodes at connection time. | - -### 2.2 Domain Separation - -- **Domain**: `"ssv"`. - Used to separate signatures for different contexts or protocols. - ---- - -## 3. Protocol Constants - -| **Name** | **Value** | **Description** | -|----------------|--------------------|------------------------------------------------------| -| `DOMAIN` | `ssv` | Fixed ASCII text used during signature generation. | -| `PAYLOAD_TYPE` | `ssv/nodeinfo` | Identifies the payload as an SSV NodeInfo structure. | -| `PROTOCOL_ID` | `/ssv/info/0.0.1` | Libp2p protocol ID used for the handshake. | - ---- - -## 4. Data Structures - -### 4.1 Envelope - -The Envelope is a Protobuf message: - -```protobuf -message Envelope { - bytes public_key = 1; - bytes payload_type = 2; - bytes payload = 3; - bytes signature = 5; -} -``` - -### 4.2 NodeInfo - -``` -NodeInfo: -- network_id: String -- metadata: NodeMetadata (optional) -``` - -### 4.3 NodeMetadata - -``` -NodeMetadata: -- node_version: String -- execution_node: String -- consensus_node: String -- subnets: String -``` - ---- - -## 5. Serialization and Signing - -### 5.1 Envelope Fields - -1. **public_key** - - Sender’s public key in serialized form (e.g., compressed Secp256k1 or raw Ed25519 bytes). - - The public key is encoded and decoded using Protobuf. - - For reference, Libp2p has a [Peer Ids and Keys](https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md), which may be consulted for consistent handling across implementations. - -2. **payload_type** - - MUST be `"ssv/nodeinfo"` in this protocol. - - Used to identify how to interpret `payload`. - -3. **payload** - - Contains `NodeInfo` data in JSON (described below). - -4. **signature** - - A cryptographic signature covering `DOMAIN || payload_type || payload`. - -### 5.2 NodeInfo JSON Layout - -Internally, the protocol uses a “legacy” layout for `NodeInfo` serialization, with a top-level JSON structure: - -```json -{ - "Entries": [ - "", // (Index 0) Old forkVersion, not used - "", // (Index 1) The NodeInfo.network_id - "" // (Index 2) if NodeMetadata is present - ] -} -``` - -- If the array has fewer than 2 entries, the payload is invalid. -- If the array has 3 entries, the 3rd entry is a JSON object for metadata, for example: - -```json -{ - "NodeVersion": "...", - "ExecutionNode": "...", - "ConsensusNode": "...", - "Subnets": "..." -} -``` - -### 5.3 Signature Preparation - -To **sign** an Envelope, implementations: - -1. Construct the unsigned message: - ``` - unsigned_message = DOMAIN || payload_type || payload - ``` -2. Sign `unsigned_message` using the node’s private key. -3. Write the resulting signature to `signature`. - -To **verify** an Envelope: - -1. Recompute the `unsigned_message`. -2. Verify using `public_key` against `signature`. - -If verification fails, the handshake **MUST** abort. - ---- - -## 6. Handshake Protocol Flows - -### 6.1 Protocol ID - -Both peers must speak the protocol identified by: - -``` -/ssv/info/0.0.1 -``` - -### 6.2 Request Phase - -1. **Build Envelope** - - The initiating node (Requester) serializes its `NodeInfo` into JSON (the `payload`). - - Sets `payload_type = "ssv/nodeinfo"`. - - Prepends `DOMAIN = "ssv"` when computing the signature. - - Places the resulting `public_key` and `signature` into the Envelope. - -2. **Send Request** - - The requester sends this Envelope as the request. - -3. **Wait for Response** - - The requester awaits the single response from the Responder. - -### 6.3 Response Phase - -1. **Receive & Verify** - - The responder verifies the incoming Envelope: - - Check signature correctness. - - Extract `NodeInfo`. - - Validate `network_id` if necessary (see [6.4](#64-network-mismatch-checks)). - -2. **Build Response** - - If valid, the responder builds and signs its own Envelope containing its `NodeInfo`. - -3. **Send Response** - - The responder sends the Envelope back to the requester. - -4. **Requester Verifies** - - The requester verifies the signature, parses `NodeInfo`, and checks `network_id`. - -### 6.4 Network Mismatch Checks - -- Implementations **MUST** check whether the received `NodeInfo`’s `network_id` matches their local `network_id`. -- If they mismatch, the implementation **SHOULD** reject the connection. - ---- - -## 7. Security Considerations - -- **Signature Validation** is mandatory. Any failure to verify the Envelope’s signature indicates an invalid handshake. -- **Public Key Authenticity**: The Envelope’s `public_key` is not implicitly trusted. It must match the verified signature. -- **Network Mismatch**: Avoid bridging distinct SSV or Ethereum networks. Peers claiming the wrong `network_id` should be rejected. -- **Payload Size**: Although `NodeInfo` is generally small, implementations **SHOULD** impose a maximum bound for payload. Any request or response exceeding this size limit **SHOULD** be rejected. - ---- - -## 8. Rationale and Notes - -- Using a Protobuf-based Envelope simplifies cross-language interoperability. -- The domain separation string (`"ssv"`) prevents signature reuse in other contexts. -- The “legacy” `Entries` layout ensures backward-compatibility with older SSV implementations. - ---- - -## 9. Examples - -### 9.1 Example Envelope in Hex - -An example Envelope could be hex-encoded as: - -``` -0a250802122102ba6a707dcec6c60ba2793d52123d34b22556964fc798d4aa88ffc41a00e42407120c7373762f6e6f6465696e666f1aa5017b22456e7472696573223a5b22222c22686f6c65736b79222c227b5c224e6f646556657273696f6e5c223a5c22676574682f785c222c5c22457865637574696f6e4e6f64655c223a5c22676574682f785c222c5c22436f6e73656e7375734e6f64655c223a5c22707279736d2f785c222c5c225375626e6574735c223a5c2230303030303030303030303030303030303030303030303030303030303030303030305c227d225d7d2a473045022100b8a2a668113330369e74b86ec818a87009e2a351f7ee4c0e431e1f659dd1bc3f02202b1ebf418efa7fb0541f77703bea8563234a1b70b8391d43daa40b6e7c3fcc84 -``` - -Decoding reveals (high-level view): - -``` -Envelope { - public_key = , - payload_type = "ssv/nodeinfo", - payload = { - "Entries": [ - "", - "holesky", - "{\"NodeVersion\":\"geth/x\",\"ExecutionNode\":\"geth/x\",\"ConsensusNode\":\"prysm/x\",\"Subnets\":\"00000000000000000000000000000000\"}" - ] - }, - signature = -} -``` - -### 9.2 Verifying the Envelope - -1. Recompute: `domain = "ssv"` - ``` - unsigned_message = "ssv" || "ssv/nodeinfo" || payload_bytes - ``` -2. Verify signature with `public_key`. -3. Parse payload JSON => parse `NodeInfo` => check `network_id`.