diff --git a/Cargo.lock b/Cargo.lock index ee7578ebc..5e285f83d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + [[package]] name = "bstr" version = "0.2.17" @@ -355,12 +361,32 @@ dependencies = [ "ff", "generic-array", "group", + "pkcs8", "rand_core", "sec1", "subtle", "zeroize", ] +[[package]] +name = "enr" +version = "0.6.2" +source = "git+https://github.com/sigp/enr#125f8a5f2deede3e47e852ea70fc9b6e1e9c6e50" +dependencies = [ + "base64", + "bs58", + "bytes", + "hex", + "k256", + "log", + "rand", + "rlp", + "secp256k1", + "serde", + "sha3", + "zeroize", +] + [[package]] name = "ethabi" version = "17.2.0" @@ -1501,6 +1527,16 @@ dependencies = [ [[package]] name = "reth-p2p" version = "0.1.0" +dependencies = [ + "enr", + "secp256k1", + "serde", + "serde_derive", + "tempfile", + "thiserror", + "toml", + "tracing", +] [[package]] name = "reth-primitives" @@ -1718,6 +1754,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7649a0b3ffb32636e60c7ce0d70511eda9c52c658cd0634e194d5a19943aeff" dependencies = [ + "rand", "secp256k1-sys", ] diff --git a/crates/net/p2p/Cargo.toml b/crates/net/p2p/Cargo.toml index ac3fef407..c0c839578 100644 --- a/crates/net/p2p/Cargo.toml +++ b/crates/net/p2p/Cargo.toml @@ -8,3 +8,16 @@ readme = "README.md" description = "Utilities for interacting with ethereum's peer to peer network." [dependencies] +enr = { git = "https://github.com/sigp/enr", features = ["serde", "rust-secp256k1"] } +serde = "1.0.145" +serde_derive = "1.0.145" +thiserror = "1.0.37" +toml = "0.5.9" +tracing = "0.1.36" + +[dev-dependencies] +tempfile = "3.3.0" + +[dev-dependencies.secp256k1] +version = "0.24" +features = ["rand-std"] diff --git a/crates/net/p2p/src/anchor.rs b/crates/net/p2p/src/anchor.rs new file mode 100644 index 000000000..eb6881743 --- /dev/null +++ b/crates/net/p2p/src/anchor.rs @@ -0,0 +1,207 @@ +//! Peer persistence utilities + +use std::{ + collections::HashSet, + fs::OpenOptions, + io::{Read, Write}, + path::{Path, PathBuf}, +}; + +use enr::{secp256k1::SecretKey, Enr}; +use serde_derive::{Deserialize, Serialize}; +use thiserror::Error; +use tracing::error; + +// TODO: enforce one-to-one mapping between IP and key + +/// Contains a list of peers to persist across node restarts. +/// +/// Addresses in this list can come from: +/// * Discovery methods (discv4, discv5, dnsdisc) +/// * Known static peers +/// +/// Updates to this list can come from: +/// * Discovery methods (discv4, discv5, dnsdisc) +/// * All peers we connect to from discovery methods are outbound peers. +/// * Inbound connections +/// * Updates for inbound connections must be based on the address advertised in the node record +/// for that peer, if a node record exists. +/// * Updates for inbound connections must NOT be based on the peer's remote address, since its +/// port may be ephemeral. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Anchor { + /// Peers that have been obtained discovery sources when the node is running + pub discovered_peers: HashSet>, + + /// Pre-determined peers to reach out to + pub static_peers: HashSet>, +} + +impl Anchor { + /// Adds peers to the discovered peer list + pub fn add_discovered(&mut self, new_peers: Vec>) { + self.discovered_peers.extend(new_peers); + } + + /// Adds peers to the static peer list. + pub fn add_static(&mut self, new_peers: Vec>) { + self.static_peers.extend(new_peers); + } + + /// Returns true if both peer lists are empty + pub fn is_empty(&self) -> bool { + self.static_peers.is_empty() && self.discovered_peers.is_empty() + } +} + +/// An error that can occur while parsing a peer list from a file +#[derive(Debug, Error)] +pub enum AnchorError { + /// Error opening the anchor file + #[error("Could not open or write to the anchor file: {0}")] + IoError(#[from] std::io::Error), + + /// Error occurred when loading the anchor file from TOML + #[error("Could not deserialize the peer list from TOML: {0}")] + LoadError(#[from] toml::de::Error), + + /// Error occurred when saving the anchor file to TOML + #[error("Could not serialize the peer list as TOML: {0}")] + SaveError(#[from] toml::ser::Error), +} + +/// A version of [`Anchor`] that is loaded from a TOML file and saves its contents when it is +/// dropped. +#[derive(Debug)] +pub struct PersistentAnchor { + /// The list of addresses to persist + anchor: Anchor, + + /// The Path to store the anchor file + path: PathBuf, +} + +impl PersistentAnchor { + /// This will attempt to load the [`Anchor`] from a file, and if the file doesn't exist it will + /// attempt to initialize it with an empty peer list. + pub fn new_from_file(path: &Path) -> Result { + let mut binding = OpenOptions::new(); + let rw_opts = binding.read(true).write(true); + + let mut file = if path.try_exists()? { + rw_opts.open(path)? + } else { + rw_opts.create(true).open(path)? + }; + + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + // if the file exists but is empty then we should initialize the file format and return + // an empty [`Anchor`] + if contents.is_empty() { + let mut anchor = Self { anchor: Anchor::default(), path: path.to_path_buf() }; + anchor.save_toml()?; + return Ok(anchor) + } + + let anchor: Anchor = toml::from_str(&contents)?; + Ok(Self { anchor, path: path.to_path_buf() }) + } + + /// Save the contents of the [`Anchor`] into the associated file as TOML. + pub fn save_toml(&mut self) -> Result<(), AnchorError> { + let mut file = OpenOptions::new().read(true).write(true).create(true).open(&self.path)?; + + if !self.anchor.is_empty() { + let anchor_contents = toml::to_string_pretty(&self.anchor)?; + file.write_all(anchor_contents.as_bytes())?; + } + Ok(()) + } +} + +impl Drop for PersistentAnchor { + fn drop(&mut self) { + if let Err(save_error) = self.save_toml() { + error!("Could not save anchor to file: {}", save_error) + } + } +} + +#[cfg(test)] +mod tests { + use enr::{ + secp256k1::{rand::thread_rng, SecretKey}, + EnrBuilder, + }; + use std::{fs::remove_file, net::Ipv4Addr}; + use tempfile::tempdir; + + use super::{Anchor, PersistentAnchor}; + + #[test] + fn serde_read_toml() { + let _: Anchor = toml::from_str(r#" + discovered_peers = [ + "enr:-Iu4QGuiaVXBEoi4kcLbsoPYX7GTK9ExOODTuqYBp9CyHN_PSDtnLMCIL91ydxUDRPZ-jem-o0WotK6JoZjPQWhTfEsTgmlkgnY0gmlwhDbOLfeJc2VjcDI1NmsxoQLVqNEoCVTC74VmUx25USyFe7lL0TgpXHaCX9CDy9H6boN0Y3CCIyiDdWRwgiMo", + "enr:-Iu4QLNTiVhgyDyvCBnewNcn9Wb7fjPoKYD2NPe-jDZ3_TqaGFK8CcWr7ai7w9X8Im_ZjQYyeoBP_luLLBB4wy39gQ4JgmlkgnY0gmlwhCOhiGqJc2VjcDI1NmsxoQMrmBYg_yR_ZKZKoLiChvlpNqdwXwodXmgw_TRow7RVwYN0Y3CCIyiDdWRwgiMo", + ] + static_peers = [ + "enr:-Iu4QLpJhdfRFsuMrAsFQOSZTIW1PAf7Ndg0GB0tMByt2-n1bwVgLsnHOuujMg-YLns9g1Rw8rfcw1KCZjQrnUcUdekNgmlkgnY0gmlwhA01ZgSJc2VjcDI1NmsxoQPk2OMW7stSjbdcMgrKEdFOLsRkIuxgBFryA3tIJM0YxYN0Y3CCIyiDdWRwgiMo", + "enr:-Iu4QBHuAmMN5ogZP_Mwh_bADnIOS2xqj8yyJI3EbxW66WKtO_JorshNQJ1NY8zo-u3G7HQvGW3zkV6_kRx5d0R19bETgmlkgnY0gmlwhDRCMUyJc2VjcDI1NmsxoQJZ8jY1HYauxirnJkVI32FoN7_7KrE05asCkZb7nj_b-YN0Y3CCIyiDdWRwgiMo", + ] + "#).expect("Parsing valid TOML into an Anchor should not fail"); + } + + #[test] + fn create_empty_anchor() { + let file_name = "temp_anchor.toml"; + let temp_file_path = tempdir().unwrap().path().with_file_name(file_name); + + // this test's purpose is to make sure new_from_file works if the file doesn't exist + assert!(!temp_file_path.exists()); + let persistent_anchor = PersistentAnchor::new_from_file(&temp_file_path); + + // make sure to clean up + let anchor = persistent_anchor.unwrap(); + + // need to drop the PersistentAnchor explicitly before cleanup or it will be saved + drop(anchor); + remove_file(&temp_file_path).unwrap(); + } + + #[test] + fn save_temp_anchor() { + let file_name = "temp_anchor_two.toml"; + let temp_file_path = tempdir().unwrap().path().with_file_name(file_name); + + let mut persistent_anchor = PersistentAnchor::new_from_file(&temp_file_path).unwrap(); + + // add some ENRs to both lists + let mut rng = thread_rng(); + + let key = SecretKey::new(&mut rng); + let ip = Ipv4Addr::new(192, 168, 1, 1); + let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + persistent_anchor.anchor.add_discovered(vec![enr]); + + let key = SecretKey::new(&mut rng); + let ip = Ipv4Addr::new(192, 168, 1, 2); + let enr = EnrBuilder::new("v4").ip4(ip).tcp4(8000).build(&key).unwrap(); + persistent_anchor.anchor.add_static(vec![enr]); + + // save the old struct before dropping + let prev_anchor = persistent_anchor.anchor.clone(); + drop(persistent_anchor); + + // finally check file contents + let new_persistent = PersistentAnchor::new_from_file(&temp_file_path).unwrap(); + let new_anchor = new_persistent.anchor.clone(); + + // need to drop the PersistentAnchor explicitly before cleanup or it will be saved + drop(new_persistent); + remove_file(&temp_file_path).unwrap(); + assert_eq!(new_anchor, prev_anchor); + } +} diff --git a/crates/net/p2p/src/lib.rs b/crates/net/p2p/src/lib.rs index bec2e0709..f4cfb7030 100644 --- a/crates/net/p2p/src/lib.rs +++ b/crates/net/p2p/src/lib.rs @@ -6,3 +6,5 @@ ))] //! Utilities for interacting with ethereum's peer to peer network. + +pub mod anchor;