Skip to content

Commit ee147d0

Browse files
committed
feat: add peerinfo (mvp)
1 parent 7b72369 commit ee147d0

18 files changed

Lines changed: 1093 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ members = [
1212
"crates/charon-p2p",
1313
"crates/charon-testutil",
1414
"crates/tracing",
15-
"crates/relay-server"
15+
"crates/relay-server",
16+
"crates/peerinfo"
1617
]
1718
resolver = "3"
1819

@@ -67,6 +68,7 @@ charon-p2p = { path = "crates/charon-p2p" }
6768
charon-testutil = { path = "crates/charon-testutil" }
6869
charon-tracing = { path = "crates/tracing" }
6970
charon-relay-server = { path = "crates/relay-server" }
71+
charon-peerinfo = { path = "crates/peerinfo" }
7072

7173
[workspace.lints.rust]
7274
missing_docs = "deny"

crates/charon/build.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
//! # Charon Peerinfo Build Script
1+
//! # Charon Build Script
22
//!
33
//! This build script compiles the protobuf files.
44
55
use std::io::Result;
66

77
fn main() -> Result<()> {
8-
charon_build_proto::compile_protos("src/peerinfo/peerinfopb/v1")?;
98
charon_build_proto::compile_protos("src/log/loki/lokipb/v1")?;
109

1110
Ok(())

crates/charon/src/lib.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
//! coordination for Ethereum 2.0 validators. This crate serves as the primary
55
//! entry point for the Charon distributed validator node implementation.
66
7-
/// Peerinfo.
8-
pub mod peerinfo;
9-
107
/// Log
118
pub mod log;
129

crates/charon/src/peerinfo/mod.rs

Lines changed: 0 additions & 2 deletions
This file was deleted.

crates/peerinfo/Cargo.toml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[package]
2+
name = "charon-peerinfo"
3+
version.workspace = true
4+
edition.workspace = true
5+
repository.workspace = true
6+
license.workspace = true
7+
publish.workspace = true
8+
9+
[dependencies]
10+
prost.workspace = true
11+
prost-types.workspace = true
12+
thiserror.workspace = true
13+
libp2p.workspace = true
14+
futures = "0.3"
15+
futures-timer = "3.0"
16+
tracing.workspace = true
17+
chrono.workspace = true
18+
unsigned-varint = { version = "0.8", features = ["futures"] }
19+
20+
[build-dependencies]
21+
charon-build-proto.workspace = true
22+
23+
[dev-dependencies]
24+
anyhow.workspace = true
25+
clap.workspace = true
26+
tokio.workspace = true
27+
tracing-subscriber.workspace = true
28+
hex.workspace = true
29+
30+
[lints]
31+
workspace = true
32+
33+
[[example]]
34+
name = "peerinfo"

crates/peerinfo/build.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//! # Charon Peerinfo Build Script
2+
//!
3+
//! This build script compiles the protobuf files.
4+
5+
use std::io::Result;
6+
7+
fn main() -> Result<()> {
8+
charon_build_proto::compile_protos("src/peerinfopb/v1")?;
9+
10+
Ok(())
11+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
//! Peerinfo example
2+
//!
3+
//! This example demonstrates the peerinfo protocol by creating two nodes
4+
//! that exchange peer information with each other using mDNS auto-discovery.
5+
//!
6+
//! Run with:
7+
//! ```sh
8+
//! cargo run --example peerinfo -p charon-peerinfo
9+
//! ```
10+
//!
11+
//! Run two instances on different ports - they will auto-discover each other:
12+
//!
13+
//! Terminal 1: `cargo run --example peerinfo -p charon-peerinfo -- --port 4001`
14+
//! Terminal 2: `cargo run --example peerinfo -p charon-peerinfo -- --port 4002`
15+
#![allow(missing_docs)]
16+
use std::time::Duration;
17+
18+
use charon_peerinfo::{Behaviour, Config, Event, LocalPeerInfo};
19+
use clap::Parser;
20+
use libp2p::{
21+
Multiaddr, Swarm, SwarmBuilder,
22+
futures::StreamExt,
23+
identify, mdns, noise, ping,
24+
swarm::{NetworkBehaviour, SwarmEvent},
25+
tcp, yamux,
26+
};
27+
use tokio::signal;
28+
use tracing_subscriber::EnvFilter;
29+
30+
/// Command line arguments
31+
#[derive(Debug, Parser)]
32+
#[command(name = "peerinfo-example")]
33+
#[command(about = "Demonstrates the peerinfo protocol with mDNS discovery")]
34+
pub struct Args {
35+
/// The port to listen on
36+
#[arg(short, long, default_value = "4001")]
37+
pub port: u16,
38+
39+
/// Optional address to dial
40+
#[arg(short, long)]
41+
pub dial: Option<Multiaddr>,
42+
43+
/// Nickname for this node
44+
#[arg(short, long, default_value = "example-node")]
45+
pub nickname: String,
46+
47+
/// Peer info exchange interval in seconds
48+
#[arg(short, long, default_value = "5")]
49+
pub interval: u64,
50+
}
51+
52+
/// Combined behaviour with peerinfo, identify, ping, and mdns
53+
#[derive(NetworkBehaviour)]
54+
pub struct CombinedBehaviour {
55+
pub peer_info: Behaviour,
56+
pub identify: identify::Behaviour,
57+
pub ping: ping::Behaviour,
58+
pub mdns: mdns::tokio::Behaviour,
59+
}
60+
61+
pub type CombinedEvent = CombinedBehaviourEvent;
62+
63+
fn build_swarm(peerinfo_config: Config) -> anyhow::Result<Swarm<CombinedBehaviour>> {
64+
let swarm = SwarmBuilder::with_new_identity()
65+
.with_tokio()
66+
.with_tcp(
67+
tcp::Config::default(),
68+
noise::Config::new,
69+
yamux::Config::default,
70+
)?
71+
.with_behaviour(|key| {
72+
Ok(CombinedBehaviour {
73+
peer_info: Behaviour::new(peerinfo_config),
74+
identify: identify::Behaviour::new(identify::Config::new(
75+
"/peerinfo-example/1.0.0".to_string(),
76+
key.public(),
77+
)),
78+
ping: ping::Behaviour::new(
79+
ping::Config::new()
80+
.with_interval(Duration::from_secs(15))
81+
.with_timeout(Duration::from_secs(10)),
82+
),
83+
mdns: mdns::tokio::Behaviour::new(
84+
mdns::Config::default(),
85+
key.public().to_peer_id(),
86+
)?,
87+
})
88+
})?
89+
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(300)))
90+
.build();
91+
92+
Ok(swarm)
93+
}
94+
95+
fn handle_event(event: SwarmEvent<CombinedEvent>, swarm: &mut Swarm<CombinedBehaviour>) {
96+
match event {
97+
SwarmEvent::NewListenAddr { address, .. } => {
98+
tracing::info!("Listening on {address}");
99+
}
100+
SwarmEvent::ConnectionEstablished {
101+
peer_id, endpoint, ..
102+
} => {
103+
tracing::info!(
104+
"Connection established with {peer_id} via {}",
105+
endpoint.get_remote_address()
106+
);
107+
}
108+
SwarmEvent::ConnectionClosed { peer_id, cause, .. } => {
109+
tracing::info!("Connection closed with {peer_id}: {cause:?}");
110+
}
111+
SwarmEvent::Behaviour(CombinedEvent::PeerInfo(Event::Received { peer, info, .. })) => {
112+
tracing::info!(
113+
"📥 Received PeerInfo from {peer}:\n\
114+
│ Version: {}\n\
115+
│ Git Hash: {}\n\
116+
│ Nickname: {}\n\
117+
│ Builder API: {}\n\
118+
│ Lock Hash: {:?}",
119+
info.charon_version,
120+
info.git_hash,
121+
info.nickname,
122+
info.builder_api_enabled,
123+
hex::encode(&info.lock_hash),
124+
);
125+
}
126+
SwarmEvent::Behaviour(CombinedEvent::PeerInfo(Event::Error { peer, error, .. })) => {
127+
tracing::warn!("PeerInfo error with {peer}: {error}");
128+
}
129+
SwarmEvent::Behaviour(CombinedEvent::Identify(identify::Event::Received {
130+
peer_id,
131+
info,
132+
..
133+
})) => {
134+
tracing::debug!(
135+
"Identify received from {peer_id}: {} {}",
136+
info.protocol_version,
137+
info.agent_version
138+
);
139+
}
140+
SwarmEvent::Behaviour(CombinedEvent::Ping(ping::Event { peer, result, .. })) => {
141+
match result {
142+
Ok(rtt) => tracing::debug!("Ping to {peer}: {rtt:?}"),
143+
Err(e) => tracing::debug!("Ping to {peer} failed: {e}"),
144+
}
145+
}
146+
SwarmEvent::Behaviour(CombinedEvent::Mdns(mdns::Event::Discovered(peers))) => {
147+
for (peer_id, addr) in peers {
148+
tracing::info!("🔍 mDNS discovered peer {peer_id} at {addr}");
149+
if let Err(e) = swarm.dial(addr) {
150+
tracing::warn!("Failed to dial discovered peer: {e}");
151+
}
152+
}
153+
}
154+
SwarmEvent::Behaviour(CombinedEvent::Mdns(mdns::Event::Expired(peers))) => {
155+
for (peer_id, addr) in peers {
156+
tracing::debug!("mDNS peer expired: {peer_id} at {addr}");
157+
}
158+
}
159+
SwarmEvent::IncomingConnection { local_addr, .. } => {
160+
tracing::debug!("Incoming connection on {local_addr}");
161+
}
162+
_ => {}
163+
}
164+
}
165+
166+
#[tokio::main]
167+
async fn main() -> anyhow::Result<()> {
168+
// Initialize logging
169+
tracing_subscriber::fmt()
170+
.with_env_filter(EnvFilter::from_default_env().add_directive("debug".parse()?))
171+
.init();
172+
173+
let args = Args::parse();
174+
175+
// Create local peer info
176+
let local_info = LocalPeerInfo::new(
177+
"v1.0.0", // charon_version
178+
vec![0xDE, 0xAD, 0xBE, 0xEF], // lock_hash (example)
179+
"abc1234", // git_hash
180+
false, // builder_api_enabled
181+
&args.nickname, // nickname
182+
);
183+
184+
// Create peerinfo config with custom interval for demonstration
185+
let peerinfo_config = Config::new(local_info)
186+
.with_interval(Duration::from_secs(args.interval))
187+
.with_timeout(Duration::from_secs(10));
188+
189+
let mut swarm = build_swarm(peerinfo_config)?;
190+
191+
let local_peer_id = *swarm.local_peer_id();
192+
tracing::info!("Local peer id: {local_peer_id}");
193+
tracing::info!("mDNS auto-discovery enabled");
194+
195+
// Listen on the specified port
196+
let listen_addr: Multiaddr = format!("/ip4/0.0.0.0/tcp/{}", args.port).parse()?;
197+
swarm.listen_on(listen_addr)?;
198+
199+
// Dial the specified address if provided
200+
if let Some(dial_addr) = &args.dial {
201+
tracing::info!("Dialing {dial_addr}");
202+
swarm.dial(dial_addr.clone())?;
203+
}
204+
205+
tracing::info!(
206+
"Peerinfo example started with nickname '{}', interval {}s",
207+
args.nickname,
208+
args.interval
209+
);
210+
tracing::info!("Press Ctrl+C to exit");
211+
212+
// Main event loop
213+
loop {
214+
tokio::select! {
215+
event = swarm.select_next_some() => handle_event(event, &mut swarm),
216+
_ = signal::ctrl_c() => {
217+
tracing::info!("Received Ctrl+C, shutting down...");
218+
break;
219+
}
220+
}
221+
}
222+
223+
Ok(())
224+
}

0 commit comments

Comments
 (0)