Skip to content

Commit

Permalink
feat(net): discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
refcell committed Aug 25, 2024
1 parent 3d8a7f9 commit 9aec543
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,5 @@ futures = "0.3.30"
async-trait = "0.1.81"
hashbrown = "0.14.5"
parking_lot = "0.12.3"
unsigned-varint = "0.8.0"
rand = { version = "0.8.3", features = ["small_rng"], default-features = false }
2 changes: 2 additions & 0 deletions crates/net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ rust-version.workspace = true
[dependencies]
# Alloy
alloy.workspace = true
alloy-rlp.workspace = true

# Kona
kona-primitives = { git = "https://github.com/ethereum-optimism/kona", default-features = true }
Expand All @@ -30,3 +31,4 @@ eyre.workspace = true
tokio.workspace = true
tracing.workspace = true
lazy_static.workspace = true
unsigned-varint.workspace = true
22 changes: 22 additions & 0 deletions crates/net/src/bootnodes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Bootnodes for consensus network discovery.
use discv5::enr::{CombinedKey, Enr};
use lazy_static::lazy_static;
use std::str::FromStr;

lazy_static! {
/// Default bootnodes to use.
pub static ref BOOTNODES: Vec<Enr<CombinedKey>> = [
// Optimism Mainnet Bootnodes
Enr::from_str("enr:-J64QBbwPjPLZ6IOOToOLsSjtFUjjzN66qmBZdUexpO32Klrc458Q24kbty2PdRaLacHM5z-cZQr8mjeQu3pik6jPSOGAYYFIqBfgmlkgnY0gmlwhDaRWFWHb3BzdGFja4SzlAUAiXNlY3AyNTZrMaECmeSnJh7zjKrDSPoNMGXoopeDF4hhpj5I0OsQUUt4u8uDdGNwgiQGg3VkcIIkBg").unwrap(),
Enr::from_str("enr:-J64QAlTCDa188Hl1OGv5_2Kj2nWCsvxMVc_rEnLtw7RPFbOfqUOV6khXT_PH6cC603I2ynY31rSQ8sI9gLeJbfFGaWGAYYFIrpdgmlkgnY0gmlwhANWgzCHb3BzdGFja4SzlAUAiXNlY3AyNTZrMaECkySjcg-2v0uWAsFsZZu43qNHppGr2D5F913Qqs5jDCGDdGNwgiQGg3VkcIIkBg").unwrap(),
Enr::from_str("enr:-J24QGEzN4mJgLWNTUNwj7riVJ2ZjRLenOFccl2dbRFxHHOCCZx8SXWzgf-sLzrGs6QgqSFCvGXVgGPBkRkfOWlT1-iGAYe6Cu93gmlkgnY0gmlwhCJBEUSHb3BzdGFja4OkAwCJc2VjcDI1NmsxoQLuYIwaYOHg3CUQhCkS-RsSHmUd1b_x93-9yQ5ItS6udIN0Y3CCIyuDdWRwgiMr").unwrap(),

// Base Mainnet Bootnodes
Enr::from_str("enr:-J24QNz9lbrKbN4iSmmjtnr7SjUMk4zB7f1krHZcTZx-JRKZd0kA2gjufUROD6T3sOWDVDnFJRvqBBo62zuF-hYCohOGAYiOoEyEgmlkgnY0gmlwhAPniryHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQKNVFlCxh_B-716tTs-h1vMzZkSs1FTu_OYTNjgufplG4N0Y3CCJAaDdWRwgiQG").unwrap(),
Enr::from_str("enr:-J24QH-f1wt99sfpHy4c0QJM-NfmsIfmlLAMMcgZCUEgKG_BBYFc6FwYgaMJMQN5dsRBJApIok0jFn-9CS842lGpLmqGAYiOoDRAgmlkgnY0gmlwhLhIgb2Hb3BzdGFja4OFQgCJc2VjcDI1NmsxoQJ9FTIv8B9myn1MWaC_2lJ-sMoeCDkusCsk4BYHjjCq04N0Y3CCJAaDdWRwgiQG").unwrap(),
Enr::from_str("enr:-J24QDXyyxvQYsd0yfsN0cRr1lZ1N11zGTplMNlW4xNEc7LkPXh0NAJ9iSOVdRO95GPYAIc6xmyoCCG6_0JxdL3a0zaGAYiOoAjFgmlkgnY0gmlwhAPckbGHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQJwoS7tzwxqXSyFL7g0JM-KWVbgvjfB8JA__T7yY_cYboN0Y3CCJAaDdWRwgiQG").unwrap(),
Enr::from_str("enr:-J24QHmGyBwUZXIcsGYMaUqGGSl4CFdx9Tozu-vQCn5bHIQbR7On7dZbU61vYvfrJr30t0iahSqhc64J46MnUO2JvQaGAYiOoCKKgmlkgnY0gmlwhAPnCzSHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQINc4fSijfbNIiGhcgvwjsjxVFJHUstK9L1T8OTKUjgloN0Y3CCJAaDdWRwgiQG").unwrap(),
Enr::from_str("enr:-J24QG3ypT4xSu0gjb5PABCmVxZqBjVw9ca7pvsI8jl4KATYAnxBmfkaIuEqy9sKvDHKuNCsy57WwK9wTt2aQgcaDDyGAYiOoGAXgmlkgnY0gmlwhDbGmZaHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQIeAK_--tcLEiu7HvoUlbV52MspE0uCocsx1f_rYvRenIN0Y3CCJAaDdWRwgiQG").unwrap(),
].to_vec();
}
135 changes: 135 additions & 0 deletions crates/net/src/discovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//! Discovery Module.
use eyre::Result;
use std::time::Duration;
use tokio::{
sync::mpsc::{channel, Receiver},
time::sleep,
};
use tracing::{trace, warn};

use discv5::{
enr::{CombinedKey, Enr, NodeId},
ConfigBuilder, Discv5, ListenConfig,
};

use crate::{
bootnodes::BOOTNODES,
op_enr::OpStackEnr,
types::{NetworkAddress, Peer},
};

/// The number of peers to buffer in the channel.
const DISCOVERY_PEER_CHANNEL_SIZE: usize = 256;

/// Discovery service builder.
#[derive(Debug, Default, Clone)]
pub struct DiscoveryBuilder {
/// The discovery service address.
address: Option<NetworkAddress>,
/// The chain ID of the network.
chain_id: Option<u64>,
}

impl DiscoveryBuilder {
/// Creates a new discovery builder.
pub fn new() -> Self {
Self::default()
}

/// Sets the discovery service address.
pub fn with_address(mut self, address: NetworkAddress) -> Self {
self.address = Some(address);
self
}

/// Sets the chain ID of the network.
pub fn with_chain_id(mut self, chain_id: u64) -> Self {
self.chain_id = Some(chain_id);
self
}

/// Generates an [Enr] and creates a [Discv5] service struct
fn create_disc(&self) -> Result<Discv5> {
let addr = self.address.ok_or_else(|| eyre::eyre!("address not set"))?;
let chain_id = self.chain_id.ok_or_else(|| eyre::eyre!("chain ID not set"))?;
let opstack = OpStackEnr::new(chain_id, 0);
let opstack_data: Vec<u8> = opstack.into();

let key = CombinedKey::generate_secp256k1();
let enr = Enr::builder().add_value_rlp("opstack", opstack_data.into()).build(&key)?;
let listen_config = ListenConfig::from_ip(addr.ip.into(), addr.port);
let config = ConfigBuilder::new(listen_config).build();

Discv5::new(enr, key, config).map_err(|_| eyre::eyre!("could not create disc service"))
}

/// Spawns a new [Discv5] discovery service in a new tokio task.
///
/// Returns a [Receiver] to receive [Peer] structs.
///
/// ## Errors
///
/// Returns an error if the address or chain ID is not set
/// on the [DiscoveryBuilder].
///
/// ## Example
///
/// ```no_run
/// use op_net::discovery::DiscoveryBuilder;
///
/// let builder = DiscoveryBuilder::new()
/// .with_address("")
/// .with_chain_id(10) // OP Mainnet chain id
/// .start()
/// .expect("Failed to start discovery service");
///
/// loop {
/// if let Some(peer) = builder.recv().await {
/// println!("Received peer: {:?}", peer);
/// }
/// ```
pub fn start(self) -> Result<Receiver<Peer>> {
let chain_id = self.chain_id.ok_or_else(|| eyre::eyre!("chain ID not set"))?;

// Clone the bootnodes since the spawned thread takes mutable ownership.
let bootnodes = BOOTNODES.clone();

// Construct the discovery service.
let mut disc = self.create_disc()?;

// Create a multi-producer, single-consumer (mpsc) channel to receive
// peers bounded by `DISCOVERY_PEER_CHANNEL_SIZE`.
let (sender, recv) = channel::<Peer>(DISCOVERY_PEER_CHANNEL_SIZE);

tokio::spawn(async move {
bootnodes.into_iter().for_each(|enr| _ = disc.add_enr(enr));
disc.start().await.unwrap();

trace!("Started peer discovery");

loop {
let target = NodeId::random();
match disc.find_node(target).await {
Ok(nodes) => {
let peers = nodes
.iter()
.filter(|node| OpStackEnr::is_valid_node(node, chain_id))
.flat_map(Peer::try_from);

for peer in peers {
_ = sender.send(peer).await;
}
}
Err(err) => {
warn!("discovery error: {:?}", err);
}
}

sleep(Duration::from_secs(10)).await;
}
});

Ok(recv)
}
}
3 changes: 3 additions & 0 deletions crates/net/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]

pub mod behaviour;
pub mod bootnodes;
pub mod builder;
pub mod config;
pub mod discovery;
pub mod driver;
pub mod event;
pub mod handler;
pub mod op_enr;
pub mod types;
65 changes: 65 additions & 0 deletions crates/net/src/op_enr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Contains the OP Stack Enr Type.
use discv5::enr::{CombinedKey, Enr};
use eyre::Result;
use unsigned_varint::{decode, encode};

/// The unique L2 network identifier
#[derive(Debug, Clone, Copy, Default)]
pub struct OpStackEnr {
/// Chain ID
pub chain_id: u64,
/// The version. Always set to 0.
pub version: u64,
}

impl OpStackEnr {
/// Instantiates a new Op Stack Enr.
pub fn new(chain_id: u64, version: u64) -> Self {
Self { chain_id, version }
}

/// Returns `true` if a node [Enr] contains an `opstack` key and is on the same network.
pub fn is_valid_node(node: &Enr<CombinedKey>, chain_id: u64) -> bool {
node.get_raw_rlp("opstack")
.map(|opstack| {
OpStackEnr::try_from(opstack)
.map(|opstack| opstack.chain_id == chain_id && opstack.version == 0)
.unwrap_or_default()
})
.unwrap_or_default()
}
}

impl TryFrom<&[u8]> for OpStackEnr {
type Error = eyre::Report;

/// Converts a slice of RLP encoded bytes to Op Stack Enr Data.
fn try_from(value: &[u8]) -> Result<Self> {
// TODO: rlp decode first?
// let bytes = Vec::<u8>::decode(&mut value)?;
let mut bytes = value;
let (chain_id, rest) =
decode::u64(bytes).map_err(|_| eyre::eyre!("could not decode chain id"))?;
bytes = rest;
let (version, _) =
decode::u64(bytes).map_err(|_| eyre::eyre!("could not decode chain id"))?;

Ok(Self { chain_id, version })
}
}

impl From<OpStackEnr> for Vec<u8> {
/// Converts Op Stack Enr data to a vector of bytes.
fn from(value: OpStackEnr) -> Vec<u8> {
let mut chain_id_buf = encode::u128_buffer();
let chain_id_slice = encode::u128(value.chain_id as u128, &mut chain_id_buf);

let mut version_buf = encode::u128_buffer();
let version_slice = encode::u128(value.version as u128, &mut version_buf);

let opstack = [chain_id_slice, version_slice].concat();

alloy_rlp::encode(&opstack).to_vec()
}
}

0 comments on commit 9aec543

Please sign in to comment.