Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
913b56b
feat: add tracing
varex83 Dec 5, 2025
51a691f
fix: linting, cargo-deny
varex83 Dec 5, 2025
e3ece2e
Merge remote-tracking branch 'origin/main' into bohdan/tracing
varex83 Dec 8, 2025
85a9b9c
feat: add p2p config
varex83 Dec 8, 2025
65d5ae7
fix: remove comment
varex83 Dec 9, 2025
a312796
Merge remote-tracking branch 'origin/main' into bohdan/p2p-config
varex83 Dec 9, 2025
1cc2a29
feat: add p2p metrics
varex83 Dec 9, 2025
c432d86
fix: rename label in example
varex83 Dec 9, 2025
aeaa971
fix: linting
varex83 Dec 9, 2025
65d3a49
Merge branch 'bohdan/p2p-config' into bohdan/p2p-create-node
varex83 Dec 10, 2025
a82bbc7
feat: [wip] add relay/ping
varex83 Dec 18, 2025
a2fe791
feat: add behaviours module, add relay client support
varex83 Dec 22, 2025
30a550b
fix: linter warnings
varex83 Dec 22, 2025
df9fdce
Merge remote-tracking branch 'origin/main' into bohdan/p2p-create-node
varex83 Dec 22, 2025
ddbb57b
fix: remove wildcard dependencies
varex83 Dec 22, 2025
3c5d3fe
feat: add connection gater
varex83 Dec 23, 2025
c663f61
fix: linter warnings
varex83 Dec 23, 2025
a244063
fix: linter warnings
varex83 Dec 23, 2025
adfc1ef
feat: create behaviour builder
varex83 Dec 24, 2025
31c4e27
feat: add relay server functionality
varex83 Dec 29, 2025
152fbcf
chore: fix linter, polish code
varex83 Dec 29, 2025
529d794
fix: cargo deny dependency issue
varex83 Dec 29, 2025
1eb752e
chore: add docs
varex83 Dec 30, 2025
7b72369
refactor: move relay-server to a different crate
varex83 Dec 30, 2025
ee147d0
feat: add peerinfo (mvp)
varex83 Jan 5, 2026
351db0e
fix: remove non-relevant comment
varex83 Jan 5, 2026
5b13d50
Merge remote-tracking branch 'origin/main' into bohdan/p2p-peerinfo
varex83 Jan 20, 2026
ef8dae3
fix: move dependencies to workspace
varex83 Jan 20, 2026
e6944a1
feat: add compatibility tests
varex83 Jan 20, 2026
b60454a
Merge remote-tracking branch 'origin/main' into bohdan/p2p-peerinfo
varex83 Jan 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 188 additions & 54 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ members = [
"crates/charon-p2p",
"crates/charon-testutil",
"crates/tracing",
"crates/relay-server",
"crates/peerinfo"
]
resolver = "3"

Expand All @@ -23,11 +25,13 @@ license = "Apache-2.0" # TODO(template) updat
publish = false

[workspace.dependencies]
anyhow = "1"
alloy = { version = "1.1.3", features = ["essentials"] }
axum = "0.8.6"
blst = "0.3.13"
cancellation = "0.1.0"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.5.53", features = ["derive"] }
crossbeam = "0.8.4"
hex = { version = "^0.4.3" }
prost = "0.14"
Expand All @@ -39,6 +43,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
thiserror = "2.0.12"
tokio = { version = "1", features = ["full"] }
tokio-util = "0.7.11"
libp2p = { version = "0.56", features = ["full", "secp256k1"] }
url = { version = "2.5.7" }
uuid = { version = "1.16", features = ["serde", "v4"] }
Expand All @@ -61,6 +66,9 @@ charon-eth2 = { path = "crates/charon-eth2" }
charon-k1util = { path = "crates/charon-k1util" }
charon-p2p = { path = "crates/charon-p2p" }
charon-testutil = { path = "crates/charon-testutil" }
charon-tracing = { path = "crates/tracing" }
charon-relay-server = { path = "crates/relay-server" }
charon-peerinfo = { path = "crates/peerinfo" }

[workspace.lints.rust]
missing_docs = "deny"
Expand Down
17 changes: 16 additions & 1 deletion crates/charon-p2p/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ license.workspace = true
publish.workspace = true

[dependencies]
axum.workspace = true
chrono.workspace = true
libp2p.workspace = true
thiserror.workspace = true
Expand All @@ -15,12 +16,26 @@ charon-eth2.workspace = true
charon-k1util.workspace = true
vise.workspace = true
tokio.workspace = true
tokio-util.workspace = true
rand.workspace = true
tempfile.workspace = true
charon-tracing.workspace = true
tracing.workspace = true
serde.workspace = true
serde_json.workspace = true

[dev-dependencies]
charon-testutil.workspace = true
tempfile.workspace = true
vise-exporter.workspace = true
anyhow.workspace = true
clap.workspace = true

[lints]
workspace = true

[features]
mdns = ["libp2p/mdns"]

[[example]]
name = "p2p"
required-features = ["mdns"]
148 changes: 148 additions & 0 deletions crates/charon-p2p/examples/p2p.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//! P2P example
//!
//! This example creates a Pluto P2P node and connects to a relay.
//! Also, it discovers other Pluto nodes using mDNS (requires the `mdns`
//! feature).

use anyhow::Result;
use charon_eth2::enr::Record;
use charon_p2p::{
behaviours::{
pluto::PlutoBehaviourEvent,
pluto_mdns::{PlutoMdnsBehaviour, PlutoMdnsBehaviourEvent},
},
config::P2PConfig,
p2p::{Node, NodeType},
};
use clap::Parser;
use k256::elliptic_curve::rand_core::OsRng;
use libp2p::{Multiaddr, futures::StreamExt, identify, multiaddr::Protocol, swarm::SwarmEvent};
use tokio::signal;

/// Command line arguments
#[derive(Debug, Parser)]
pub struct Args {
/// The port to listen on
#[arg(short, long, default_value = "1050")]
pub port: u16,
/// The ENRs to listen on
#[arg(short, long)]
pub enrs: Vec<String>,
/// The relay URL to dial
#[arg(short, long)]
pub relay_url: Option<Multiaddr>,
}

#[tokio::main]
async fn main() -> Result<()> {
let key = k256::SecretKey::random(&mut OsRng);
let mut p2p: Node<_> = Node::new(
P2PConfig::default(),
key.clone(),
false,
NodeType::QUIC,
PlutoMdnsBehaviour::new,
)?;

let args = Args::parse();

let swarm = &mut p2p.swarm;

let enr = Record::new(key.clone(), vec![])?;

if let Some(relay_url) = &args.relay_url {
swarm.dial(relay_url.clone())?;
println!("Dialed relay");
let mut learned_observed_addr = false;
let mut told_relay_observed_addr = false;

loop {
match swarm
.next()
.await
.ok_or(anyhow::anyhow!("Swarm event is None"))?
{
SwarmEvent::NewListenAddr { .. } => {}
SwarmEvent::Dialing { .. } => {}
SwarmEvent::ConnectionEstablished { .. } => {}
SwarmEvent::Behaviour(PlutoMdnsBehaviourEvent::Pluto(
PlutoBehaviourEvent::Ping(_),
)) => {}
SwarmEvent::Behaviour(PlutoMdnsBehaviourEvent::Pluto(
PlutoBehaviourEvent::Identify(identify::Event::Sent { .. }),
)) => {
println!("Told relay its public address");
told_relay_observed_addr = true;
}
SwarmEvent::Behaviour(PlutoMdnsBehaviourEvent::Pluto(
PlutoBehaviourEvent::Identify(identify::Event::Received {
info: identify::Info { observed_addr, .. },
..
}),
)) => {
println!("Relay told us our observed address: {}", observed_addr);
learned_observed_addr = true;
}
event => panic!("{event:?}"),
}
if learned_observed_addr && told_relay_observed_addr {
break;
}
}
}

println!("ENR: {}", enr);

swarm.listen_on(format!("/ip4/0.0.0.0/udp/{}/quic-v1", args.port).parse()?)?;
swarm.listen_on(format!("/ip4/0.0.0.0/tcp/{}", args.port).parse()?)?;
if let Some(relay_url) = args.relay_url {
swarm.listen_on(relay_url.with(Protocol::P2pCircuit))?;
}

loop {
tokio::select! {
event = swarm.select_next_some() => match event {
SwarmEvent::Behaviour(PlutoMdnsBehaviourEvent::Pluto(PlutoBehaviourEvent::Identify(identify::Event::Received { info: identify::Info { observed_addr, .. }, .. }))) => {
swarm.add_external_address(observed_addr.clone());
println!("Address observed {}", observed_addr);
}
SwarmEvent::Behaviour(PlutoMdnsBehaviourEvent::Pluto(PlutoBehaviourEvent::Relay(event))) => {
println!("Got relay event: {:?}", event);
},
SwarmEvent::Behaviour(PlutoMdnsBehaviourEvent::Mdns(libp2p::mdns::Event::Discovered(nodes))) => {
for node in nodes {
println!("Discovered node: {:?}", node);
swarm.dial(node.1)?;
}
}
SwarmEvent::NewListenAddr { address, .. } => {
println!("Local node is listening on {address}");
}
SwarmEvent::Behaviour(PlutoMdnsBehaviourEvent::Pluto(PlutoBehaviourEvent::Ping(ping_event))) => {
println!("Got ping event: {:?}", ping_event);
}
SwarmEvent::IncomingConnection { connection_id, local_addr, send_back_addr } => {
println!("Incoming connection (id={connection_id}) from {:?} (send on {:?})", local_addr, send_back_addr);
}
SwarmEvent::IncomingConnectionError {peer_id,connection_id,error, local_addr, send_back_addr } => {
println!("Incoming connection (id={connection_id}) error from {:?} (send on {:?} to {:?}): {:?}", peer_id, local_addr, send_back_addr, error);
}
event => {
println!("{:?}", event);
}
},
_ = signal::ctrl_c() => {
println!("\nReceived Ctrl+C, shutting down gracefully...");

// Perform cleanup
let _ = swarm;
drop(p2p);

println!("Shutdown complete");
break;
}
}
}

Ok(())
}
9 changes: 9 additions & 0 deletions crates/charon-p2p/src/behaviours/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Behaviours.
#![allow(missing_docs)] // we need to allow missing docs for the derive macro

/// Pluto behaviour.
pub mod pluto;

#[cfg(feature = "mdns")]
/// Pluto Mdns behaviour.
pub mod pluto_mdns;
103 changes: 103 additions & 0 deletions crates/charon-p2p/src/behaviours/pluto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! Pluto behaviour.

use std::time::Duration;

use libp2p::{identify, identity::Keypair, ping, relay, swarm::NetworkBehaviour};

use crate::gater::ConnGater;

/// Pluto network behaviour.
#[derive(NetworkBehaviour)]
pub struct PlutoBehaviour {
/// Connection gater behaviour.
pub gater: ConnGater,
/// Relay client behaviour.
pub relay: relay::client::Behaviour,
/// Identify behaviour.
pub identify: identify::Behaviour,
/// Ping behaviour.
pub ping: ping::Behaviour,
}

impl PlutoBehaviour {
/// Creates a new Pluto behaviour with default configuration.
pub fn new(key: &Keypair, relay_client: relay::client::Behaviour) -> Self {
PlutoBehaviourBuilder::default().build(key, relay_client)
}

/// Returns a new builder for configuring a PlutoBehaviour.
pub fn builder() -> PlutoBehaviourBuilder {
PlutoBehaviourBuilder::default()
}
}

/// Builder for [`PlutoBehaviour`].
#[derive(Debug, Clone)]
pub struct PlutoBehaviourBuilder {
gater: Option<ConnGater>,
identify_protocol: String,
ping_interval: Duration,
ping_timeout: Duration,
}

impl Default for PlutoBehaviourBuilder {
fn default() -> Self {
Self {
gater: None,
identify_protocol: "/pluto/1.0.0-alpha".into(),
ping_interval: Duration::from_secs(1),
ping_timeout: Duration::from_secs(2),
}
}
}

impl PlutoBehaviourBuilder {
/// Creates a new builder with default configuration.
pub fn new() -> Self {
Self::default()
}

/// Sets the connection gater.
pub fn with_gater(mut self, gater: ConnGater) -> Self {
self.gater = Some(gater);
self
}

/// Sets the identify protocol string.
pub fn with_identify_protocol(mut self, protocol: impl Into<String>) -> Self {
self.identify_protocol = protocol.into();
self
}

/// Sets the ping interval.
pub fn with_ping_interval(mut self, interval: Duration) -> Self {
self.ping_interval = interval;
self
}

/// Sets the ping timeout.
pub fn with_ping_timeout(mut self, timeout: Duration) -> Self {
self.ping_timeout = timeout;
self
}

/// Builds the [`PlutoBehaviour`] with the provided keypair and relay
/// client.
pub fn build(self, key: &Keypair, relay_client: relay::client::Behaviour) -> PlutoBehaviour {
PlutoBehaviour {
gater: self
.gater
.unwrap_or_else(|| ConnGater::new_conn_gater(vec![], vec![])),
relay: relay_client,
identify: identify::Behaviour::new(identify::Config::new(
self.identify_protocol,
key.public(),
)),
ping: ping::Behaviour::new(
ping::Config::new()
.with_interval(self.ping_interval)
.with_timeout(self.ping_timeout),
),
}
}
}
Loading