Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
37 changes: 37 additions & 0 deletions Cargo.lock

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

13 changes: 13 additions & 0 deletions crates/net/p2p/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
211 changes: 211 additions & 0 deletions crates/net/p2p/src/anchor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
//! Peer persistence utilities

use std::{
collections::HashSet,
fs::{File, OpenOptions},
io::{Read, Write},
path::Path,
};

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<Enr<SecretKey>>,

/// Pre-determined peers to reach out to
pub static_peers: HashSet<Enr<SecretKey>>,
}

impl Anchor {
/// Adds peers to the discovered peer list
pub fn add_discovered(&mut self, new_peers: Vec<Enr<SecretKey>>) {
self.discovered_peers.extend(new_peers);
}

/// Adds peers to the static peer list.
pub fn add_static(&mut self, new_peers: Vec<Enr<SecretKey>>) {
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 File handle for the anchor
file: File,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we store the path instead of the file?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah the path should work and is the input for new_from_file anyways

}

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<Self, AnchorError> {
let file = OpenOptions::new().read(true).write(true).create(true).open(path)?;
Comment thread
Rjected marked this conversation as resolved.
Outdated

// if the file does not exist then we should create it
if file.metadata()?.len() == 0 {
Comment thread
Rjected marked this conversation as resolved.
Outdated
let mut anchor = Self { anchor: Anchor::default(), file };
anchor.save_toml()?;
return Ok(anchor)
}

Self::from_toml(file)
}

/// Load the [`Anchor`] from the given TOML file.
pub fn from_toml(mut file: File) -> Result<Self, AnchorError> {
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(), file };
anchor.save_toml()?;
return Ok(anchor)
}

let anchor: Anchor = toml::from_str(&contents)?;
Ok(Self { anchor, file })
}

/// Save the contents of the [`Anchor`] into the associated file as TOML.
pub fn save_toml(&mut self) -> Result<(), AnchorError> {
if !self.anchor.is_empty() {
let anchor_contents = toml::to_string_pretty(&self.anchor)?;
self.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, OpenOptions},
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
remove_file(temp_file_path).unwrap();
persistent_anchor.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 temp_file =
OpenOptions::new().read(true).write(true).create(true).open(&temp_file_path).unwrap();

let mut persistent_anchor = PersistentAnchor::from_toml(temp_file).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 prev_file =
OpenOptions::new().read(true).write(true).create(true).open(&temp_file_path).unwrap();
let new_anchor = PersistentAnchor::from_toml(prev_file).unwrap();

remove_file(temp_file_path).unwrap();
assert_eq!(new_anchor.anchor, prev_anchor);
}
}
2 changes: 2 additions & 0 deletions crates/net/p2p/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
))]

//! Utilities for interacting with ethereum's peer to peer network.

pub mod anchor;