diff --git a/Cargo.lock b/Cargo.lock index ae25a8c0b0..e46ab8de56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6687,7 +6687,6 @@ dependencies = [ "nym-types", "nym-validator-client", "opentelemetry", - "pretty_env_logger", "rand 0.7.3", "serde", "serde_json", @@ -7492,6 +7491,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-validator-rewarder" +version = "0.1.0" +dependencies = [ + "anyhow", + "bip39", + "clap 4.4.7", + "nym-bin-common", + "nym-config", + "nym-network-defaults", + "nym-task", + "nym-validator-client", + "serde", + "tendermint-rpc", + "thiserror", + "tokio", + "tracing", + "url", + "zeroize", +] + [[package]] name = "nym-vesting-contract-common" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index bbd1f04c64..d12f2cd6d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,7 @@ members = [ "nym-node", "nym-node/nym-node-requests", "nym-outfox", + "nym-validator-rewarder", "tools/internal/ssl-inject", "tools/internal/sdk-version-bump", "tools/nym-cli", @@ -124,6 +125,7 @@ default-members = [ "nym-api", "tools/nymvisor", "explorer-api", + "nym-validator-rewarder", ] exclude = ["explorer", "contracts", "nym-wallet", "nym-connect/mobile/src-tauri", "nym-connect/desktop", "nym-vpn/ui/src-tauri", "cpu-cycles"] diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index 25a6a8d5a7..9ad7c5081f 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -18,7 +18,7 @@ rust-version = "1.58.1" [dependencies] axum = { workspace = true } -anyhow = "1.0.40" +anyhow = { workspace = true } bs58 = "0.4.0" clap = { workspace = true, features = ["cargo", "derive"] } colored = "2.0" @@ -28,7 +28,6 @@ futures = { workspace = true } humantime-serde = "1.0" lazy_static = "1.4.0" log = { workspace = true } -pretty_env_logger = "0.4.0" rand = "0.7.3" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -41,7 +40,7 @@ cfg-if = "1.0.0" thiserror = { workspace = true } ## tracing -tracing = { version = "0.1.37", optional = true } +tracing = { workspace = true, optional = true } opentelemetry = { version = "0.19.0", optional = true } @@ -65,7 +64,7 @@ nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } cpu-cycles = { path = "../cpu-cycles", optional = true } [dev-dependencies] -tokio = { version = "1.21.2", features = [ +tokio = { workspace = true, features = [ "rt-multi-thread", "net", "signal", diff --git a/nym-validator-rewarder/Cargo.toml b/nym-validator-rewarder/Cargo.toml new file mode 100644 index 0000000000..b380e053ba --- /dev/null +++ b/nym-validator-rewarder/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "nym-validator-rewarder" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license = "GPL-3" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow.workspace = true +bip39 = { workspace = true, features = ["zeroize"] } +clap = { workspace = true, features = ["cargo"] } +serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros"] } +tracing.workspace = true +url.workspace = true +zeroize.workspace = true +tendermint-rpc = { workspace = true, features = ["websocket-client"] } + +# internal +nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } +nym-config = { path = "../common/config" } +nym-network-defaults = { path = "../common/network-defaults" } +nym-task = { path = "../common/task" } +nym-validator-client = { path = "../common/client-libs/validator-client" } diff --git a/nym-validator-rewarder/src/cli/build_info.rs b/nym-validator-rewarder/src/cli/build_info.rs new file mode 100644 index 0000000000..b5328f039c --- /dev/null +++ b/nym-validator-rewarder/src/cli/build_info.rs @@ -0,0 +1,17 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymRewarderError; +use nym_bin_common::bin_info_owned; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) fn execute(args: Args) -> Result<(), NymRewarderError> { + println!("{}", args.output.format(&bin_info_owned!())); + Ok(()) +} diff --git a/nym-validator-rewarder/src/cli/init.rs b/nym-validator-rewarder/src/cli/init.rs new file mode 100644 index 0000000000..c55b6a6806 --- /dev/null +++ b/nym-validator-rewarder/src/cli/init.rs @@ -0,0 +1,47 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::Config; +use crate::error::NymRewarderError; +use std::path::PathBuf; + +#[derive(Debug, clap::Args)] +pub struct Args { + #[command(flatten)] + config_override: ConfigOverridableArgs, + + /// Specifies custom location for the configuration file of nym validators rewarder. + #[clap(long)] + custom_config_path: Option, + + /// Mnemonic used for rewarding operations + #[clap(long)] + mnemonic: bip39::Mnemonic, + + /// Overwrite existing configuration file. + #[clap(long, short)] + force: bool, +} + +#[derive(Debug, clap::Args)] +pub struct ConfigOverridableArgs { + // +} + +pub(crate) fn execute(args: Args) -> Result<(), NymRewarderError> { + let path = args + .custom_config_path + .clone() + .unwrap_or(Config::default_location()); + + if path.exists() && !args.force { + return Err(NymRewarderError::ExistingConfig { path }); + } + + Config::new(args.mnemonic) + .with_override(args.config_override) + .save_to_path(&path) + .map_err(|source| NymRewarderError::ConfigSaveFailure { path, source })?; + + Ok(()) +} diff --git a/nym-validator-rewarder/src/cli/mod.rs b/nym-validator-rewarder/src/cli/mod.rs new file mode 100644 index 0000000000..ba64c80e7f --- /dev/null +++ b/nym-validator-rewarder/src/cli/mod.rs @@ -0,0 +1,91 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::Config; +use crate::error::NymRewarderError; +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::path::PathBuf; +use std::sync::OnceLock; +use tracing::{debug, error}; + +pub mod build_info; +pub mod init; +pub mod run; +pub mod upgrade_helpers; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + /// Path pointing to an env file that configures the validator rewarder and overrides any preconfigured values. + #[clap(short, long)] + pub(crate) config_env_file: Option, + + /// Flag used for disabling the printed banner in tty. + #[clap(long)] + pub(crate) no_banner: bool, + + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + pub(crate) async fn execute(self) -> Result<(), NymRewarderError> { + match self.command { + Commands::Init(args) => init::execute(args), + Commands::Run(args) => run::execute(args).await, + Commands::BuildInfo(args) => build_info::execute(args), + } + } +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Initialise a validator rewarder with persistent config.toml file. + Init(init::Args), + + /// Run the validator rewarder with the preconfigured settings. + Run(run::Args), + + /// Show build information of this binary + BuildInfo(build_info::Args), +} + +fn try_load_current_config(custom_path: &Option) -> Result { + let config_path = custom_path.clone().unwrap_or(Config::default_location()); + + debug!( + "attempting to load configuration file from {}", + config_path.display() + ); + + if let Ok(cfg) = Config::read_from_toml_file(&config_path) { + return Ok(cfg); + } + + upgrade_helpers::try_upgrade_config(&config_path)?; + + Config::read_from_toml_file(&config_path).map_err(|err| { + error!( + "Failed to load config. Are you sure you have run `init` before? (Error was: {err})", + ); + err + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/nym-validator-rewarder/src/cli/run.rs b/nym-validator-rewarder/src/cli/run.rs new file mode 100644 index 0000000000..717edff440 --- /dev/null +++ b/nym-validator-rewarder/src/cli/run.rs @@ -0,0 +1,19 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::try_load_current_config; +use crate::error::NymRewarderError; +use crate::rewarder::Rewarder; +use std::path::PathBuf; + +#[derive(Debug, clap::Args)] +pub struct Args { + /// Specifies custom location for the configuration file of nym validators rewarder. + custom_config_path: Option, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NymRewarderError> { + let config = try_load_current_config(&args.custom_config_path)?.with_override(args); + + Rewarder::new(config).run().await +} diff --git a/nym-validator-rewarder/src/cli/upgrade_helpers/mod.rs b/nym-validator-rewarder/src/cli/upgrade_helpers/mod.rs new file mode 100644 index 0000000000..d1592df69a --- /dev/null +++ b/nym-validator-rewarder/src/cli/upgrade_helpers/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymRewarderError; +use std::path::Path; + +pub(crate) fn try_upgrade_config>(config_path: P) -> Result<(), NymRewarderError> { + let _ = config_path; + Ok(()) +} diff --git a/nym-validator-rewarder/src/config/mod.rs b/nym-validator-rewarder/src/config/mod.rs new file mode 100644 index 0000000000..dd79dce7a8 --- /dev/null +++ b/nym-validator-rewarder/src/config/mod.rs @@ -0,0 +1,166 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::r#override::ConfigOverride; +use crate::config::template::CONFIG_TEMPLATE; +use crate::error::NymRewarderError; +use nym_config::{ + must_get_home, read_config_from_toml_file, save_formatted_config_to_file, NymConfigTemplate, + DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, +}; +use nym_network_defaults::NymNetworkDetails; +use serde::{Deserialize, Serialize}; +use std::io; +use std::path::{Path, PathBuf}; +use tracing::{debug, warn}; +use url::Url; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +pub mod r#override; +pub mod persistence; +mod template; + +const DEFAULT_REWARDER_DIR: &str = "validators-rewarder"; + +/// Get default path to rewarder's config directory. +/// It should get resolved to `$HOME/.nym/validators-rewarder/config` +pub fn default_config_directory() -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_REWARDER_DIR) + .join(DEFAULT_CONFIG_DIR) +} + +/// Get default path to rewarder's config file. +/// It should get resolved to `$HOME/.nym/validators-rewarder/config/config.toml` +pub fn default_config_filepath() -> PathBuf { + default_config_directory().join(DEFAULT_CONFIG_FILENAME) +} + +/// Get default path to rewarder's data directory where files, such as keys, are stored. +/// It should get resolved to `$HOME/.nym/validators-rewarder/data` +pub fn default_data_directory() -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_REWARDER_DIR) + .join(DEFAULT_DATA_DIR) +} + +#[derive(Debug, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)] +pub struct Config { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + #[zeroize(skip)] + pub(crate) save_path: Option, + + pub distribution: RewardingRatios, + + #[serde(flatten)] + pub base: Base, +} + +impl NymConfigTemplate for Config { + fn template(&self) -> &'static str { + CONFIG_TEMPLATE + } +} + +impl Config { + pub fn new(mnemonic: bip39::Mnemonic) -> Self { + let network = NymNetworkDetails::new_from_env(); + + Config { + save_path: None, + distribution: RewardingRatios::default(), + base: Base { + upstream_nyxd: network.endpoints[0].nyxd_url(), + mnemonic, + }, + } + } + + pub fn ensure_is_valid(&self) -> Result<(), NymRewarderError> { + self.distribution.ensure_is_valid()?; + Ok(()) + } + + pub fn r#override(&mut self, r#override: O) { + r#override.override_config(self) + } + + pub fn with_override(mut self, r#override: O) -> Self { + self.r#override(r#override); + self + } + + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> Result { + let path = path.as_ref(); + let mut loaded: Config = read_config_from_toml_file(path).map_err(|source| { + NymRewarderError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + } + })?; + loaded.ensure_is_valid()?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } + + pub fn read_from_toml_file>(path: P) -> Result { + Self::read_from_path(path) + } + + pub fn default_location() -> PathBuf { + default_config_filepath() + } + + pub fn save_to_default_location(&self) -> io::Result<()> { + let config_save_location: PathBuf = Self::default_location(); + save_formatted_config_to_file(self, config_save_location) + } + + pub fn save_to_path>(&self, path: P) -> io::Result<()> { + save_formatted_config_to_file(self, path) + } +} + +#[derive(Debug, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)] +pub struct Base { + /// Url to the upstream instance of nyxd to use for any queries. + #[zeroize(skip)] + pub upstream_nyxd: Url, + + /// Mnemonic to the nyx account distributing the rewards + pub(crate) mnemonic: bip39::Mnemonic, +} + +#[derive(Debug, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)] +pub struct RewardingRatios { + /// The percent of the epoch reward being awarded for block signing. + pub block_signing: f32, + + /// The percent of the epoch reward being awarded for credential issuance. + pub credential_issuance: f32, + + /// The percent of the epoch reward given to Nym. + pub nym: f32, +} + +impl Default for RewardingRatios { + fn default() -> Self { + RewardingRatios { + // TODO: define proper defaults + block_signing: 67.0, + credential_issuance: 33.0, + nym: 0.0, + } + } +} + +impl RewardingRatios { + pub fn ensure_is_valid(&self) -> Result<(), NymRewarderError> { + Ok(()) + } +} diff --git a/nym-validator-rewarder/src/config/override.rs b/nym-validator-rewarder/src/config/override.rs new file mode 100644 index 0000000000..4c1089b3c3 --- /dev/null +++ b/nym-validator-rewarder/src/config/override.rs @@ -0,0 +1,17 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::{init, run}; +use crate::config::Config; + +pub trait ConfigOverride { + fn override_config(self, config: &mut Config); +} + +impl ConfigOverride for init::ConfigOverridableArgs { + fn override_config(self, config: &mut Config) {} +} + +impl ConfigOverride for run::Args { + fn override_config(self, config: &mut Config) {} +} diff --git a/nym-validator-rewarder/src/config/persistence/mod.rs b/nym-validator-rewarder/src/config/persistence/mod.rs new file mode 100644 index 0000000000..af282c5deb --- /dev/null +++ b/nym-validator-rewarder/src/config/persistence/mod.rs @@ -0,0 +1,2 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-validator-rewarder/src/config/template.rs b/nym-validator-rewarder/src/config/template.rs new file mode 100644 index 0000000000..3bb141e2d8 --- /dev/null +++ b/nym-validator-rewarder/src/config/template.rs @@ -0,0 +1,14 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +// While using normal toml marshalling would have been way simpler with less overhead, +// I think it's useful to have comments attached to the saved config file to explain behaviour of +// particular fields. +// Note: any changes to the template must be reflected in the appropriate structs. +pub(crate) const CONFIG_TEMPLATE: &str = r#" +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + + + +"#; diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs new file mode 100644 index 0000000000..0e68ca6852 --- /dev/null +++ b/nym-validator-rewarder/src/error.rs @@ -0,0 +1,38 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_validator_client::nyxd::error::NyxdError; +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NymRewarderError { + #[error( + "failed to load config file using path '{}'. detailed message: {source}", path.display() + )] + ConfigLoadFailure { + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error( + "failed to save config file using path '{}'. detailed message: {source}", path.display() + )] + ConfigSaveFailure { + path: PathBuf, + #[source] + source: io::Error, + }, + + #[error("there already exists a config file at: {}. if you want to overwrite its content, use --force flag", path.display())] + ExistingConfig { path: PathBuf }, + + // TODO: I think this one should get split into more, explicit, variants + #[error(transparent)] + NyxdFailure(#[from] NyxdError), + + #[error("the provided rewarding ratios don't add up to 1. ratios: {ratios:?}")] + InvalidRewardingRatios { ratios: Vec }, +} diff --git a/nym-validator-rewarder/src/main.rs b/nym-validator-rewarder/src/main.rs new file mode 100644 index 0000000000..5baeace799 --- /dev/null +++ b/nym-validator-rewarder/src/main.rs @@ -0,0 +1,26 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::Cli; +use clap::{crate_name, crate_version, Parser}; +use nym_bin_common::logging::maybe_print_banner; +use nym_network_defaults::setup_env; + +pub mod cli; +pub mod config; +pub mod error; +mod rewarder; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let args = Cli::parse(); + setup_env(args.config_env_file.as_ref()); + + if !args.no_banner { + maybe_print_banner(crate_name!(), crate_version!()); + } + + args.execute().await?; + + Ok(()) +} diff --git a/nym-validator-rewarder/src/rewarder/mod.rs b/nym-validator-rewarder/src/rewarder/mod.rs new file mode 100644 index 0000000000..0c54f8a835 --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/mod.rs @@ -0,0 +1,98 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::Config; +use crate::error::NymRewarderError; +use nym_network_defaults::NymNetworkDetails; +use nym_task::TaskManager; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; +use tendermint_rpc::WebSocketClient; +use tracing::info; + +mod tasks; + +pub struct Rewarder { + config: Config, +} + +impl Rewarder { + pub fn new(config: Config) -> Self { + Rewarder { config } + } + + pub async fn run(mut self) -> Result<(), NymRewarderError> { + info!("Starting nym validators rewarder"); + + // setup shutdowns + let task_manager = TaskManager::new(5); + + // + // + + // let client_config = + // nyxd::Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?; + // + // let client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + // client_config, + // self.config.base.upstream_nyxd.as_str(), + // // note: the clone here is fine as the mnemonic itself implements ZeroizeOnDrop + // self.config.base.mnemonic.clone(), + // )?; + + let (client, driver) = WebSocketClient::new("https://rpc.nymtech.net/") + .await + .unwrap(); + /* + /// #[tokio::main] + /// async fn main() { + /// let (client, driver) = WebSocketClient::new("ws://127.0.0.1:26657/websocket") + /// .await + /// .unwrap(); + /// let driver_handle = tokio::spawn(async move { driver.run().await }); + /// + /// // Standard client functionality + /// let tx = format!("some-key=some-value"); + /// client.broadcast_tx_async(Transaction::from(tx.into_bytes())).await.unwrap(); + /// + /// // Subscription functionality + /// let mut subs = client.subscribe(EventType::NewBlock.into()) + /// .await + /// .unwrap(); + /// + /// // Grab 5 NewBlock events + /// let mut ev_count = 5_i32; + /// + /// while let Some(res) = subs.next().await { + /// let ev = res.unwrap(); + /// println!("Got event: {:?}", ev); + /// ev_count -= 1; + /// if ev_count < 0 { + /// break; + /// } + /// } + /// + /// // Signal to the driver to terminate. + /// client.close().unwrap(); + /// // Await the driver's termination to ensure proper connection closure. + /// let _ = driver_handle.await.unwrap(); + /// } + /// ``` + */ + + /* + task 1: + on timer: + - go to DKG contract + - get all coconut signers + - for each of them get the info, verify, etc + + task 2: + on timer (or maybe per block?): + - query abci endpoint for VP + - also maybe missed blocks, etc + + */ + + todo!() + } +} diff --git a/nym-validator-rewarder/src/rewarder/tasks/mod.rs b/nym-validator-rewarder/src/rewarder/tasks/mod.rs new file mode 100644 index 0000000000..af282c5deb --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/tasks/mod.rs @@ -0,0 +1,2 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only