From 190d3c4972f02084ef1031d17d52ca9bd77d1622 Mon Sep 17 00:00:00 2001 From: Dave Hrycyszyn Date: Wed, 19 May 2021 09:25:11 -0400 Subject: [PATCH] Feature/add node description api (#605) * Starting to build out node descriptions * Renamed the mod to node_description instead of descriptor * Returning results loaded from file * Taking user input for node describe command * Readline without new dependencies * Adding input format hint * Making sure the node can still start even when the descriptor file isn't there yet * Adding some additional runtime checks * Removing -dev from minimum node version * Removing initial :: on serde * Fixing comment on the /description endpoint * Swapped json for toml * Being a bit more specific with my startup message. * Cleaning up path building * Nicer runtime checks * Put path building inside the file methods --- Cargo.lock | 7 ++- mixnode/Cargo.toml | 1 + mixnode/src/commands/describe.rs | 65 ++++++++++++++++++++++++++++ mixnode/src/commands/mod.rs | 1 + mixnode/src/commands/run.rs | 5 ++- mixnode/src/main.rs | 4 +- mixnode/src/node/http/description.rs | 9 ++++ mixnode/src/node/http/mod.rs | 5 ++- mixnode/src/node/mod.rs | 16 +++++-- mixnode/src/node/node_description.rs | 46 ++++++++++++++++++++ 10 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 mixnode/src/commands/describe.rs create mode 100644 mixnode/src/node/http/description.rs create mode 100644 mixnode/src/node/node_description.rs diff --git a/Cargo.lock b/Cargo.lock index f760cb6aa3..8669d00b45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,8 @@ # It is not intended for manual editing. # Copyright 2020 - Nym Technologies SA # SPDX-License-Identifier: Apache-2.0 +version = 3 + [[package]] name = "aes-ctr" version = "0.3.0" @@ -1733,6 +1735,7 @@ dependencies = [ "serde", "tokio", "tokio-util", + "toml", "topology", "validator-client", "version-checker", @@ -2819,9 +2822,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.4.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" [[package]] name = "snafu" diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index 8d894ee148..6a9a348a7b 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -25,6 +25,7 @@ rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev="f442ad9 serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.4", features = [ "rt-multi-thread", "net", "signal" ] } tokio-util = { version = "0.6", features = ["codec"] } +toml = "0.5" ## internal config = { path = "../common/config" } diff --git a/mixnode/src/commands/describe.rs b/mixnode/src/commands/describe.rs new file mode 100644 index 0000000000..4887ae832b --- /dev/null +++ b/mixnode/src/commands/describe.rs @@ -0,0 +1,65 @@ +use crate::config::Config; +use crate::node::node_description::NodeDescription; +use clap::{App, Arg, ArgMatches}; +use colored::Colorize; +use config::NymConfig; +use std::io; +use std::io::Write; + +pub fn command_args<'a, 'b>() -> App<'a, 'b> { + App::new("describe") + .about("Describe your mixnode and tell people why they should delegate stake to you") + .arg( + Arg::with_name("id") + .long("id") + .help("The id of the mixnode you want to describe") + .takes_value(true) + .required(true), + ) +} + +pub fn execute(matches: &ArgMatches) { + // figure out which node the user is describing + let id = matches + .value_of("id") + .expect("Please provide the id of your mixnode"); + + // ensure that the mixnode has in fact been initialized + match Config::load_from_file(id) { + Ok(cfg) => cfg, + Err(err) => { + error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err); + return; + } + }; + + // get input from the user + print!("name: "); + io::stdout().flush().unwrap(); + let mut name_buf = String::new(); + io::stdin().read_line(&mut name_buf).unwrap(); + let name = name_buf.trim().to_string(); + + print!("description: "); + io::stdout().flush().unwrap(); + let mut desc_buf = String::new(); + io::stdin().read_line(&mut desc_buf).unwrap(); + let description = desc_buf.trim().to_string(); + + let example_url = "https://mixnode.yourdomain.com".bright_cyan(); + + print!("link, e.g. {}: ", example_url); + io::stdout().flush().unwrap(); + let mut link_buf = String::new(); + io::stdin().read_line(&mut link_buf).unwrap(); + let link = link_buf.trim().to_string(); + + let node_description = NodeDescription { + name, + description, + link, + }; + + // save the struct + NodeDescription::save_to_file(&node_description, Config::default_config_directory(id)).unwrap() +} diff --git a/mixnode/src/commands/mod.rs b/mixnode/src/commands/mod.rs index 83415938ff..6c452f4b8f 100644 --- a/mixnode/src/commands/mod.rs +++ b/mixnode/src/commands/mod.rs @@ -5,6 +5,7 @@ use crate::config::Config; use clap::ArgMatches; use nymsphinx::params::DEFAULT_NUM_MIX_HOPS; +pub(crate) mod describe; pub(crate) mod init; pub(crate) mod run; pub(crate) mod sign; diff --git a/mixnode/src/commands/run.rs b/mixnode/src/commands/run.rs index 144546abd8..2d05632d51 100644 --- a/mixnode/src/commands/run.rs +++ b/mixnode/src/commands/run.rs @@ -3,6 +3,7 @@ use crate::commands::override_config; use crate::config::{persistence::pathfinder::MixNodePathfinder, Config}; +use crate::node::node_description::NodeDescription; use crate::node::MixNode; use clap::{App, Arg, ArgMatches}; use config::NymConfig; @@ -183,5 +184,7 @@ pub fn execute(matches: &ArgMatches) { config.get_version(), ); - MixNode::new(config, identity_keypair, sphinx_keypair).run(); + let description = + NodeDescription::load_from_file(Config::default_config_directory(id)).unwrap_or_default(); + MixNode::new(config, description, identity_keypair, sphinx_keypair).run(); } diff --git a/mixnode/src/main.rs b/mixnode/src/main.rs index 1f3df39610..a0ad7774ec 100644 --- a/mixnode/src/main.rs +++ b/mixnode/src/main.rs @@ -18,7 +18,8 @@ fn main() { let arg_matches = App::new("Nym Mixnode") .version(env!("CARGO_PKG_VERSION")) .author("Nymtech") - .about("Implementation of the Loopix-based Mixnode") + .about("Implementation of a Loopix-based Mixnode") + .subcommand(commands::describe::command_args()) .subcommand(commands::init::command_args()) .subcommand(commands::run::command_args()) .subcommand(commands::upgrade::command_args()) @@ -30,6 +31,7 @@ fn main() { fn execute(matches: ArgMatches) { match matches.subcommand() { + ("describe", Some(m)) => commands::describe::execute(m), ("init", Some(m)) => commands::init::execute(m), ("run", Some(m)) => commands::run::execute(m), ("sign", Some(m)) => commands::sign::execute(m), diff --git a/mixnode/src/node/http/description.rs b/mixnode/src/node/http/description.rs new file mode 100644 index 0000000000..cf1fd72951 --- /dev/null +++ b/mixnode/src/node/http/description.rs @@ -0,0 +1,9 @@ +use crate::node::node_description::NodeDescription; +use rocket::State; +use rocket_contrib::json::Json; + +/// Returns a description of the node and why someone might want to delegate stake to it. +#[get("/description")] +pub(crate) fn description(description: State) -> Json { + Json(description.inner().clone()) +} diff --git a/mixnode/src/node/http/mod.rs b/mixnode/src/node/http/mod.rs index d5052fa399..13c134b8d1 100644 --- a/mixnode/src/node/http/mod.rs +++ b/mixnode/src/node/http/mod.rs @@ -1,7 +1,8 @@ -use rocket::Request; - +pub(crate) mod description; pub(crate) mod verloc; +use rocket::Request; + #[catch(404)] pub(crate) fn not_found(req: &Request) -> String { format!("I couldn't find '{}'. Try something else?", req.uri()) diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 01f1cc9320..3a1d4d15f5 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -3,12 +3,14 @@ use crate::config::Config; use crate::node::http::{ + description::description, not_found, verloc::{verloc, VerlocState}, }; use crate::node::listener::connection_handler::packet_processing::PacketProcessor; use crate::node::listener::connection_handler::ConnectionHandler; use crate::node::listener::Listener; +use crate::node::node_description::NodeDescription; use crate::node::packet_delayforwarder::{DelayForwarder, PacketDelayForwardSender}; use crypto::asymmetric::{encryption, identity}; use log::{error, info, warn}; @@ -21,11 +23,13 @@ use version_checker::parse_version; pub(crate) mod http; mod listener; mod metrics; +pub(crate) mod node_description; pub(crate) mod packet_delayforwarder; // the MixNode will live for whole duration of this program pub struct MixNode { config: Config, + descriptor: NodeDescription, identity_keypair: Arc, sphinx_keypair: Arc, } @@ -33,31 +37,35 @@ pub struct MixNode { impl MixNode { pub fn new( config: Config, + descriptor: NodeDescription, identity_keypair: identity::KeyPair, sphinx_keypair: encryption::KeyPair, ) -> Self { MixNode { config, + descriptor, identity_keypair: Arc::new(identity_keypair), sphinx_keypair: Arc::new(sphinx_keypair), } } fn start_http_api(&self, atomic_verloc_result: AtomicVerlocResult) { - info!("Starting HTTP API on port 8000..."); + info!("Starting HTTP API on http://localhost:8000"); let mut config = rocket::config::Config::release_default(); // bind to the same address as we are using for mixnodes config.address = self.config.get_listening_address().ip(); - let state = VerlocState::new(atomic_verloc_result); + let verloc_state = VerlocState::new(atomic_verloc_result); + let descriptor = self.descriptor.clone(); tokio::spawn(async move { rocket::build() .configure(config) - .mount("/", routes![verloc]) + .mount("/", routes![verloc, description]) .register("/", catchers![not_found]) - .manage(state) + .manage(verloc_state) + .manage(descriptor) .launch() .await }); diff --git a/mixnode/src/node/node_description.rs b/mixnode/src/node/node_description.rs new file mode 100644 index 0000000000..fdc6f1072f --- /dev/null +++ b/mixnode/src/node/node_description.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; +use serde::Serialize; +use std::path::PathBuf; +use std::{fs, io}; + +pub(crate) const DESCRIPTION_FILE: &str = "description.toml"; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct NodeDescription { + pub(crate) name: String, + pub(crate) description: String, + pub(crate) link: String, +} + +impl Default for NodeDescription { + fn default() -> Self { + NodeDescription { + name: "This node has not yet set a name".to_string(), + description: "This node has not yet set a description".to_string(), + link: "https://nymtech.net".to_string(), + } + } +} + +impl NodeDescription { + pub(crate) fn load_from_file(config_path: PathBuf) -> io::Result { + let description_file_path: PathBuf = [config_path.to_str().unwrap(), DESCRIPTION_FILE] + .iter() + .collect(); + let toml = fs::read_to_string(description_file_path)?; + toml::from_str(&toml).map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + } + + pub(crate) fn save_to_file( + description: &NodeDescription, + config_path: PathBuf, + ) -> io::Result<()> { + let description_file_path: PathBuf = [config_path.to_str().unwrap(), DESCRIPTION_FILE] + .iter() + .collect(); + let description_toml = + toml::to_string(description).expect("could not encode description to toml"); + fs::write(description_file_path, description_toml)?; + Ok(()) + } +}