-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat(p2p): add anchor file for discovery state #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 13 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
35ca74f
feat(p2p): add anchor file for discovery state
Rjected e6a62b9
move rustdoc and improve error messages
Rjected 34c9c6f
add temp file tests and log drop error
Rjected 5eee279
remove redundant new
Rjected c1260e0
replace println with tracing
Rjected 319524c
show underlying error in custom error message
Rjected fd4f0c9
chore: cargo fmt
Rjected 0284175
change AsRef<Path> to &Path
Rjected d9347a6
remove ineffective dedups
Rjected 64a752a
chore: cargo fmt
Rjected f4d5e24
switch out Vec<Enr<K>> for HashSet<Enr<K>>
Rjected 95f8ef1
cargo fmt
Rjected 925be64
use tempdir instead of of std::env::temp_dir
Rjected af66121
refactor anchor to contain &Path instead of File
Rjected a67730d
use PathBuf instead of Path
Rjected File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| } | ||
|
|
||
| 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)?; | ||
|
Rjected marked this conversation as resolved.
Outdated
|
||
|
|
||
| // if the file does not exist then we should create it | ||
| if file.metadata()?.len() == 0 { | ||
|
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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,3 +6,5 @@ | |
| ))] | ||
|
|
||
| //! Utilities for interacting with ethereum's peer to peer network. | ||
|
|
||
| pub mod anchor; | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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_fileanyways