Files
nym/validator-api/src/main.rs
T
Bogdan-Ștefan Neacşu 10bf70b22b Feature/bandwidth token (#832)
* Remove check for bandwidth for incoming packets

We should only accunt for packets that the client inputs to the mixnet

* Introduce BandwidthController for both types of bandwidth creds

* Add some non-coconut token bandwidth handling

* Use thiserror for gateway-client lib

* Add error handling

* Unable to build for wasm for now

* Fix wasm strange error

* Disable non-coconut credentials for wasm client

* Check for status and throw the error up

* Send encrypted token cred from client

* Gateway receive message and signature validation

* Put the correct amount of tokens that were burned

* [ci skip] Generate TS types

* Eth endpoint and secret key as config parameters

* Add eth_endpoint config argument for gateway

* Update test as well

* Separate panicable code from the safe one

* Move some bandwidth controller panics up the call stack

* Save contract corresponding to the eth endpoint

* Fix template

* Pass the web3 interface as well

* Made event reads possible in gateway

* Add checks for event data

* Cosmos contract for double spending prevention

* Add workflow for the new contract

* Add validator rest URL to config

* Rename eth_events to erc20_bridge

* Pass cosmos mnemonic as well, and put the nymd client in ERC20Bridge

* Call cosmos contract for final verification

* Ask for config parameters in cli

* Fix various stuff

* Increase timeout to allow gateway to check the two chains

* Put some logs for the new flow

* Set consumed bandwidth invariantly of coconut feature

* Fix clippy error

* Add non-coconut checks

* Use 2018 rust instead of 2021

* More verbose nymd error

* Explicitly specify TOKENS_TO_BURN constant

* Put eth burn function in a constant

* Replace to_vec & append with iter & chain

* Test for (de)serialization of TokenCredential

* Minor rename

* Separate credential creation from bandwidth claiming

* Switch from panics to errors when claiming coconut bandwidth

* Another append changed to chain

* Update QA cosmos contract address

* Simplify build/test/clippy separation on coconut feature

* Fix bad features arg positioning

* Use the start_after in cosmos contract query

* Set a limit in line with a range on cosmos queries

* Added unit tests for new cosmos contract

* Fix bandwidth_remaining comparation

* Get remaining bandwidth from gateway

* Add contract build flag

* Add a useful info log

* Use a more robust eth depth for release builds

* Include recipt logs in error message

* Fix clippy for tests

* Use Arc instead of clone

* Rename as_bytes to to_bytes

* Make signature verification in contract more verbose

* Missed rename of paging constant

* Fix gateway start with coconut enabled

* Rename function to claim_token

* Simplify nymd client setup

* Check with block buffer on gateway as well

* Update comment of double spending protection

* Correct contract address

* Backup the keypairs used for buying tokens, in case of error cases

* Don't take any chances with the gateway timeout

* [ci skip] Generate TS types

* Updated cosmos contract to latest QA address

* Add cli options for eth

* Update network monitor timeout value as well

Co-authored-by: neacsu <neacsu@users.noreply.github.com>
2021-11-10 14:53:56 +02:00

520 lines
18 KiB
Rust

// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[macro_use]
extern crate rocket;
use crate::cache::ValidatorCacheRefresher;
use crate::config::Config;
use crate::network_monitor::NetworkMonitorBuilder;
use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater;
use crate::nymd_client::Client;
use crate::rewarding::epoch::Epoch;
use crate::rewarding::Rewarder;
use crate::storage::ValidatorApiStorage;
use ::config::NymConfig;
use anyhow::Result;
use cache::ValidatorCache;
use clap::{App, Arg, ArgMatches};
use log::{info, warn};
use rocket::fairing::AdHoc;
use rocket::http::Method;
use rocket::{Ignite, Rocket};
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors};
use std::process;
use std::sync::Arc;
use std::time::Duration;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use tokio::sync::Notify;
use url::Url;
use validator_client::nymd::SigningNymdClient;
#[cfg(feature = "coconut")]
use coconut::InternalSignRequest;
pub(crate) mod cache;
pub(crate) mod config;
mod network_monitor;
mod node_status_api;
pub(crate) mod nymd_client;
mod rewarding;
pub(crate) mod storage;
#[cfg(feature = "coconut")]
mod coconut;
const MONITORING_ENABLED: &str = "enable-monitor";
const REWARDING_ENABLED: &str = "enable-rewarding";
const MIXNET_CONTRACT_ARG: &str = "mixnet-contract";
const MNEMONIC_ARG: &str = "mnemonic";
const WRITE_CONFIG_ARG: &str = "save-config";
const NYMD_VALIDATOR_ARG: &str = "nymd-validator";
const API_VALIDATORS_ARG: &str = "api-validators";
#[cfg(feature = "coconut")]
const KEYPAIR_ARG: &str = "keypair";
#[cfg(feature = "coconut")]
const COCONUT_ONLY_FLAG: &str = "coconut-only";
#[cfg(not(feature = "coconut"))]
const ETH_ENDPOINT: &str = "eth_endpoint";
#[cfg(not(feature = "coconut"))]
const ETH_PRIVATE_KEY: &str = "eth_private_key";
const EPOCH_LENGTH_ARG: &str = "epoch-length";
const FIRST_REWARDING_EPOCH_ARG: &str = "first-epoch";
const REWARDING_MONITOR_THRESHOLD_ARG: &str = "monitor-threshold";
fn parse_validators(raw: &str) -> Vec<Url> {
raw.split(',')
.map(|raw_validator| {
raw_validator
.trim()
.parse()
.expect("one of the provided validator api urls is invalid")
})
.collect()
}
fn parse_args<'a>() -> ArgMatches<'a> {
#[cfg(feature = "coconut")]
let monitor_reqs = &[];
#[cfg(not(feature = "coconut"))]
let monitor_reqs = &[ETH_ENDPOINT, ETH_PRIVATE_KEY];
let base_app = App::new("Nym Validator API")
.author("Nymtech")
.arg(
Arg::with_name(MONITORING_ENABLED)
.help("specifies whether a network monitoring is enabled on this API")
.long(MONITORING_ENABLED)
.short("m")
.requires_all(monitor_reqs)
)
.arg(
Arg::with_name(REWARDING_ENABLED)
.help("specifies whether a network rewarding is enabled on this API")
.long(REWARDING_ENABLED)
.short("r")
.requires(MONITORING_ENABLED)
)
.arg(
Arg::with_name(NYMD_VALIDATOR_ARG)
.help("Endpoint to nymd part of the validator from which the monitor will grab nodes to test")
.long(NYMD_VALIDATOR_ARG)
.takes_value(true)
)
.arg(Arg::with_name(MIXNET_CONTRACT_ARG)
.long(MIXNET_CONTRACT_ARG)
.help("Address of the validator contract managing the network")
.takes_value(true),
)
.arg(Arg::with_name(MNEMONIC_ARG)
.long(MNEMONIC_ARG)
.help("Mnemonic of the network monitor used for rewarding operators")
.takes_value(true)
.requires(REWARDING_ENABLED),
)
.arg(
Arg::with_name(WRITE_CONFIG_ARG)
.help("specifies whether a config file based on provided arguments should be saved to a file")
.long(WRITE_CONFIG_ARG)
.short("w")
)
.arg(
Arg::with_name(API_VALIDATORS_ARG)
.help("specifies list of all validators on the network issuing coconut credentials. Ensure they are properly ordered")
.long(API_VALIDATORS_ARG)
.takes_value(true)
)
.arg(
Arg::with_name(FIRST_REWARDING_EPOCH_ARG)
.help("Datetime specifying beginning of the first rewarding epoch of this length. It must be a valid rfc3339 datetime.")
.takes_value(true)
.long(FIRST_REWARDING_EPOCH_ARG)
.requires(REWARDING_ENABLED)
)
.arg(
Arg::with_name(EPOCH_LENGTH_ARG)
.help("Length of the current rewarding epoch in hours")
.takes_value(true)
.long(EPOCH_LENGTH_ARG)
.requires(REWARDING_ENABLED)
)
.arg(
Arg::with_name(REWARDING_MONITOR_THRESHOLD_ARG)
.help("Specifies the minimum percentage of monitor test run data present in order to distribute rewards for given epoch.")
.takes_value(true)
.long(REWARDING_MONITOR_THRESHOLD_ARG)
.requires(REWARDING_ENABLED)
);
#[cfg(feature = "coconut")]
let base_app = base_app.arg(
Arg::with_name(KEYPAIR_ARG)
.help("Path to the secret key file")
.takes_value(true)
.long(KEYPAIR_ARG),
).arg(
Arg::with_name(COCONUT_ONLY_FLAG)
.help("Flag to indicate whether validator api should only be used for credential issuance with no blockchain connection")
.long(COCONUT_ONLY_FLAG),
);
#[cfg(not(feature = "coconut"))]
let base_app = base_app.arg(
Arg::with_name(ETH_ENDPOINT)
.help("URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens")
.takes_value(true)
.long(ETH_ENDPOINT),
).arg(
Arg::with_name(ETH_PRIVATE_KEY)
.help("Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens")
.takes_value(true)
.long(ETH_PRIVATE_KEY),
);
base_app.get_matches()
}
async fn wait_for_interrupt() {
if let Err(e) = tokio::signal::ctrl_c().await {
error!(
"There was an error while capturing SIGINT - {:?}. We will terminate regardless",
e
);
}
println!("Received SIGINT - the network monitor will terminate now");
}
fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
log_builder.parse_filters(&s);
} else {
// default to 'Info'
log_builder.filter(None, log::LevelFilter::Info);
}
log_builder
.filter_module("hyper", log::LevelFilter::Warn)
.filter_module("tokio_reactor", log::LevelFilter::Warn)
.filter_module("reqwest", log::LevelFilter::Warn)
.filter_module("mio", log::LevelFilter::Warn)
.filter_module("want", log::LevelFilter::Warn)
.filter_module("sled", log::LevelFilter::Warn)
.filter_module("tungstenite", log::LevelFilter::Warn)
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
.init();
}
fn override_config(mut config: Config, matches: &ArgMatches) -> Config {
if matches.is_present(MONITORING_ENABLED) {
config = config.with_network_monitor_enabled(true)
}
if matches.is_present(REWARDING_ENABLED) {
config = config.with_rewarding_enabled(true)
}
if let Some(raw_validators) = matches.value_of(API_VALIDATORS_ARG) {
config = config.with_custom_validator_apis(parse_validators(raw_validators));
}
if let Some(raw_validator) = matches.value_of(NYMD_VALIDATOR_ARG) {
let parsed = match raw_validator.parse() {
Err(err) => {
error!("Passed validator argument is invalid - {}", err);
process::exit(1)
}
Ok(url) => url,
};
config = config.with_custom_nymd_validator(parsed);
}
if let Some(mixnet_contract) = matches.value_of(MIXNET_CONTRACT_ARG) {
config = config.with_custom_mixnet_contract(mixnet_contract)
}
if let Some(mnemonic) = matches.value_of(MNEMONIC_ARG) {
config = config.with_mnemonic(mnemonic)
}
if let Some(rewarding_epoch_datetime) = matches.value_of(FIRST_REWARDING_EPOCH_ARG) {
let first_epoch = OffsetDateTime::parse(rewarding_epoch_datetime, &Rfc3339)
.expect("Provided first epoch is not a valid rfc3339 datetime!");
config = config.with_first_rewarding_epoch(first_epoch)
}
if let Some(epoch_length) = matches
.value_of(EPOCH_LENGTH_ARG)
.map(|len| len.parse::<u64>())
{
let epoch_length = epoch_length.expect("Provided epoch length is not a number!");
config = config.with_epoch_length(Duration::from_secs(epoch_length * 60 * 60));
}
if let Some(monitor_threshold) = matches
.value_of(REWARDING_MONITOR_THRESHOLD_ARG)
.map(|t| t.parse::<u8>())
{
let monitor_threshold =
monitor_threshold.expect("Provided monitor threshold is not a number!");
assert!(
!(monitor_threshold > 100),
"Provided monitor threshold is greater than 100!"
);
config = config.with_minimum_epoch_monitor_threshold(monitor_threshold)
}
#[cfg(feature = "coconut")]
if let Some(keypair_path) = matches.value_of(KEYPAIR_ARG) {
let keypair_bs58 = std::fs::read_to_string(keypair_path)
.unwrap()
.trim()
.to_string();
config = config.with_keypair(keypair_bs58)
}
#[cfg(not(feature = "coconut"))]
if let Some(eth_private_key) = matches.value_of("eth_private_key") {
config = config.with_eth_private_key(String::from(eth_private_key));
}
#[cfg(not(feature = "coconut"))]
if let Some(eth_endpoint) = matches.value_of("eth_endpoint") {
config = config.with_eth_endpoint(String::from(eth_endpoint));
}
if matches.is_present(WRITE_CONFIG_ARG) {
info!("Saving the configuration to a file");
if let Err(err) = config.save_to_file(None) {
error!("Failed to write config to a file - {}", err);
process::exit(1)
}
}
config
}
fn setup_cors() -> Result<Cors> {
let allowed_origins = AllowedOrigins::all();
// You can also deserialize this
let cors = rocket_cors::CorsOptions {
allowed_origins,
allowed_methods: vec![Method::Post, Method::Get]
.into_iter()
.map(From::from)
.collect(),
allowed_headers: AllowedHeaders::all(),
allow_credentials: true,
..Default::default()
}
.to_cors()?;
Ok(cors)
}
fn setup_liftoff_notify(notify: Arc<Notify>) -> AdHoc {
AdHoc::on_liftoff("Liftoff notifier", |_| {
Box::pin(async move { notify.notify_one() })
})
}
fn setup_network_monitor<'a>(
config: &'a Config,
system_version: &str,
rocket: &Rocket<Ignite>,
) -> Option<NetworkMonitorBuilder<'a>> {
if !config.get_network_monitor_enabled() {
return None;
}
// get instances of managed states
let node_status_storage = rocket.state::<ValidatorApiStorage>().unwrap().clone();
let validator_cache = rocket.state::<ValidatorCache>().unwrap().clone();
Some(NetworkMonitorBuilder::new(
config,
system_version,
node_status_storage,
validator_cache,
))
}
fn expected_monitor_test_runs(config: &Config) -> usize {
let epoch_length = config.get_epoch_length();
let test_delay = config.get_network_monitor_run_interval();
// this is just a rough estimate. In real world there will be slightly fewer test runs
// as they are not instantaneous and hence do not happen exactly every test_delay
(epoch_length.as_secs() / test_delay.as_secs()) as usize
}
fn setup_rewarder(
config: &Config,
rocket: &Rocket<Ignite>,
nymd_client: &Client<SigningNymdClient>,
) -> Option<Rewarder> {
if config.get_rewarding_enabled() && config.get_network_monitor_enabled() {
// get instances of managed states
let node_status_storage = rocket.state::<ValidatorApiStorage>().unwrap().clone();
let validator_cache = rocket.state::<ValidatorCache>().unwrap().clone();
let first_epoch = Epoch::new(
config.get_first_rewarding_epoch(),
config.get_epoch_length(),
);
Some(Rewarder::new(
nymd_client.clone(),
validator_cache,
node_status_storage,
first_epoch,
expected_monitor_test_runs(config),
config.get_minimum_epoch_monitor_threshold(),
))
} else if config.get_rewarding_enabled() {
warn!("Cannot enable rewarding with the network monitor being disabled");
None
} else {
None
}
}
async fn setup_rocket(config: &Config, liftoff_notify: Arc<Notify>) -> Result<Rocket<Ignite>> {
// let's build our rocket!
let rocket = rocket::build()
.attach(setup_cors()?)
.attach(setup_liftoff_notify(liftoff_notify))
.attach(ValidatorCache::stage());
#[cfg(feature = "coconut")]
let rocket = rocket.attach(InternalSignRequest::stage(config.keypair()));
// see if we should start up network monitor and if so, attach the node status api
if config.get_network_monitor_enabled() {
Ok(rocket
.attach(node_status_api::stage(
config.get_node_status_api_database_path(),
))
.ignite()
.await?)
} else {
Ok(rocket.ignite().await?)
}
}
#[tokio::main]
async fn main() -> Result<()> {
setup_logging();
let system_version = env!("CARGO_PKG_VERSION");
println!("Starting validator api...");
// try to load config from the file, if it doesn't exist, use default values
let config = match Config::load_from_file(None) {
Ok(cfg) => cfg,
Err(_) => {
let config_path = Config::default_config_file_path(None)
.into_os_string()
.into_string()
.unwrap();
warn!(
"Could not load the configuration file from {}. Either the file did not exist or was malformed. Using the default values instead",
config_path
);
Config::new()
}
};
let matches = parse_args();
let config = override_config(config, &matches);
// if we just wanted to write data to the config, exit
if matches.is_present(WRITE_CONFIG_ARG) {
return Ok(());
}
#[cfg(feature = "coconut")]
if matches.is_present(COCONUT_ONLY_FLAG) {
// this simplifies everything - we just want to run coconut things
return rocket::build()
.attach(setup_cors()?)
.attach(InternalSignRequest::stage(config.keypair()))
.launch()
.await
.map_err(|err| err.into());
}
let liftoff_notify = Arc::new(Notify::new());
// let's build our rocket!
let rocket = setup_rocket(&config, Arc::clone(&liftoff_notify)).await?;
let monitor_builder = setup_network_monitor(&config, system_version, &rocket);
let validator_cache = rocket.state::<ValidatorCache>().unwrap().clone();
// if network monitor is disabled, we're not going to be sending any rewarding hence
// we're not starting signing client
if config.get_network_monitor_enabled() {
let nymd_client = Client::new_signing(&config);
let validator_cache_refresher = ValidatorCacheRefresher::new(
nymd_client.clone(),
config.get_caching_interval(),
validator_cache.clone(),
);
// spawn our cacher
tokio::spawn(async move { validator_cache_refresher.run().await });
// setup our daily uptime updater. Note that if network monitor is disabled, then we have
// no data for the updates and hence we don't need to start it up
let storage = rocket.state::<ValidatorApiStorage>().unwrap().clone();
let uptime_updater = HistoricalUptimeUpdater::new(storage);
tokio::spawn(async move { uptime_updater.run().await });
if let Some(rewarder) = setup_rewarder(&config, &rocket, &nymd_client) {
info!("Periodic rewarding is starting...");
tokio::spawn(async move { rewarder.run().await });
} else {
info!("Periodic rewarding is disabled.");
}
} else {
let nymd_client = Client::new_query(&config);
let validator_cache_refresher = ValidatorCacheRefresher::new(
nymd_client,
config.get_caching_interval(),
validator_cache,
);
// spawn our cacher
tokio::spawn(async move { validator_cache_refresher.run().await });
}
// launch the rocket!
let shutdown_handle = rocket.shutdown();
tokio::spawn(rocket.launch());
// to finish building our monitor, we need to have rocket up and running so that we could
// obtain our bandwidth credential
if let Some(monitor_builder) = monitor_builder {
info!("Starting network monitor...");
// wait for rocket's liftoff stage
liftoff_notify.notified().await;
// we're ready to go! spawn the network monitor!
let runnables = monitor_builder.build().await;
runnables.spawn_tasks();
} else {
info!("Network monitoring is disabled.");
}
wait_for_interrupt().await;
shutdown_handle.notify();
Ok(())
}