From b93d88b58c1bbbb501cc64d071a1fc5df367037e Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Wed, 27 Nov 2024 14:01:38 +0000 Subject: [PATCH] Addition of seed-check logic to top-level crate (#3801) * Addition of initial seed check logic * updated to call from command line, now need to do something about peer store root output * rework check to delete temp files, add output options, testing --- Cargo.lock | 2 + Cargo.toml | 3 + src/bin/grin.rs | 25 +++++ src/bin/grin.yml | 11 ++ src/bin/tools/mod.rs | 18 ++++ src/bin/tools/seedcheck.rs | 209 +++++++++++++++++++++++++++++++++++++ 6 files changed, 268 insertions(+) create mode 100644 src/bin/tools/mod.rs create mode 100644 src/bin/tools/seedcheck.rs diff --git a/Cargo.lock b/Cargo.lock index 773aa76c1..7f89ef038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -952,8 +952,10 @@ dependencies = [ "humansize", "log", "serde", + "serde_derive", "serde_json", "term", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3ae72b9ff..4e0adbf6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,11 +22,13 @@ path = "src/bin/grin.rs" [dependencies] blake2-rfc = "0.2" chrono = "0.4.11" +thiserror = "1" clap = { version = "2.33", features = ["yaml"] } ctrlc = { version = "3.1", features = ["termination"] } cursive_table_view = "0.15.0" humansize = "1.1.0" serde = "1" +serde_derive = "1" futures = "0.3.19" serde_json = "1" log = "0.4" @@ -40,6 +42,7 @@ grin_keychain = { path = "./keychain", version = "5.4.0-alpha.0" } grin_p2p = { path = "./p2p", version = "5.4.0-alpha.0" } grin_servers = { path = "./servers", version = "5.4.0-alpha.0" } grin_util = { path = "./util", version = "5.4.0-alpha.0" } +grin_store = { path = "./store", version = "5.4.0-alpha.0" } [dependencies.cursive] version = "0.21" diff --git a/src/bin/grin.rs b/src/bin/grin.rs index e50afbb7c..2d32be29a 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -21,6 +21,7 @@ extern crate clap; extern crate log; use crate::config::config::SERVER_CONFIG_FILE_NAME; use crate::core::global; +use crate::tools::check_seeds; use crate::util::init_logger; use clap::App; use futures::channel::oneshot; @@ -32,9 +33,16 @@ use grin_p2p as p2p; use grin_servers as servers; use grin_util as util; use grin_util::logger::LogEntry; +use std::fs::File; +use std::io::Write; use std::sync::mpsc; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + mod cmd; +mod tools; pub mod tui; // include build information @@ -197,6 +205,23 @@ fn real_main() -> i32 { } } + // seedcheck command + ("seedcheck", Some(seedcheck_args)) => { + let is_testnet = seedcheck_args.is_present("testnet"); + let results = check_seeds(is_testnet); + let output = + serde_json::to_string_pretty(&results).expect("Unable to serialize results"); + + if let Some(output_file) = seedcheck_args.value_of("output") { + let mut file = File::create(output_file).expect("Unable to create file"); + writeln!(file, "{}", output).expect("Unable to write data"); + println!("Results written to {}", output_file); + } else { + println!("{}", output); + } + 0 + } + // If nothing is specified, try to just use the config file instead // this could possibly become the way to configure most things // with most command line options being phased out diff --git a/src/bin/grin.yml b/src/bin/grin.yml index 28c7fd0d5..20a493744 100644 --- a/src/bin/grin.yml +++ b/src/bin/grin.yml @@ -92,3 +92,14 @@ subcommands: - hash: help: The header hash to invalidate required: true + - seedcheck: + about: Check the health of seed nodes + args: + - testnet: + help: Run seed check against Testnet (as opposed to Mainnet) + long: testnet + takes_value: false + - output: + help: Output file to write the results to + long: output + takes_value: true diff --git a/src/bin/tools/mod.rs b/src/bin/tools/mod.rs new file mode 100644 index 000000000..e4ab9f5f4 --- /dev/null +++ b/src/bin/tools/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Grin tools +mod seedcheck; + +pub use seedcheck::check_seeds; diff --git a/src/bin/tools/seedcheck.rs b/src/bin/tools/seedcheck.rs new file mode 100644 index 000000000..2ffff88b5 --- /dev/null +++ b/src/bin/tools/seedcheck.rs @@ -0,0 +1,209 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Relatively self-contained seed health checker +use std::sync::Arc; + +use grin_core::core::hash::Hashed; +use grin_core::pow::Difficulty; +use grin_core::{genesis, global}; +use grin_p2p as p2p; +use grin_servers::{resolve_dns_to_addrs, MAINNET_DNS_SEEDS, TESTNET_DNS_SEEDS}; +use std::fs; +use std::net::{SocketAddr, TcpStream}; +use std::time::Duration; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SeedCheckError { + #[error("Seed Connect Error {0}")] + SeedConnectError(String), + #[error("Grin Store Error {0}")] + StoreError(String), +} + +impl From for SeedCheckError { + fn from(e: p2p::Error) -> Self { + SeedCheckError::SeedConnectError(format!("{:?}", e)) + } +} + +impl From for SeedCheckError { + fn from(e: grin_store::lmdb::Error) -> Self { + SeedCheckError::StoreError(format!("{:?}", e)) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SeedCheckResults { + pub mainnet: Vec, + pub testnet: Vec, +} + +impl Default for SeedCheckResults { + fn default() -> Self { + Self { + mainnet: vec![], + testnet: vec![], + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SeedCheckResult { + pub url: String, + pub dns_resolutions_found: bool, + pub success: bool, + pub successful_attempts: Vec, + pub unsuccessful_attempts: Vec, +} + +impl Default for SeedCheckResult { + fn default() -> Self { + Self { + url: "".into(), + dns_resolutions_found: false, + success: false, + successful_attempts: vec![], + unsuccessful_attempts: vec![], + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SeedCheckConnectAttempt { + pub ip_addr: String, + pub handshake_success: bool, + pub user_agent: Option, + pub capabilities: Option, +} + +pub fn check_seeds(is_testnet: bool) -> Vec { + let mut result = vec![]; + let (default_seeds, port) = match is_testnet { + true => (TESTNET_DNS_SEEDS, "13414"), + false => (MAINNET_DNS_SEEDS, "3414"), + }; + + if is_testnet { + global::set_local_chain_type(global::ChainTypes::Testnet); + } + + let config = p2p::types::P2PConfig::default(); + let adapter = Arc::new(p2p::DummyAdapter {}); + let peers = Arc::new(p2p::Peers::new( + p2p::store::PeerStore::new(".__grintmp__/peer_store_root").unwrap(), + adapter, + config.clone(), + )); + + for s in default_seeds.iter() { + info!("Checking seed health for {}", s); + let mut seed_result = SeedCheckResult::default(); + seed_result.url = s.to_string(); + let resolved_dns_entries = resolve_dns_to_addrs(&vec![format!("{}:{}", s, port)]); + if resolved_dns_entries.is_empty() { + info!("FAIL - No dns entries found for {}", s); + result.push(seed_result); + continue; + } + seed_result.dns_resolutions_found = true; + // Check backwards, last contains the latest (at least on my machine!) + for r in resolved_dns_entries.iter().rev() { + let res = check_seed_health(*r, is_testnet, &peers); + if let Ok(p) = res { + info!( + "SUCCESS - Performed Handshake with seed for {} at {}. {} - {:?}", + s, r, p.info.user_agent, p.info.capabilities + ); + //info!("{:?}", p); + seed_result.success = true; + seed_result + .successful_attempts + .push(SeedCheckConnectAttempt { + ip_addr: r.to_string(), + handshake_success: true, + user_agent: Some(p.info.user_agent), + capabilities: Some(format!("{:?}", p.info.capabilities)), + }); + } else { + seed_result + .unsuccessful_attempts + .push(SeedCheckConnectAttempt { + ip_addr: r.to_string(), + handshake_success: false, + user_agent: None, + capabilities: None, + }); + } + } + + if !seed_result.success { + info!( + "FAIL - Unable to handshake at any known DNS resolutions for {}", + s + ); + } + + result.push(seed_result); + } + + // Clean up temporary files + fs::remove_dir_all(".__grintmp__").expect("Unable to delete temporary files"); + + result +} + +fn check_seed_health( + addr: p2p::PeerAddr, + is_testnet: bool, + peers: &Arc, +) -> Result { + let config = p2p::types::P2PConfig::default(); + let capabilities = p2p::types::Capabilities::default(); + let genesis_hash = match is_testnet { + true => genesis::genesis_test().hash(), + false => genesis::genesis_main().hash(), + }; + + let handshake = p2p::handshake::Handshake::new(genesis_hash, config.clone()); + + match TcpStream::connect_timeout(&addr.0, Duration::from_secs(5)) { + Ok(stream) => { + let addr = SocketAddr::new(config.host, config.port); + let total_diff = Difficulty::from_num(1); + + let peer = p2p::Peer::connect( + stream, + capabilities, + total_diff, + p2p::PeerAddr(addr), + &handshake, + peers.clone(), + )?; + Ok(peer) + } + Err(e) => { + trace!( + "connect_peer: on {}:{}. Could not connect to {}: {:?}", + config.host, + config.port, + addr, + e + ); + Err(p2p::Error::Connection(e).into()) + } + } +}