diff --git a/Cargo.lock b/Cargo.lock index b1ea2db16a..f6b7be01c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1649,6 +1649,16 @@ dependencies = [ "tendermint-proto", ] +[[package]] +name = "cosmos-sdk-proto" +version = "0.20.0" +source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#41ed4631e146268b0300033c8bbb993d79a49f58" +dependencies = [ + "prost 0.12.1", + "prost-types 0.12.1", + "tendermint-proto", +] + [[package]] name = "cosmrs" version = "0.15.0" @@ -1656,7 +1666,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" dependencies = [ "bip32", - "cosmos-sdk-proto", + "cosmos-sdk-proto 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ecdsa 0.16.8", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature 2.1.0", + "subtle-encoding", + "tendermint", + "thiserror", +] + +[[package]] +name = "cosmrs" +version = "0.15.0" +source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#41ed4631e146268b0300033c8bbb993d79a49f58" +dependencies = [ + "bip32", + "cosmos-sdk-proto 0.20.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "ecdsa 0.16.8", "eyre", "k256", @@ -2212,6 +2241,16 @@ dependencies = [ "darling_macro 0.14.4", ] +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core 0.20.3", + "darling_macro 0.20.3", +] + [[package]] name = "darling_core" version = "0.13.4" @@ -2240,6 +2279,20 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.38", +] + [[package]] name = "darling_macro" version = "0.13.4" @@ -2262,6 +2315,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core 0.20.3", + "quote", + "syn 2.0.38", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -5945,7 +6009,7 @@ name = "nym-api-requests" version = "0.1.0" dependencies = [ "bs58 0.4.0", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "cosmwasm-std", "getset", "nym-coconut-interface", @@ -6003,7 +6067,7 @@ name = "nym-bity-integration" version = "0.1.0" dependencies = [ "anyhow", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "eyre", "k256", "nym-cli-commands", @@ -6048,7 +6112,7 @@ dependencies = [ "cfg-if", "clap 4.4.7", "comfy-table", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "cosmwasm-std", "cw-utils", "handlebars", @@ -6317,7 +6381,7 @@ name = "nym-credentials" version = "0.1.0" dependencies = [ "bls12_381", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "log", "nym-api-requests", "nym-coconut-interface", @@ -6676,7 +6740,6 @@ dependencies = [ "nym-types", "nym-validator-client", "opentelemetry", - "pretty_env_logger", "rand 0.7.3", "serde", "serde_json", @@ -7405,7 +7468,7 @@ name = "nym-types" version = "1.0.0" dependencies = [ "base64 0.21.4", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "cosmwasm-std", "eyre", "hmac 0.12.1", @@ -7439,7 +7502,7 @@ dependencies = [ "bip32", "bip39", "colored", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "cosmwasm-std", "cw-controllers", "cw-utils", @@ -7481,6 +7544,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-validator-rewarder" +version = "0.1.0" +dependencies = [ + "anyhow", + "bip39", + "clap 4.4.7", + "cosmwasm-std", + "futures", + "humantime 2.1.0", + "humantime-serde", + "nym-bin-common", + "nym-coconut", + "nym-coconut-bandwidth-contract-common", + "nym-coconut-dkg-common", + "nym-config", + "nym-credentials", + "nym-crypto", + "nym-network-defaults", + "nym-task", + "nym-validator-client", + "nyxd-scraper", + "serde", + "serde_with", + "sha2 0.10.8", + "sqlx", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + [[package]] name = "nym-vesting-contract-common" version = "0.7.0" @@ -7499,7 +7596,7 @@ dependencies = [ name = "nym-wallet-types" version = "1.0.0" dependencies = [ - "cosmrs", + "cosmrs 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "cosmwasm-std", "hex-literal", "nym-config", @@ -7583,7 +7680,7 @@ version = "0.1.0" dependencies = [ "async-trait", "const_format", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "eyre", "futures", "nym-bin-common", @@ -9807,6 +9904,35 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64 0.21.4", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.2", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling 0.20.3", + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "serde_yaml" version = "0.9.25" diff --git a/Cargo.toml b/Cargo.toml index bbd1f04c64..f319ff744f 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"] @@ -201,7 +203,10 @@ cw-controllers = { version = "=1.1.0" } # cosmrs-related bip32 = "0.5.1" -cosmrs = "=0.15.0" + +# temporarily using a fork again (yay.) because we need staking and slashing support +cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch ="nym-temp/all-validator-features" } +#cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch = "nym-temp/all-validator-features" } # unfortuntely we need a fork by yours truly to get the staking support tendermint = "0.34" # same version as used by cosmrs tendermint-rpc = "0.34" # same version as used by cosmrs prost = "0.12" diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index a04b8f05d5..f28e45bd37 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -42,6 +42,14 @@ pub struct Config { nyxd_config: nyxd::Config, } +impl TryFrom for Config { + type Error = ValidatorClientError; + + fn try_from(value: NymNetworkDetails) -> Result { + Config::try_from_nym_network_details(&value) + } +} + impl Config { pub fn try_from_nym_network_details( details: &NymNetworkDetails, diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index d4902bd1a1..ced2e4caff 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -5,21 +5,24 @@ use crate::nym_api::error::NymAPIError; use crate::nym_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; use async_trait::async_trait; use http_api_client::{ApiClient, NO_PARAMS}; -use nym_api_requests::coconut::models::{ - EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, +pub use nym_api_requests::{ + coconut::{ + models::{ + EpochCredentialsResponse, IssuedCredential, IssuedCredentialBody, + IssuedCredentialResponse, IssuedCredentialsResponse, + }, + BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, + VerifyCredentialBody, VerifyCredentialResponse, + }, + models::{ + ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, + GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, + MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, + MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, + StakeSaturationResponse, UptimeResponse, + }, }; -use nym_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, VerifyCredentialBody, - VerifyCredentialResponse, -}; -use nym_api_requests::models::{ - ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, - GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, - MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, - MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, - StakeSaturationResponse, UptimeResponse, -}; -use nym_coconut_dkg_common::types::EpochId; +pub use nym_coconut_dkg_common::types::EpochId; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; use nym_name_service_common::response::NamesListResponse; diff --git a/common/client-libs/validator-client/src/nyxd/coin.rs b/common/client-libs/validator-client/src/nyxd/coin.rs index f780bd32f3..0fa175a243 100644 --- a/common/client-libs/validator-client/src/nyxd/coin.rs +++ b/common/client-libs/validator-client/src/nyxd/coin.rs @@ -8,6 +8,8 @@ use cosmwasm_std::{Fraction, Uint128}; use serde::{Deserialize, Serialize}; use std::fmt; use std::ops::Div; +use std::str::FromStr; +use thiserror::Error; #[derive(Serialize, Deserialize, Clone, Copy, Default, Debug, PartialEq, Eq)] pub struct MismatchedDenoms; @@ -126,6 +128,37 @@ impl From for Coin { } } +// unfortunately cosmwasm didn't re-export this correct so we just redefine its +#[derive(Error, Debug, PartialEq, Eq)] +pub enum CoinFromStrError { + #[error("Missing denominator")] + MissingDenom, + #[error("Missing amount or non-digit characters in amount")] + MissingAmount, + #[error("Invalid amount: {0}")] + InvalidAmount(#[from] std::num::ParseIntError), +} + +impl FromStr for Coin { + type Err = CoinFromStrError; + + fn from_str(s: &str) -> Result { + let pos = s + .find(|c: char| !c.is_ascii_digit()) + .ok_or(CoinFromStrError::MissingDenom)?; + let (amount, denom) = s.split_at(pos); + + if amount.is_empty() { + return Err(CoinFromStrError::MissingAmount); + } + + Ok(Coin { + amount: amount.parse::()?, + denom: denom.to_string(), + }) + } +} + pub trait CoinConverter { type Target; diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index d3ac4807e8..46712cb81b 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -7,12 +7,12 @@ use crate::nyxd::error::NyxdError; use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; -use nym_coconut_dkg_common::dealer::{ - ContractDealing, DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse, +use nym_coconut_dkg_common::{ + dealer::{ContractDealing, DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse}, + msg::QueryMsg as DkgQueryMsg, + types::{DealerDetails, Epoch, EpochId, InitialReplacementData}, + verification_key::{ContractVKShare, PagedVKSharesResponse}, }; -use nym_coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; -use nym_coconut_dkg_common::types::{DealerDetails, Epoch, EpochId, InitialReplacementData}; -use nym_coconut_dkg_common::verification_key::{ContractVKShare, PagedVKSharesResponse}; use serde::Deserialize; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index a6ed3afddc..7d389adafb 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -52,10 +52,6 @@ use wasmtimer::tokio::sleep; pub const DEFAULT_BROADCAST_POLLING_RATE: Duration = Duration::from_secs(4); pub const DEFAULT_BROADCAST_TIMEOUT: Duration = Duration::from_secs(60); -#[cfg(feature = "http-client")] -#[async_trait] -impl CosmWasmClient for cosmrs::rpc::HttpClient {} - #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait CosmWasmClient: TendermintRpcClient { @@ -522,3 +518,7 @@ pub trait CosmWasmClient: TendermintRpcClient { res.try_into() } } + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl CosmWasmClient for T where T: TendermintRpcClient {} diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index 5b3deadb68..d5baa96499 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -425,7 +425,7 @@ where amount: amount.into_iter().map(Into::into).collect(), } .to_any() - .map_err(|_| NyxdError::SerializationError("MsgExecuteContract".to_owned())) + .map_err(|_| NyxdError::SerializationError("MsgSend".to_owned())) }) .collect::>()?; diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs index ab78a8c418..98f2f8d07c 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::nyxd::cosmwasm_client::client_traits::{CosmWasmClient, SigningCosmWasmClient}; +use crate::nyxd::cosmwasm_client::client_traits::SigningCosmWasmClient; use crate::nyxd::error::NyxdError; use crate::nyxd::{Config, GasPrice, Hash, Height}; use crate::rpc::TendermintRpcClient; @@ -26,6 +26,7 @@ use cosmrs::rpc::{HttpClient, HttpClientUrl}; pub mod client_traits; mod helpers; pub mod logs; +pub mod module_traits; pub mod types; #[derive(Debug)] @@ -329,14 +330,6 @@ where } } -#[async_trait] -impl CosmWasmClient for MaybeSigningClient -where - C: TendermintRpcClient + Send + Sync, - S: Send + Sync, -{ -} - #[async_trait] impl SigningCosmWasmClient for MaybeSigningClient where diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/mod.rs new file mode 100644 index 0000000000..e27ad09f98 --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod slashing; +pub mod staking; + +pub use staking::query::StakingQueryClient; +// pub use slashing::query diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/slashing/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/slashing/mod.rs new file mode 100644 index 0000000000..de0f6e779a --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/slashing/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod query; diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/slashing/query.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/slashing/query.rs new file mode 100644 index 0000000000..38dc36ebc3 --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/slashing/query.rs @@ -0,0 +1,2 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/staking/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/staking/mod.rs new file mode 100644 index 0000000000..a0fddf08e9 --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/staking/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod query; + +pub use cosmrs::staking::{ + QueryHistoricalInfoResponse, QueryValidatorResponse, QueryValidatorsResponse, Validator, +}; diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/staking/query.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/staking/query.rs new file mode 100644 index 0000000000..1ae27138ff --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/module_traits/staking/query.rs @@ -0,0 +1,78 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{QueryHistoricalInfoResponse, QueryValidatorResponse, QueryValidatorsResponse}; +use crate::nyxd::error::NyxdError; +use crate::nyxd::{CosmWasmClient, PageRequest}; +use async_trait::async_trait; +use cosmrs::proto::cosmos::staking::v1beta1::{ + QueryHistoricalInfoRequest as ProtoQueryHistoricalInfoRequest, + QueryHistoricalInfoResponse as ProtoQueryHistoricalInfoResponse, + QueryValidatorRequest as ProtoQueryValidatorRequest, + QueryValidatorResponse as ProtoQueryValidatorResponse, + QueryValidatorsRequest as ProtoQueryValidatorsRequest, + QueryValidatorsResponse as ProtoQueryValidatorsResponse, +}; +use cosmrs::staking::{QueryHistoricalInfoRequest, QueryValidatorRequest, QueryValidatorsRequest}; +use cosmrs::AccountId; + +// TODO: change trait restriction from `CosmWasmClient` to `TendermintRpcClient` +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait StakingQueryClient: CosmWasmClient { + async fn historical_info(&self, height: i64) -> Result { + let path = Some("/cosmos.staking.v1beta1.Query/HistoricalInfo".to_owned()); + + let req = QueryHistoricalInfoRequest { height }; + + let res = self + .make_abci_query::( + path, + req.into(), + ) + .await?; + + Ok(res.try_into()?) + } + + async fn validator( + &self, + validator_addr: AccountId, + ) -> Result { + let path = Some("/cosmos.staking.v1beta1.Query/Validator".to_owned()); + + let req = QueryValidatorRequest { validator_addr }; + + let res = self + .make_abci_query::( + path, + req.into(), + ) + .await?; + + Ok(res.try_into()?) + } + + async fn validators( + &self, + status: String, + pagination: Option, + ) -> Result { + let path = Some("/cosmos.staking.v1beta1.Query/Validators".to_owned()); + + let req = QueryValidatorsRequest { status, pagination }; + + let res = self + .make_abci_query::( + path, + req.into(), + ) + .await?; + + Ok(res.try_into()?) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl StakingQueryClient for T where T: CosmWasmClient {} diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 46f9b0622a..da8910a46f 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -29,24 +29,30 @@ use tendermint_rpc::endpoint::*; use tendermint_rpc::{Error as TendermintRpcError, Order}; use url::Url; -pub use crate::nyxd::cosmwasm_client::client_traits::{CosmWasmClient, SigningCosmWasmClient}; -pub use crate::nyxd::fee::Fee; +pub use crate::nyxd::{ + cosmwasm_client::{ + client_traits::{CosmWasmClient, SigningCosmWasmClient}, + module_traits::{self, StakingQueryClient}, + }, + fee::Fee, +}; pub use crate::rpc::TendermintRpcClient; pub use coin::Coin; -pub use cosmrs::bank::MsgSend; -pub use cosmrs::tendermint::abci::{ - response::DeliverTx, types::ExecTxResult, Event, EventAttribute, +pub use cosmrs::{ + bank::MsgSend, + bip32, + crypto::PublicKey, + query::{PageRequest, PageResponse}, + tendermint::{ + abci::{response::DeliverTx, types::ExecTxResult, Event, EventAttribute}, + block::Height, + hash::{self, Algorithm, Hash}, + validator::Info as TendermintValidatorInfo, + Time as TendermintTime, + }, + tx::{self, Msg}, + AccountId, Any, Coin as CosmosCoin, Denom, Gas, }; -pub use cosmrs::tendermint::block::Height; -pub use cosmrs::tendermint::hash::{self, Algorithm, Hash}; -pub use cosmrs::tendermint::validator::Info as TendermintValidatorInfo; -pub use cosmrs::tendermint::Time as TendermintTime; -pub use cosmrs::tx::Msg; -pub use cosmrs::tx::{self}; -pub use cosmrs::Any; -pub use cosmrs::Coin as CosmosCoin; -pub use cosmrs::Gas; -pub use cosmrs::{bip32, AccountId, Denom}; pub use cosmwasm_std::Coin as CosmWasmCoin; pub use cw2; pub use cw3; @@ -56,9 +62,8 @@ pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment}; pub use tendermint_rpc::{ endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse}, query::Query, - Paging, + Paging, Request, Response, SimpleRequest, }; -pub use tendermint_rpc::{Request, Response, SimpleRequest}; #[cfg(feature = "http-client")] use crate::http_client; @@ -98,6 +103,14 @@ impl Config { } } +impl TryFrom for Config { + type Error = NyxdError; + + fn try_from(value: NymNetworkDetails) -> Result { + Config::try_from_nym_network_details(&value) + } +} + #[derive(Debug)] pub struct NyxdClient { client: MaybeSigningClient, @@ -729,7 +742,7 @@ where where H: Into + Send, { - self.client.validators(height, paging).await + TendermintRpcClient::validators(&self.client, height, paging).await } async fn latest_consensus_params( @@ -804,14 +817,6 @@ where } } -#[async_trait] -impl CosmWasmClient for NyxdClient -where - C: TendermintRpcClient + Send + Sync, - S: Send + Sync, -{ -} - impl OfflineSigner for NyxdClient where S: OfflineSigner, diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index bcf02d1f02..1d2af785c1 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -111,6 +111,7 @@ impl NymNetworkDetails { .with_additional_validator_endpoint(ValidatorDetails::new( var(var_names::NYXD).expect("nyxd validator not set"), Some(var(var_names::NYM_API).expect("nym api not set")), + get_optional_env(var_names::NYXD_WEBSOCKET), )) .with_mixnet_contract(Some( var(var_names::MIXNET_CONTRACT_ADDRESS).expect("mixnet contract not set"), @@ -340,6 +341,9 @@ impl DenomDetailsOwned { pub struct ValidatorDetails { // it is assumed those values are always valid since they're being provided in our defaults file pub nyxd_url: String, + // + pub websocket_url: Option, + // Right now api_url is optional as we are not running the api reliably on all validators // however, later on it should be a mandatory field pub api_url: Option, @@ -347,9 +351,10 @@ pub struct ValidatorDetails { } impl ValidatorDetails { - pub fn new>(nyxd_url: S, api_url: Option) -> Self { + pub fn new>(nyxd_url: S, api_url: Option, websocket_url: Option) -> Self { ValidatorDetails { nyxd_url: nyxd_url.into(), + websocket_url: websocket_url.map(Into::into), api_url: api_url.map(Into::into), } } @@ -357,6 +362,7 @@ impl ValidatorDetails { pub fn new_nyxd_only>(nyxd_url: S) -> Self { ValidatorDetails { nyxd_url: nyxd_url.into(), + websocket_url: None, api_url: None, } } @@ -372,6 +378,12 @@ impl ValidatorDetails { .as_ref() .map(|url| url.parse().expect("the provided api url is invalid!")) } + + pub fn websocket_url(&self) -> Option { + self.websocket_url + .as_ref() + .map(|url| url.parse().expect("the provided websocket url is invalid!")) + } } fn fix_deprecated_environmental_variables() { diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 13e8f21024..4cd9d0f53c 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -26,6 +26,7 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772 pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/"; pub const NYXD_URL: &str = "https://rpc.nymtech.net"; pub const NYM_API: &str = "https://validator.nymtech.net/api/"; +pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket"; pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/"; // I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging @@ -33,7 +34,11 @@ pub const EXIT_POLICY_URL: &str = "https://nymtech.net/.wellknown/network-requester/exit-policy.txt"; pub(crate) fn validators() -> Vec { - vec![ValidatorDetails::new(NYXD_URL, Some(NYM_API))] + vec![ValidatorDetails::new( + NYXD_URL, + Some(NYM_API), + Some(NYXD_WS), + )] } const DEFAULT_SUFFIX: &str = "_MAINNET_DEFAULT"; @@ -111,6 +116,7 @@ pub fn export_to_env() { ); set_var_to_default(var_names::NYXD, NYXD_URL); set_var_to_default(var_names::NYM_API, NYM_API); + set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS); set_var_to_default(var_names::EXPLORER_API, EXPLORER_API); set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); } @@ -159,6 +165,7 @@ pub fn export_to_env_if_not_set() { ); set_var_conditionally_to_default(var_names::NYXD, NYXD_URL); set_var_conditionally_to_default(var_names::NYM_API, NYM_API); + set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS); set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API); set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); } diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index c74117630e..edda2686d1 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -26,6 +26,7 @@ pub const SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS: &str = pub const NAME_SERVICE_CONTRACT_ADDRESS: &str = "NAME_SERVICE_CONTRACT_ADDRESS"; pub const NYXD: &str = "NYXD"; pub const NYM_API: &str = "NYM_API"; +pub const NYXD_WEBSOCKET: &str = "NYXD_WS"; pub const EXPLORER_API: &str = "EXPLORER_API"; pub const EXIT_POLICY_URL: &str = "EXIT_POLICY"; diff --git a/common/nymcoconut/benches/benchmarks.rs b/common/nymcoconut/benches/benchmarks.rs index 6bf582a244..966e676f42 100644 --- a/common/nymcoconut/benches/benchmarks.rs +++ b/common/nymcoconut/benches/benchmarks.rs @@ -255,7 +255,7 @@ fn bench_coconut(c: &mut Criterion) { b.iter(|| { verify_partial_blind_signature( ¶ms, - &blind_sign_request, + blind_sign_request.get_private_attributes_pedersen_commitments(), &public_attributes, random_blind_signature, partial_verification_key, diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index 4f0db30a15..7f1631f801 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -43,3 +43,5 @@ mod utils; pub type Attribute = bls12_381::Scalar; pub type PrivateAttribute = Attribute; pub type PublicAttribute = Attribute; + +pub use bls12_381::G1Projective; diff --git a/common/nymcoconut/src/scheme/issuance.rs b/common/nymcoconut/src/scheme/issuance.rs index 11d0dcb35b..1126e24d98 100644 --- a/common/nymcoconut/src/scheme/issuance.rs +++ b/common/nymcoconut/src/scheme/issuance.rs @@ -362,12 +362,12 @@ pub fn blind_sign( /// The function returns `true` if the partial blind signature is valid, and `false` otherwise. pub fn verify_partial_blind_signature( params: &Parameters, - blind_sign_request: &BlindSignRequest, + private_attribute_commitments: &[G1Projective], public_attributes: &[&Attribute], blind_sig: &BlindedSignature, partial_verification_key: &VerificationKey, ) -> bool { - let num_private_attributes = blind_sign_request.private_attributes_commitments.len(); + let num_private_attributes = private_attribute_commitments.len(); if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { return false; } @@ -388,8 +388,7 @@ pub fn verify_partial_blind_signature( ]; // for each private attribute, add (cm_i, beta_i) to the miller terms - for (private_attr_commit, beta_g2) in blind_sign_request - .private_attributes_commitments + for (private_attr_commit, beta_g2) in private_attribute_commitments .iter() .zip(&partial_verification_key.beta_g2) { @@ -422,7 +421,7 @@ pub fn verify_partial_blind_signature( // is equivalent to checking e(a, b) • e(c, d)^{-1} == id // and thus to e(a, b) • e(c^{-1}, d) == id // - // compute e(c^1, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j}) + // compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j}) multi_miller_loop(&terms_refs) .final_exponentiation() .is_identity() @@ -519,7 +518,7 @@ mod tests { assert!(verify_partial_blind_signature( ¶ms, - &request, + &request.private_attributes_commitments, &public_attributes, &blind_sig, validator_keypair.verification_key() @@ -539,7 +538,7 @@ mod tests { assert!(verify_partial_blind_signature( ¶ms, - &request, + &request.private_attributes_commitments, &[], &blind_sig, validator_keypair.verification_key() @@ -568,7 +567,7 @@ mod tests { // this assertion should fail, as we try to verify with a wrong validator key assert!(!verify_partial_blind_signature( ¶ms, - &request, + &request.private_attributes_commitments, &public_attributes, &blind_sig, validator2_keypair.verification_key() diff --git a/common/nyxd-scraper/src/block_processor/mod.rs b/common/nyxd-scraper/src/block_processor/mod.rs index bb3e886b0e..d60be91e77 100644 --- a/common/nyxd-scraper/src/block_processor/mod.rs +++ b/common/nyxd-scraper/src/block_processor/mod.rs @@ -11,8 +11,10 @@ use crate::storage::{persist_block, ScraperStorage}; use futures::StreamExt; use std::collections::{BTreeMap, HashSet, VecDeque}; use std::ops::{Add, Range}; +use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::{Sender, UnboundedReceiver}; +use tokio::sync::Notify; use tokio::time::{interval_at, Instant}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_util::sync::CancellationToken; @@ -39,6 +41,7 @@ impl PendingSync { pub struct BlockProcessor { cancel: CancellationToken, + synced: Arc, last_processed_height: u32, last_processed_at: Instant, pending_sync: PendingSync, @@ -60,6 +63,7 @@ pub struct BlockProcessor { impl BlockProcessor { pub async fn new( cancel: CancellationToken, + synced: Arc, incoming: UnboundedReceiver, block_requester: Sender, storage: ScraperStorage, @@ -69,6 +73,7 @@ impl BlockProcessor { Ok(BlockProcessor { cancel, + synced, last_processed_height: last_processed.try_into().unwrap_or_default(), last_processed_at: Instant::now(), pending_sync: Default::default(), @@ -243,6 +248,10 @@ impl BlockProcessor { } self.try_request_pending().await; + + if self.pending_sync.is_empty() { + self.synced.notify_one(); + } } async fn try_request_pending(&mut self) -> bool { @@ -294,7 +303,7 @@ impl BlockProcessor { error!("failed to perform startup sync: {err}"); self.cancel.cancel(); return; - } + }; loop { tokio::select! { diff --git a/common/nyxd-scraper/src/lib.rs b/common/nyxd-scraper/src/lib.rs index 79372e0e30..af084c4471 100644 --- a/common/nyxd-scraper/src/lib.rs +++ b/common/nyxd-scraper/src/lib.rs @@ -16,3 +16,4 @@ pub mod storage; pub use modules::{BlockModule, MsgModule, TxModule}; pub use scraper::{Config, NyxdScraper}; +pub use storage::models; diff --git a/common/nyxd-scraper/src/scraper/mod.rs b/common/nyxd-scraper/src/scraper/mod.rs index 656cd6c921..f417318227 100644 --- a/common/nyxd-scraper/src/scraper/mod.rs +++ b/common/nyxd-scraper/src/scraper/mod.rs @@ -9,8 +9,10 @@ use crate::rpc_client::RpcClient; use crate::scraper::subscriber::{run_websocket_driver, ChainSubscriber}; use crate::storage::ScraperStorage; use std::path::PathBuf; +use std::sync::Arc; use tendermint_rpc::WebSocketClientDriver; use tokio::sync::mpsc::{channel, unbounded_channel}; +use tokio::sync::Notify; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; use tracing::info; @@ -54,6 +56,7 @@ impl NyxdScraperBuilder { ); let mut block_processor = BlockProcessor::new( scraper.cancel_token.clone(), + scraper.startup_sync.clone(), processing_rx, req_tx, scraper.storage.clone(), @@ -112,7 +115,8 @@ pub struct NyxdScraper { task_tracker: TaskTracker, cancel_token: CancellationToken, - storage: ScraperStorage, + startup_sync: Arc, + pub storage: ScraperStorage, } impl NyxdScraper { @@ -127,6 +131,7 @@ impl NyxdScraper { config, task_tracker: TaskTracker::new(), cancel_token: CancellationToken::new(), + startup_sync: Arc::new(Default::default()), storage, }) } @@ -164,6 +169,7 @@ impl NyxdScraper { ); let block_processor = BlockProcessor::new( self.cancel_token.clone(), + self.startup_sync.clone(), processing_rx, req_tx, self.storage.clone(), @@ -189,8 +195,15 @@ impl NyxdScraper { Ok(()) } + pub async fn wait_for_startup_sync(&self) { + info!("awaiting startup chain sync"); + self.startup_sync.notified().await + } + pub async fn stop(self) { info!("stopping the chain scrapper"); + assert!(self.task_tracker.is_closed()); + self.cancel_token.cancel(); self.task_tracker.wait().await } diff --git a/common/nyxd-scraper/src/storage/manager.rs b/common/nyxd-scraper/src/storage/manager.rs index 67ea50d610..1843634714 100644 --- a/common/nyxd-scraper/src/storage/manager.rs +++ b/common/nyxd-scraper/src/storage/manager.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::storage::models::Validator; +use crate::storage::models::{CommitSignature, Validator}; use sqlx::types::time::OffsetDateTime; use sqlx::{Executor, Sqlite}; use tracing::{instrument, trace}; @@ -84,8 +84,8 @@ impl StorageManager { SELECT COUNT(*) as count FROM pre_commit WHERE validator_address == ? - AND height > ? - AND height < ? + AND height >= ? + AND height <= ? "#, consensus_address, start_height, @@ -98,6 +98,24 @@ impl StorageManager { Ok(count) } + pub(crate) async fn get_precommit( + &self, + consensus_address: &str, + height: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT * FROM pre_commit + WHERE validator_address = ? + AND height = ? + "#, + ) + .bind(consensus_address) + .bind(height) + .fetch_optional(&self.connection_pool) + .await + } + pub(crate) async fn get_block_validators( &self, height: i64, @@ -118,6 +136,12 @@ impl StorageManager { .await } + pub(crate) async fn get_validators(&self) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM validator") + .fetch_all(&self.connection_pool) + .await + } + pub(crate) async fn get_last_processed_height(&self) -> Result { let maybe_record = sqlx::query!( r#" diff --git a/common/nyxd-scraper/src/storage/mod.rs b/common/nyxd-scraper/src/storage/mod.rs index 1889752a97..564f74d352 100644 --- a/common/nyxd-scraper/src/storage/mod.rs +++ b/common/nyxd-scraper/src/storage/mod.rs @@ -7,7 +7,7 @@ use crate::storage::manager::{ insert_block, insert_message, insert_precommit, insert_transaction, insert_validator, update_last_processed, StorageManager, }; -use crate::storage::models::Validator; +use crate::storage::models::{CommitSignature, Validator}; use sqlx::types::time::OffsetDateTime; use sqlx::{ConnectOptions, Sqlite, Transaction}; use std::fmt::Debug; @@ -19,7 +19,7 @@ use tracing::{debug, error, info, instrument, trace, warn}; mod helpers; mod manager; -mod models; +pub mod models; pub type StorageTransaction = Transaction<'static, Sqlite>; @@ -31,8 +31,6 @@ pub struct ScraperStorage { impl ScraperStorage { #[instrument] pub async fn init + Debug>(database_path: P) -> Result { - // TODO: we can inject here more stuff based on our nym-api global config - // struct. Maybe different pool size or timeout intervals? let mut opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) .create_if_missing(true); @@ -91,6 +89,21 @@ impl ScraperStorage { Ok(self.manager.get_last_block_height_before(time).await?) } + pub async fn get_blocks_between( + &self, + start_time: OffsetDateTime, + end_time: OffsetDateTime, + ) -> Result { + let Some(block_start) = self.get_first_block_height_after(start_time).await? else { + return Ok(0); + }; + let Some(block_end) = self.get_last_block_height_before(end_time).await? else { + return Ok(0); + }; + + Ok(block_end - block_start) + } + pub async fn get_signed_between( &self, consensus_address: &str, @@ -120,10 +133,25 @@ impl ScraperStorage { .await } + pub async fn get_precommit( + &self, + consensus_address: &str, + height: i64, + ) -> Result, ScraperError> { + Ok(self + .manager + .get_precommit(consensus_address, height) + .await?) + } + pub async fn get_block_signers(&self, height: i64) -> Result, ScraperError> { Ok(self.manager.get_block_validators(height).await?) } + pub async fn get_all_known_validators(&self) -> Result, ScraperError> { + Ok(self.manager.get_validators().await?) + } + pub async fn get_last_processed_height(&self) -> Result { Ok(self.manager.get_last_processed_height().await?) } diff --git a/common/nyxd-scraper/src/storage/models.rs b/common/nyxd-scraper/src/storage/models.rs index 38006bfdaf..74a3dd9b1c 100644 --- a/common/nyxd-scraper/src/storage/models.rs +++ b/common/nyxd-scraper/src/storage/models.rs @@ -4,7 +4,7 @@ use sqlx::types::time::OffsetDateTime; use sqlx::FromRow; -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, FromRow)] pub struct Validator { pub consensus_address: String, pub consensus_pubkey: String, diff --git a/common/task/src/manager.rs b/common/task/src/manager.rs index 35b4df3bf8..6a913fdc47 100644 --- a/common/task/src/manager.rs +++ b/common/task/src/manager.rs @@ -116,8 +116,8 @@ impl TaskManager { } #[cfg(not(target_arch = "wasm32"))] - pub async fn catch_interrupt(mut self) -> Result<(), SentError> { - let res = crate::wait_for_signal_and_error(&mut self).await; + pub async fn catch_interrupt(&mut self) -> Result<(), SentError> { + let res = crate::wait_for_signal_and_error(self).await; log::info!("Sending shutdown"); self.signal_shutdown().ok(); @@ -629,7 +629,7 @@ impl TaskHandle { #[cfg(not(target_arch = "wasm32"))] pub async fn wait_for_shutdown(self) -> Result<(), SentError> { match self { - TaskHandle::Internal(task_manager) => task_manager.catch_interrupt().await, + TaskHandle::Internal(mut task_manager) => task_manager.catch_interrupt().await, TaskHandle::External(mut task_client) => { task_client.recv().await; Ok(()) diff --git a/envs/mainnet.env b/envs/mainnet.env index 9dc5b5272d..018b4f2e38 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -22,6 +22,7 @@ REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYXD="https://rpc.nymtech.net" NYM_API="https://validator.nymtech.net/api/" +NYXD_WS="wss://rpc.nymtech.net/websocket" EXPLORER_API="https://explorer.nymtech.net/api/" DKG_TIME_CONFIGURATION="259200,300,300,60,60,1209600" diff --git a/envs/sandbox.env b/envs/sandbox.env index 856550cca0..5349798f41 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -24,4 +24,5 @@ SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1ps5yutd7sufwg058qd7ac7ldnlazsvmhzq STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" EXPLORER_API=https://sandbox-explorer.nymtech.net/api NYXD="https://rpc.sandbox.nymtech.net" +NYXD_WS="wss://rpc.sandbox.nymtech.net/websocket" NYM_API="https://sandbox-nym-api1.nymtech.net/api" diff --git a/ephemera/src/cli/run_node.rs b/ephemera/src/cli/run_node.rs index a33854f52d..adbc36ec41 100644 --- a/ephemera/src/cli/run_node.rs +++ b/ephemera/src/cli/run_node.rs @@ -60,7 +60,7 @@ impl RunExternalNodeCmd { .with_members_provider(members_provider)? .build(); - let shutdown = TaskManager::new(10); + let mut shutdown = TaskManager::new(10); let shutdown_listener = shutdown.subscribe(); tokio::spawn(ephemera.run(shutdown_listener)); diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index 1c00c1548f..8cb98280af 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -79,7 +79,7 @@ impl ExplorerApi { self.wait_for_interrupt(shutdown).await } - async fn wait_for_interrupt(&self, shutdown: TaskManager) { + async fn wait_for_interrupt(&self, mut shutdown: TaskManager) { let _res = shutdown.catch_interrupt().await; log::info!("Stopping explorer API"); } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 0a5fa00b05..7c147670ac 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -391,7 +391,9 @@ impl Gateway { )) } - async fn wait_for_interrupt(shutdown: TaskManager) -> Result<(), Box> { + async fn wait_for_interrupt( + mut shutdown: TaskManager, + ) -> Result<(), Box> { let res = shutdown.catch_interrupt().await; log::info!("Stopping nym gateway"); res 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/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 5dca2113dc..1667b108fd 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -215,7 +215,7 @@ impl MixNode { }) } - async fn wait_for_interrupt(&self, shutdown: TaskManager) { + async fn wait_for_interrupt(&self, mut shutdown: TaskManager) { let _res = shutdown.catch_interrupt().await; log::info!("Stopping nym mixnode"); } diff --git a/nym-api/nym-api-requests/src/coconut/models.rs b/nym-api/nym-api-requests/src/coconut/models.rs index 79db872288..aa6e82ef00 100644 --- a/nym-api/nym-api-requests/src/coconut/models.rs +++ b/nym-api/nym-api-requests/src/coconut/models.rs @@ -187,18 +187,18 @@ pub struct EpochCredentialsResponse { #[serde(rename_all = "camelCase")] pub struct IssuedCredentialsResponse { // note: BTreeMap returns ordered results so it's fine to use it with pagination - pub credentials: BTreeMap, + pub credentials: BTreeMap, } #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct IssuedCredentialResponse { - pub credential: Option, + pub credential: Option, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] -pub struct IssuedCredentialInner { +pub struct IssuedCredentialBody { pub credential: IssuedCredential, pub signature: identity::Signature, diff --git a/nym-api/src/coconut/api_routes/helpers.rs b/nym-api/src/coconut/api_routes/helpers.rs index 6589499767..93446d0d15 100644 --- a/nym-api/src/coconut/api_routes/helpers.rs +++ b/nym-api/src/coconut/api_routes/helpers.rs @@ -3,7 +3,7 @@ use crate::coconut::error::Result; use crate::coconut::storage::models::IssuedCredential; -use nym_api_requests::coconut::models::IssuedCredentialInner; +use nym_api_requests::coconut::models::IssuedCredentialBody; use nym_api_requests::coconut::models::IssuedCredentialsResponse; use std::collections::BTreeMap; @@ -14,7 +14,7 @@ pub(crate) fn build_credentials_response( for raw_credential in raw { let id = raw_credential.id; - let api_issued = IssuedCredentialInner::try_from(raw_credential)?; + let api_issued = IssuedCredentialBody::try_from(raw_credential)?; let old = credentials.insert(id, api_issued); if old.is_some() { // why do we panic here rather than return an error? because it's a critical failure because diff --git a/nym-api/src/coconut/storage/models.rs b/nym-api/src/coconut/storage/models.rs index fe1afbac22..4d2935c057 100644 --- a/nym-api/src/coconut/storage/models.rs +++ b/nym-api/src/coconut/storage/models.rs @@ -4,7 +4,7 @@ use crate::coconut::error::CoconutError; use nym_api_requests::coconut::models::{ EpochCredentialsResponse, IssuedCredential as ApiIssuedCredential, - IssuedCredentialInner as ApiIssuedCredentialInner, + IssuedCredentialBody as ApiIssuedCredentialInner, }; use nym_api_requests::coconut::BlindedSignatureResponse; use nym_coconut::{Base58, BlindedSignature}; @@ -97,13 +97,6 @@ impl TryFrom for BlindedSignature { } } -impl IssuedCredential { - // safety: this should only ever be called on sanitized data from the database, - // thus the unwraps are fine (if somebody manually entered their db file and modified it, it's on them) - // pub fn private_attribute_commitments(&self) -> Vec - // pub fn public_attributes(&self) -} - pub fn join_attributes(attrs: I) -> String where I: IntoIterator, diff --git a/nym-api/src/coconut/tests/mod.rs b/nym-api/src/coconut/tests/mod.rs index 446a85eab6..6c7f70ad41 100644 --- a/nym-api/src/coconut/tests/mod.rs +++ b/nym-api/src/coconut/tests/mod.rs @@ -9,7 +9,7 @@ use async_trait::async_trait; use cosmwasm_std::{coin, to_binary, Addr, CosmosMsg, Decimal, WasmMsg}; use cw3::ProposalResponse; use cw4::MemberResponse; -use nym_api_requests::coconut::models::{IssuedCredentialInner, IssuedCredentialResponse}; +use nym_api_requests::coconut::models::{IssuedCredentialBody, IssuedCredentialResponse}; use nym_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; @@ -716,7 +716,7 @@ impl TestFixture { serde_json::from_str(&response.into_string().await.unwrap()).unwrap() } - async fn issued_unchecked(&self, id: i64) -> IssuedCredentialInner { + async fn issued_unchecked(&self, id: i64) -> IssuedCredentialBody { self.issued_credential(id) .await .unwrap() diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 549438d095..b796044cd1 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -65,7 +65,7 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { config.validate()?; - let shutdown_handlers = start_nym_api_tasks(config).await?; + let mut shutdown_handlers = start_nym_api_tasks(config).await?; let res = shutdown_handlers .task_manager_handle diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 3e0ffc3ee2..6de58c0993 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -1013,8 +1013,7 @@ dependencies = [ [[package]] name = "cosmos-sdk-proto" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" +source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#67718aa27a96efb9881e927a8f998c94b885093c" dependencies = [ "prost", "prost-types", @@ -1024,8 +1023,7 @@ dependencies = [ [[package]] name = "cosmrs" version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" +source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#67718aa27a96efb9881e927a8f998c94b885093c" dependencies = [ "bip32", "cosmos-sdk-proto", diff --git a/nym-validator-rewarder/Cargo.toml b/nym-validator-rewarder/Cargo.toml new file mode 100644 index 0000000000..7b7396babd --- /dev/null +++ b/nym-validator-rewarder/Cargo.toml @@ -0,0 +1,48 @@ +[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"] } +cosmwasm-std.workspace = true +clap = { workspace = true, features = ["cargo"] } +futures.workspace = true +serde = { workspace = true, features = ["derive"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +thiserror.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros"] } +tracing.workspace = true +time.workspace = true +url.workspace = true +zeroize.workspace = true + +serde_with = "3.4.0" +sha2 = "0.10.8" +humantime = "2.1.0" +humantime-serde = "1.0" + +# internal +nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } +nym-config = { path = "../common/config" } +nym-coconut = { path = "../common/nymcoconut" } +nym-crypto = { path = "../common/crypto", features = ["asymmetric"] } +nym-credentials = { path = "../common/credentials" } +nym-network-defaults = { path = "../common/network-defaults" } +nym-task = { path = "../common/task" } +nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg"} +nym-coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract"} +nyxd-scraper = { path = "../common/nyxd-scraper" } + +[build-dependencies] +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/nym-validator-rewarder/build.rs b/nym-validator-rewarder/build.rs new file mode 100644 index 0000000000..6bd3636b5e --- /dev/null +++ b/nym-validator-rewarder/build.rs @@ -0,0 +1,28 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#[tokio::main] +async fn main() { + use sqlx::{Connection, SqliteConnection}; + use std::env; + + let out_dir = env::var("OUT_DIR").unwrap(); + let database_path = format!("{out_dir}/scraper-example.sqlite"); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) + .await + .expect("Failed to create SQLx database connection"); + + sqlx::migrate!("./migrations") + .run(&mut conn) + .await + .expect("Failed to perform SQLx migrations"); + + #[cfg(target_family = "unix")] + println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); +} diff --git a/nym-validator-rewarder/migrations/01_initial.sql b/nym-validator-rewarder/migrations/01_initial.sql new file mode 100644 index 0000000000..b7b7b9f22f --- /dev/null +++ b/nym-validator-rewarder/migrations/01_initial.sql @@ -0,0 +1,71 @@ +/* + * Copyright 2023 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE rewarding_epoch +( + id INTEGER NOT NULL PRIMARY KEY, + start_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, + budget TEXT NOT NULL, + spent TEXT NOT NULL, + rewarding_tx TEXT, + rewarding_error TEXT +); + +CREATE TABLE epoch_block_signing +( + rewarding_epoch_id INTEGER NOT NULL PRIMARY KEY REFERENCES rewarding_epoch (id), + total_voting_power_at_epoch_start INTEGER NOT NULL, + num_blocks INTEGER NOT NULL, + budget TEXT NOT NULL +); + +CREATE TABLE block_signing_reward +( + rewarding_epoch_id INTEGER NOT NULL REFERENCES rewarding_epoch (id), + validator_consensus_address TEXT NOT NULL, + operator_account TEXT NOT NULL, + amount TEXT NOT NULL, + voting_power BIGINT NOT NULL, + voting_power_share TEXT NOT NULL, + signed_blocks INTEGER NOT NULL, + signed_blocks_percent TEXT NOT NULL, + + UNIQUE (rewarding_epoch_id, operator_account) +); + +CREATE TABLE epoch_credential_issuance +( + rewarding_epoch_id INTEGER NOT NULL PRIMARY KEY REFERENCES rewarding_epoch (id), + starting_dkg_epoch INTEGER NOT NULL, + ending_dkg_epoch INTEGER NOT NULL, + total_issued_partial_credentials INTEGER NOT NULL, + budget TEXT NOT NULL +); + +CREATE TABLE malformed_credential +( + rewarding_epoch_id INTEGER NOT NULL REFERENCES rewarding_epoch (id) + +); + +CREATE TABLE credential_issuance_reward +( + rewarding_epoch_id INTEGER NOT NULL REFERENCES rewarding_epoch (id), + operator_account TEXT NOT NULL, + amount TEXT NOT NULL, + api_endpoint TEXT NOT NULL, + issued_partial_credentials INTEGER NOT NULL, + issued_credentials_share TEXT NOT NULL, + validated_issued_credentials INTEGER NOT NULL, + + UNIQUE (rewarding_epoch_id, operator_account) +); + +-- CREATE TABLE credential_verification_reward +-- ( +-- +-- ); + 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..071bc0af97 --- /dev/null +++ b/nym-validator-rewarder/src/cli/init.rs @@ -0,0 +1,60 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::ConfigOverridableArgs; +use crate::config::{default_config_directory, default_data_directory, Config}; +use crate::error::NymRewarderError; +use nym_network_defaults::NymNetworkDetails; +use std::path::PathBuf; +use std::{fs, io}; + +#[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, +} + +fn init_paths() -> io::Result<()> { + fs::create_dir_all(default_data_directory())?; + fs::create_dir_all(default_config_directory()) +} + +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 }); + } + + init_paths().map_err(|source| NymRewarderError::PathInitialisationFailure { source })?; + + let network = NymNetworkDetails::new_from_env(); + let nyxd = network.endpoints[0].nyxd_url(); + let Some(websocket) = network.endpoints[0].websocket_url() else { + return Err(NymRewarderError::UnavailableWebsocketUrl); + }; + + let config = Config::new(args.mnemonic, websocket, nyxd).with_override(args.config_override); + config.ensure_is_valid()?; + + config + .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..7fe1dc8117 --- /dev/null +++ b/nym-validator-rewarder/src/cli/mod.rs @@ -0,0 +1,136 @@ +// 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 humantime_serde::re::humantime; +use nym_bin_common::bin_info; +use nym_validator_client::nyxd::Coin; +use std::path::PathBuf; +use std::sync::OnceLock; +use tracing::{debug, error}; +use url::Url; + +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(Debug, clap::Args)] +pub struct ConfigOverridableArgs { + #[clap(long)] + pub disable_block_signing_rewarding: bool, + + #[clap(long)] + pub disable_credential_issuance_rewarding: bool, + + #[clap(long)] + pub credential_monitor_run_interval: Option, + + #[clap(long)] + pub credential_monitor_min_validation: Option, + + #[clap(long)] + pub credential_monitor_sampling_rate: Option, + + #[clap(long)] + pub scraper_endpoint: Option, + + #[clap(long)] + pub nyxd_endpoint: Option, + + #[clap(long)] + pub epoch_budget: Option, + + #[clap(long)] + pub epoch_duration: Option, + + #[clap(long)] + pub block_signing_reward_ratio: Option, + + #[clap(long)] + pub credential_issuance_reward_ratio: Option, + + #[clap(long)] + pub credential_verification_reward_ratio: Option, +} + +#[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) { + cfg.ensure_is_valid()?; + return Ok(cfg); + } + + upgrade_helpers::try_upgrade_config(&config_path)?; + + let config = 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 + })?; + config.ensure_is_valid()?; + Ok(config) +} + +#[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..540657c618 --- /dev/null +++ b/nym-validator-rewarder/src/cli/run.rs @@ -0,0 +1,24 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::{try_load_current_config, ConfigOverridableArgs}; +use crate::error::NymRewarderError; +use crate::rewarder::Rewarder; +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, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NymRewarderError> { + let config = + try_load_current_config(&args.custom_config_path)?.with_override(args.config_override); + + Rewarder::new(config).await?.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..9003d32ceb --- /dev/null +++ b/nym-validator-rewarder/src/config/mod.rs @@ -0,0 +1,278 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::persistence::paths::ValidatorRewarderPaths; +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_validator_client::nyxd::Coin; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use std::io; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use tracing::debug; +use url::Url; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +pub mod r#override; +pub mod persistence; +mod template; + +const DEFAULT_REWARDER_DIR: &str = "validators-rewarder"; + +#[allow(clippy::inconsistent_digit_grouping)] +const DEFAULT_MIX_REWARDING_BUDGET: u128 = 1000_000000; +const DEFAULT_MIX_REWARDING_DENOM: &str = "unym"; + +const DEFAULT_EPOCH_DURATION: Duration = Duration::from_secs(60 * 60); +const DEFAULT_MONITOR_RUN_INTERVAL: Duration = Duration::from_secs(10 * 60); +const DEFAULT_MONITOR_MIN_VALIDATE: usize = 10; +const DEFAULT_MONITOR_SAMPLING_RATE: f64 = 0.10; + +/// 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, + + #[zeroize(skip)] + #[serde(default)] + pub rewarding: Rewarding, + + #[zeroize(skip)] + #[serde(default)] + pub block_signing: BlockSigning, + + #[zeroize(skip)] + #[serde(default)] + pub issuance_monitor: IssuanceMonitor, + + #[zeroize(skip)] + pub nyxd_scraper: NyxdScraper, + + #[serde(flatten)] + pub base: Base, + + #[zeroize(skip)] + pub storage_paths: ValidatorRewarderPaths, +} + +impl NymConfigTemplate for Config { + fn template(&self) -> &'static str { + CONFIG_TEMPLATE + } +} + +impl Config { + pub fn new(mnemonic: bip39::Mnemonic, websocket_url: Url, nyxd_url: Url) -> Self { + Config { + save_path: None, + rewarding: Rewarding::default(), + block_signing: Default::default(), + issuance_monitor: IssuanceMonitor::default(), + nyxd_scraper: NyxdScraper { websocket_url }, + base: Base { + upstream_nyxd: nyxd_url, + mnemonic, + }, + storage_paths: Default::default(), + } + } + + pub fn scraper_config(&self) -> nyxd_scraper::Config { + nyxd_scraper::Config { + websocket_url: self.nyxd_scraper.websocket_url.clone(), + rpc_url: self.base.upstream_nyxd.clone(), + database_path: self.storage_paths.nyxd_scraper.clone(), + } + } + + pub fn ensure_is_valid(&self) -> Result<(), NymRewarderError> { + self.rewarding.ratios.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 and rewarding. + #[zeroize(skip)] + pub upstream_nyxd: Url, + + /// Mnemonic to the nyx account distributing the rewards + pub(crate) mnemonic: bip39::Mnemonic, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Rewarding { + /// Specifies total budget for the epoch + #[serde_as(as = "DisplayFromStr")] + pub epoch_budget: Coin, + + #[serde(with = "humantime_serde")] + pub epoch_duration: Duration, + + pub ratios: RewardingRatios, +} + +impl Default for Rewarding { + fn default() -> Self { + Rewarding { + epoch_budget: Coin::new(DEFAULT_MIX_REWARDING_BUDGET, DEFAULT_MIX_REWARDING_DENOM), + epoch_duration: DEFAULT_EPOCH_DURATION, + ratios: RewardingRatios::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] +pub struct RewardingRatios { + /// The percent of the epoch reward being awarded for block signing. + pub block_signing: f64, + + /// The percent of the epoch reward being awarded for credential issuance. + pub credential_issuance: f64, + + /// The percent of the epoch reward being awarded for credential verification. + pub credential_verification: f64, + // /// The percent of the epoch reward given to Nym. + // pub nym: f64, +} + +impl Default for RewardingRatios { + fn default() -> Self { + RewardingRatios { + block_signing: 0.67, + credential_issuance: 0.33, + credential_verification: 0.0, + // nym: 0.0, + } + } +} + +impl RewardingRatios { + pub fn ensure_is_valid(&self) -> Result<(), NymRewarderError> { + if self.block_signing + self.credential_verification + self.credential_issuance != 1.0 { + todo!() + } + Ok(()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct NyxdScraper { + /// Url to the websocket endpoint of a validator, for example `wss://rpc.nymtech.net/websocket` + pub websocket_url: Url, + // TODO: debug with everything that's currently hardcoded in the scraper +} + +#[derive(Debug, Clone, Deserialize, Serialize, Copy)] +pub struct BlockSigning { + /// Specifies whether credential issuance for block signing is enabled. + pub enabled: bool, +} + +impl Default for BlockSigning { + fn default() -> Self { + BlockSigning { enabled: true } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Copy)] +pub struct IssuanceMonitor { + /// Specifies whether credential issuance monitoring (and associated rewards) are enabled. + pub enabled: bool, + + #[serde(with = "humantime_serde")] + pub run_interval: Duration, + + /// Defines the minimum number of credentials the monitor will validate + /// regardless of the sampling rate + pub min_validate_per_issuer: usize, + + /// The sampling rate of the issued credentials + pub sampling_rate: f64, +} + +impl Default for IssuanceMonitor { + fn default() -> Self { + IssuanceMonitor { + enabled: true, + run_interval: DEFAULT_MONITOR_RUN_INTERVAL, + min_validate_per_issuer: DEFAULT_MONITOR_MIN_VALIDATE, + sampling_rate: DEFAULT_MONITOR_SAMPLING_RATE, + } + } +} diff --git a/nym-validator-rewarder/src/config/override.rs b/nym-validator-rewarder/src/config/override.rs new file mode 100644 index 0000000000..01a42ef739 --- /dev/null +++ b/nym-validator-rewarder/src/config/override.rs @@ -0,0 +1,63 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::ConfigOverridableArgs; +use crate::config::Config; + +pub trait ConfigOverride { + fn override_config(self, config: &mut Config); +} + +impl ConfigOverride for ConfigOverridableArgs { + fn override_config(self, config: &mut Config) { + if self.disable_block_signing_rewarding { + config.block_signing.enabled = false + } + + if self.disable_credential_issuance_rewarding { + config.issuance_monitor.enabled = false + } + + if let Some(credential_monitor_run_interval) = self.credential_monitor_run_interval { + config.issuance_monitor.run_interval = credential_monitor_run_interval.into() + } + + if let Some(credential_monitor_min_validation) = self.credential_monitor_min_validation { + config.issuance_monitor.min_validate_per_issuer = credential_monitor_min_validation + } + + if let Some(credential_monitor_sampling_rate) = self.credential_monitor_sampling_rate { + config.issuance_monitor.sampling_rate = credential_monitor_sampling_rate + } + + if let Some(scraper_endpoint) = self.scraper_endpoint { + config.nyxd_scraper.websocket_url = scraper_endpoint + } + + if let Some(nyxd_endpoint) = self.nyxd_endpoint { + config.base.upstream_nyxd = nyxd_endpoint + } + + if let Some(epoch_budget) = self.epoch_budget { + config.rewarding.epoch_budget = epoch_budget + } + + if let Some(epoch_duration_secs) = self.epoch_duration { + config.rewarding.epoch_duration = epoch_duration_secs.into() + } + + if let Some(block_signing_reward_ratio) = self.block_signing_reward_ratio { + config.rewarding.ratios.block_signing = block_signing_reward_ratio; + } + + if let Some(credential_issuance_reward_ratio) = self.credential_issuance_reward_ratio { + config.rewarding.ratios.credential_issuance = credential_issuance_reward_ratio; + } + + if let Some(credential_verification_reward_ratio) = + self.credential_verification_reward_ratio + { + config.rewarding.ratios.credential_verification = credential_verification_reward_ratio; + } + } +} 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..fcead5e8ef --- /dev/null +++ b/nym-validator-rewarder/src/config/persistence/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod paths; diff --git a/nym-validator-rewarder/src/config/persistence/paths.rs b/nym-validator-rewarder/src/config/persistence/paths.rs new file mode 100644 index 0000000000..daee529076 --- /dev/null +++ b/nym-validator-rewarder/src/config/persistence/paths.rs @@ -0,0 +1,25 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::default_data_directory; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +pub const DEFAULT_SCRAPER_DB_FILENAME: &str = "nyxd_blocks.sqlite"; +pub const DEFAULT_REWARD_HISTORY_DB_FILENAME: &str = "rewards.sqlite"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ValidatorRewarderPaths { + pub nyxd_scraper: PathBuf, + + pub reward_history: PathBuf, +} + +impl Default for ValidatorRewarderPaths { + fn default() -> Self { + ValidatorRewarderPaths { + nyxd_scraper: default_data_directory().join(DEFAULT_SCRAPER_DB_FILENAME), + reward_history: default_data_directory().join(DEFAULT_REWARD_HISTORY_DB_FILENAME), + } + } +} diff --git a/nym-validator-rewarder/src/config/template.rs b/nym-validator-rewarder/src/config/template.rs new file mode 100644 index 0000000000..17ba1f9649 --- /dev/null +++ b/nym-validator-rewarder/src/config/template.rs @@ -0,0 +1,61 @@ +// 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 + +# Url to the upstream instance of nyxd to use for any queries and rewarding. +upstream_nyxd = '{{ upstream_nyxd }}' + +# Mnemonic to the nyx account distributing the rewards +mnemonic = '{{ mnemonic }}' + +[storage_paths] + +nyxd_scraper = '{{ storage_paths.nyxd_scraper }}' +reward_history = '{{ storage_paths.reward_history }}' + +[rewarding] +# Specifies total budget for the epoch +epoch_budget = '{{ rewarding.epoch_budget }}' + +epoch_duration = '{{ rewarding.epoch_duration }}' + +[rewarding.ratios] +# The percent of the epoch reward being awarded for block signing. +block_signing = {{ rewarding.ratios.block_signing }} + +# The percent of the epoch reward being awarded for credential issuance. +credential_issuance = {{ rewarding.ratios.credential_issuance }} + +# The percent of the epoch reward being awarded for credential verification. +credential_verification = {{ rewarding.ratios.credential_verification }} + + +[block_signing] +# Specifies whether credential issuance for block signing is enabled. +enabled = {{ block_signing.enabled }} + + +[issuance_monitor] +# Specifies whether credential issuance monitoring (and associated rewards) are enabled. +enabled = {{ issuance_monitor.enabled }} + +run_interval = '{{ issuance_monitor.run_interval }}' + +# Defines the minimum number of credentials the monitor will validate +# regardless of the sampling rate +min_validate_per_issuer = {{ issuance_monitor.min_validate_per_issuer }} + +# The sampling rate of the issued credentials +sampling_rate = {{ issuance_monitor.sampling_rate }} + +[nyxd_scraper] +# Url to the websocket endpoint of a validator, for example `wss://rpc.nymtech.net/websocket` +websocket_url = '{{ nyxd_scraper.websocket_url }}' +"#; diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs new file mode 100644 index 0000000000..b6dae63537 --- /dev/null +++ b/nym-validator-rewarder/src/error.rs @@ -0,0 +1,166 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_coconut::CoconutError; +use nym_validator_client::nym_api::error::NymAPIError; +use nym_validator_client::nyxd::error::NyxdError; +use nym_validator_client::nyxd::tx::ErrorReport; +use nym_validator_client::nyxd::{AccountId, Coin, Hash}; +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NymRewarderError { + #[error("experienced internal database error: {0}")] + InternalDatabaseError(#[from] sqlx::Error), + + #[error("failed to perform startup SQL migration: {0}")] + StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), + + #[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("failed to initialise paths")] + PathInitialisationFailure { + #[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 }, + + #[error("chain scraping failure: {source}")] + ScraperFailure { + #[from] + source: nyxd_scraper::error::ScraperError, + }, + + // this should never happen but unwrapping everywhere was more cumbersome than just propagating the error + #[error("failed to determine epoch boundaries: {0}")] + TimeComponentFailure(#[from] time::error::ComponentRange), + + #[error( + "could not convert operator address: {operator_address} to a nym account address: {source}" + )] + MalformedBech32Address { + operator_address: String, + #[source] + source: ErrorReport, + }, + + #[error( + "could not convert validator public key: {public_key} into a consensus address: {source}" + )] + MalformedConsensusPublicKey { + public_key: String, + #[source] + source: ErrorReport, + }, + + #[error("somehow the total voting power was negative: {val}")] + NegativeTotalVotingPower { val: i64 }, + + #[error("somehow the signed blocks was negative: {val}")] + NegativeSignedBlocks { val: i64 }, + + #[error("could not find details for validator {consensus_address}")] + MissingValidatorDetails { consensus_address: String }, + + #[error("api url ({raw}) provided by {runner_account} is invalid: {source}")] + MalformedApiUrl { + raw: String, + runner_account: AccountId, + #[source] + source: url::ParseError, + }, + + #[error("failed to resolve nym-api query: {0}")] + ApiQueryFailure(#[from] NymAPIError), + + #[error("operator {runner_account} didn't return all requested credentials! requested {requested} but got only {received}")] + IncompleteRequest { + runner_account: AccountId, + requested: usize, + received: usize, + }, + + #[error("the following private attribute commitment is malformed: {raw}: {source}")] + MalformedCredentialCommitment { + raw: String, + #[source] + source: CoconutError, + }, + + #[error("the partial verification key for runner {runner} is malformed: {source}")] + MalformedPartialVerificationKey { + runner: String, + #[source] + source: CoconutError, + }, + + #[error("could not verify the blinded credential")] + BlindVerificationFailure, + + #[error("the same deposit transaction ({tx_hash}) has been used for multiple issued credentials! {first} and {other}")] + DuplicateDepositHash { + tx_hash: Hash, + first: i64, + other: i64, + }, + + #[error("could not find the deposit value in the event of transaction {tx_hash}")] + DepositValueNotFound { tx_hash: Hash }, + + #[error("could not find the deposit info in the event of transaction {tx_hash}")] + DepositInfoNotFound { tx_hash: Hash }, + + #[error("the provided deposit value of transaction {tx_hash} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] + InconsistentDepositValue { + tx_hash: Hash, + request: Option, + on_chain: String, + }, + + #[error("the provided deposit info of transaction {tx_hash} is inconsistent. got '{request:?}' while the value on chain is '{on_chain}'")] + InconsistentDepositInfo { + tx_hash: Hash, + request: Option, + on_chain: String, + }, + + #[error("the current rewarder balance is insufficient to start the process. The epoch budget is: {} while we currently have {}. (the minimum is set to {})", .0.epoch_budget, .0.balance, .0.minimum)] + InsufficientRewarderBalance(Box), + + #[error("the scraper websocket endpoint hasn't been provided")] + UnavailableWebsocketUrl, +} + +#[derive(Debug)] +pub struct InsufficientBalance { + pub epoch_budget: Coin, + pub balance: Coin, + pub minimum: Coin, +} diff --git a/nym-validator-rewarder/src/main.rs b/nym-validator-rewarder/src/main.rs new file mode 100644 index 0000000000..fb03008a9a --- /dev/null +++ b/nym-validator-rewarder/src/main.rs @@ -0,0 +1,35 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] + +use crate::cli::Cli; +use clap::{crate_name, crate_version, Parser}; +use nym_bin_common::logging::{maybe_print_banner, setup_tracing_logger}; +use nym_network_defaults::setup_env; + +pub mod cli; +pub mod config; +pub mod error; +mod rewarder; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // std::env::set_var( + // "RUST_LOG", + // "debug,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn", + // ); + + let args = Cli::parse(); + setup_env(args.config_env_file.as_ref()); + setup_tracing_logger(); + + if !args.no_banner { + maybe_print_banner(crate_name!(), crate_version!()); + } + + args.execute().await?; + + Ok(()) +} diff --git a/nym-validator-rewarder/src/rewarder/block_signing/mod.rs b/nym-validator-rewarder/src/rewarder/block_signing/mod.rs new file mode 100644 index 0000000000..4fcc75bc66 --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/block_signing/mod.rs @@ -0,0 +1,139 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymRewarderError; +use crate::rewarder::block_signing::types::{EpochSigningResults, RawValidatorResult}; +use crate::rewarder::epoch::Epoch; +use crate::rewarder::nyxd_client::NyxdClient; +use nym_validator_client::nyxd::module_traits::staking; +use nym_validator_client::nyxd::PageRequest; +use nyxd_scraper::NyxdScraper; +use std::cmp::min; +use std::collections::HashMap; +use std::ops::Range; +use tracing::info; + +pub(crate) mod types; + +pub struct EpochSigning { + pub(crate) nyxd_client: NyxdClient, + pub(crate) nyxd_scraper: NyxdScraper, +} + +impl EpochSigning { + async fn get_voting_power( + &self, + address: &str, + height_range: Range, + ) -> Result, NymRewarderError> { + for height in height_range { + if let Some(precommit) = self + .nyxd_scraper + .storage + .get_precommit(address, height) + .await? + { + return Ok(Some(precommit.voting_power)); + } + } + + Ok(None) + } + + // TODO: eventually this will be replaced by scraping the data from the staking module in the scraper itself + async fn get_validator_details( + &self, + height: i64, + ) -> Result, NymRewarderError> { + // first attempt to get it via the historical info. + // if that fails, attempt to use current block information to at least get **something** + if let Some(validators) = self.nyxd_client.historical_info(height).await?.hist { + Ok(validators.valset) + } else { + let mut page_request = None; + let mut response = Vec::new(); + + loop { + let mut res = self.nyxd_client.validators(page_request).await?; + response.append(&mut res.validators); + + let Some(pagination) = res.pagination else { + break; + }; + + page_request = Some(PageRequest { + key: pagination.next_key, + offset: 0, + limit: 0, + count_total: false, + reverse: false, + }); + } + + Ok(response) + } + } + + pub(crate) async fn get_signed_blocks_results( + &self, + current_epoch: Epoch, + ) -> Result { + info!( + "looking up block signers for epoch {} ({} - {})", + current_epoch.id, + current_epoch.start_rfc3339(), + current_epoch.end_rfc3339() + ); + + let validators = self.nyxd_scraper.storage.get_all_known_validators().await?; + let epoch_start = current_epoch.start_time; + let epoch_end = current_epoch.end_time; + let first_block = self + .nyxd_scraper + .storage + .get_first_block_height_after(epoch_start) + .await? + .unwrap_or_default(); + let last_block = self + .nyxd_scraper + .storage + .get_last_block_height_before(epoch_end) + .await? + .unwrap_or_default(); + + // each validator MUST be online at some point during the first 20 blocks, otherwise they're not getting anything. + let vp_range_end = min(first_block + 20, last_block); + let vp_range = first_block..vp_range_end; + + let mut total_vp = 0; + let mut signed_in_epoch = HashMap::new(); + + // for each validator, with a valid voting power, get number of signed blocks in the rewarding epoch + for validator in validators { + let Some(vp) = self + .get_voting_power(&validator.consensus_address, vp_range.clone()) + .await? + else { + continue; + }; + total_vp += vp; + + let signed = self + .nyxd_scraper + .storage + .get_signed_between_times(&validator.consensus_address, epoch_start, epoch_end) + .await?; + signed_in_epoch.insert(validator, RawValidatorResult::new(signed, vp)); + } + + let total = self + .nyxd_scraper + .storage + .get_blocks_between(epoch_start, epoch_end) + .await?; + + let details = self.get_validator_details(last_block).await?; + + EpochSigningResults::construct(total, total_vp, signed_in_epoch, details) + } +} diff --git a/nym-validator-rewarder/src/rewarder/block_signing/types.rs b/nym-validator-rewarder/src/rewarder/block_signing/types.rs new file mode 100644 index 0000000000..bd28b2f49d --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/block_signing/types.rs @@ -0,0 +1,141 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymRewarderError; +use crate::rewarder::helpers::{consensus_pubkey_to_address, operator_account_to_owner_account}; +use cosmwasm_std::{Decimal, Uint128}; +use nym_validator_client::nyxd::module_traits::staking; +use nym_validator_client::nyxd::{AccountId, Coin}; +use nyxd_scraper::models; +use std::collections::HashMap; +use tracing::info; + +#[derive(Debug)] +pub struct ValidatorSigning { + pub validator: models::Validator, + pub staking_details: staking::Validator, + pub operator_account: AccountId, + + pub voting_power_at_epoch_start: i64, + pub voting_power_ratio: Decimal, + + pub signed_blocks: i32, + pub ratio_signed: Decimal, +} + +impl ValidatorSigning { + pub fn moniker(&self) -> String { + self.staking_details + .description + .as_ref() + .map(|d| d.moniker.clone()) + .unwrap_or("UNKNOWN MONIKER".to_string()) + } + + pub fn reward_amount(&self, signing_budget: &Coin) -> Coin { + let amount = + Uint128::new(signing_budget.amount) * self.ratio_signed * self.voting_power_ratio; + + Coin::new(amount.u128(), &signing_budget.denom) + } +} + +#[derive(Debug)] +pub struct EpochSigningResults { + pub blocks: i64, + pub total_voting_power_at_epoch_start: i64, + + pub validators: Vec, +} + +pub struct RawValidatorResult { + pub signed_blocks: i32, + pub voting_power: i64, +} + +impl RawValidatorResult { + pub fn new(signed_blocks: i32, voting_power: i64) -> Self { + Self { + signed_blocks, + voting_power, + } + } +} + +impl EpochSigningResults { + pub fn construct( + blocks: i64, + total_vp: i64, + validator_results: HashMap, + validator_details: Vec, + ) -> Result { + let Ok(total_vp_u64): Result = total_vp.try_into() else { + return Err(NymRewarderError::NegativeTotalVotingPower { val: total_vp }); + }; + let Ok(blocks_u64): Result = blocks.try_into() else { + return Err(NymRewarderError::NegativeSignedBlocks { val: blocks }); + }; + + let mut validator_details: HashMap<_, _> = validator_details + .into_iter() + .filter(|v| v.consensus_pubkey.is_some()) + .map(|v| { + // safety: we know the key is definitely set as we just filtered the iterator based on that condition + #[allow(clippy::unwrap_used)] + consensus_pubkey_to_address(v.consensus_pubkey.unwrap()) + .map(|addr| (addr.to_string(), v)) + }) + .collect::>()?; + + let mut validators = Vec::new(); + + for (validator, raw_results) in validator_results { + let vp: u64 = raw_results.voting_power.try_into().unwrap_or_default(); + let signed: u64 = raw_results.signed_blocks.try_into().unwrap_or_default(); + + let voting_power_ratio = Decimal::from_ratio(vp, total_vp_u64); + + debug_assert!(signed <= blocks_u64); + let ratio_signed = Decimal::from_ratio(signed, blocks_u64); + let staking_details = validator_details + .remove(&validator.consensus_address) + .ok_or_else(|| NymRewarderError::MissingValidatorDetails { + consensus_address: validator.consensus_address.clone(), + })?; + + let operator_account = + operator_account_to_owner_account(&staking_details.operator_address)?; + + validators.push(ValidatorSigning { + validator, + staking_details, + operator_account, + voting_power_at_epoch_start: raw_results.voting_power, + voting_power_ratio, + signed_blocks: raw_results.signed_blocks, + ratio_signed, + }) + } + + Ok(EpochSigningResults { + blocks, + total_voting_power_at_epoch_start: total_vp, + validators, + }) + } + + pub fn rewarding_amounts(&self, budget: &Coin) -> Vec<(AccountId, Vec)> { + self.validators + .iter() + .inspect(|v| { + info!( + "validator {} will receive {} at address {} for block signing work", + v.moniker(), + v.reward_amount(budget), + v.operator_account + ); + }) + .map(|v| (v.operator_account.clone(), vec![v.reward_amount(budget)])) + .collect() + } +} diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/mod.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/mod.rs new file mode 100644 index 0000000000..5807a60dec --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/mod.rs @@ -0,0 +1,58 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config; +use crate::error::NymRewarderError; +use crate::rewarder::credential_issuance::monitor::CredentialIssuanceMonitor; +use crate::rewarder::credential_issuance::types::{CredentialIssuanceResults, MonitoringResults}; +use crate::rewarder::epoch::Epoch; +use crate::rewarder::nyxd_client::NyxdClient; +use nym_task::TaskClient; +use tracing::info; + +mod monitor; +pub mod types; + +pub struct CredentialIssuance { + monitoring_results: MonitoringResults, +} + +impl CredentialIssuance { + pub(crate) async fn new( + epoch: Epoch, + nyxd_client: &NyxdClient, + ) -> Result { + Ok(CredentialIssuance { + monitoring_results: MonitoringResults::new_initial(epoch, nyxd_client).await?, + }) + } + + pub(crate) fn start_monitor( + &self, + monitor_config: config::IssuanceMonitor, + nyxd_client: NyxdClient, + task_client: TaskClient, + ) { + let monitoring_results = self.monitoring_results.clone(); + let mut monitor = + CredentialIssuanceMonitor::new(monitor_config, nyxd_client, monitoring_results); + + tokio::spawn(async move { monitor.run(task_client).await }); + } + + pub(crate) async fn get_issued_credentials_results( + &self, + current_epoch: Epoch, + ) -> Result { + info!( + "looking up credential issuers for epoch {} ({} - {})", + current_epoch.id, + current_epoch.start_rfc3339(), + current_epoch.end_rfc3339() + ); + + let raw_results = self.monitoring_results.finish_epoch().await; + + Ok(raw_results.into()) + } +} diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs new file mode 100644 index 0000000000..884a63b9fe --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs @@ -0,0 +1,340 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config; +use crate::error::NymRewarderError; +use crate::rewarder::credential_issuance::types::{ + CredentialIssuer, MonitoringResults, RawOperatorResult, +}; +use crate::rewarder::helpers::api_client; +use crate::rewarder::nyxd_client::NyxdClient; +use bip39::rand::prelude::SliceRandom; +use bip39::rand::thread_rng; +use nym_coconut::{ + hash_to_scalar, verify_partial_blind_signature, Base58, G1Projective, Parameters, + VerificationKey, +}; +use nym_coconut_dkg_common::types::EpochId; +use nym_credentials::coconut::bandwidth::BandwidthVoucher; +use nym_task::TaskClient; +use nym_validator_client::nym_api::{IssuedCredential, IssuedCredentialBody, NymApiClientExt}; +use nym_validator_client::nyxd::Hash; +use std::cmp::max; +use std::collections::HashMap; +use std::sync::OnceLock; +use tokio::time::interval; +use tracing::{debug, error, info, instrument, trace, warn}; + +pub(crate) fn bandwidth_voucher_params() -> &'static Parameters { + static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock = OnceLock::new(); + BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(BandwidthVoucher::default_parameters) +} + +pub struct CredentialIssuanceMonitor { + nyxd_client: NyxdClient, + monitoring_results: MonitoringResults, + config: config::IssuanceMonitor, + + // map of validator address -> transaction hash -> issued credential + // (ideally we'd have hashed the AccountId directly, but it doesn't implement `Hash`) + seen_deposits: HashMap>, +} + +impl CredentialIssuanceMonitor { + pub fn new( + config: config::IssuanceMonitor, + nyxd_client: NyxdClient, + monitoring_results: MonitoringResults, + ) -> CredentialIssuanceMonitor { + CredentialIssuanceMonitor { + config, + nyxd_client, + monitoring_results, + seen_deposits: Default::default(), + } + } + + fn validate_credential_signature( + &mut self, + _issued_credential: &IssuedCredentialBody, + ) -> Result<(), NymRewarderError> { + warn!("unimplemented: public key sharing mechanism"); + // let plaintext = issued_credential.credential.signable_plaintext(); + // if !operator_public_key.verify(&plaintext) { + // ... + // } + Ok(()) + } + + fn check_deposit_reuse( + &mut self, + runner: &str, + credential_id: i64, + deposit_tx: Hash, + ) -> Result { + // check if we've seen this tx hash before + // TODO: we should persist them in the database in case we crash + if let Some(known_runner) = self.seen_deposits.get_mut(runner) { + if let Some(&used) = known_runner.get(&deposit_tx) { + return if used != credential_id { + Err(NymRewarderError::DuplicateDepositHash { + tx_hash: deposit_tx, + first: used, + other: credential_id, + }) + } else { + debug!("we have already verified this credential before"); + Ok(true) + }; + } else { + known_runner.insert(deposit_tx, credential_id); + } + } + Ok(false) + } + + async fn validate_deposit( + &mut self, + issued_credential: &IssuedCredentialBody, + ) -> Result<(), NymRewarderError> { + // check if this deposit even exists + let deposit_tx = issued_credential.credential.tx_hash; + + let (deposit_value, deposit_info) = self + .nyxd_client + .get_deposit_transaction_attributes(deposit_tx) + .await?; + trace!("deposit exists"); + + // check if the deposit values match + let credential_value = issued_credential.credential.public_attributes.first(); + let credential_info = issued_credential.credential.public_attributes.get(1); + + if credential_value != Some(&deposit_value) { + return Err(NymRewarderError::InconsistentDepositValue { + tx_hash: deposit_tx, + request: credential_value.cloned(), + on_chain: deposit_value, + }); + } + trace!("credential values matches the deposit"); + + if credential_info != Some(&deposit_info) { + return Err(NymRewarderError::InconsistentDepositInfo { + tx_hash: deposit_tx, + request: credential_info.cloned(), + on_chain: deposit_info, + }); + } + trace!("credential info matches the deposit"); + Ok(()) + } + + fn verify_credential( + &mut self, + vk: &VerificationKey, + credential: IssuedCredential, + ) -> Result<(), NymRewarderError> { + let public_attributes = credential + .public_attributes + .iter() + .map(hash_to_scalar) + .collect::>(); + + #[allow(clippy::map_identity)] + let attributes_refs = public_attributes.iter().collect::>(); + + let mut public_attribute_commitments = + Vec::with_capacity(credential.bs58_encoded_private_attributes_commitments.len()); + + for raw_cm in credential.bs58_encoded_private_attributes_commitments { + match G1Projective::try_from_bs58(&raw_cm) { + Ok(cm) => public_attribute_commitments.push(cm), + Err(source) => { + return Err(NymRewarderError::MalformedCredentialCommitment { + raw: raw_cm, + source, + }) + } + } + } + + // actually do verify the credential now + if !verify_partial_blind_signature( + bandwidth_voucher_params(), + &public_attribute_commitments, + &attributes_refs, + &credential.blinded_partial_credential, + vk, + ) { + return Err(NymRewarderError::BlindVerificationFailure); + } + trace!("credential correctly verifies"); + + Ok(()) + } + + // TODO: currently we can't obtain public key of the runner in order to verify the signature + #[instrument(skip_all, fields(credential_id = credential_id, deposit = %issued_credential.credential.tx_hash))] + async fn validate_issued_credential( + &mut self, + runner: String, + credential_id: i64, + issued_credential: IssuedCredentialBody, + vk: &VerificationKey, + ) -> Result<(), NymRewarderError> { + self.validate_credential_signature(&issued_credential)?; + + let deposit_tx = issued_credential.credential.tx_hash; + + // make sure the issuer is not using the same deposit for multiple credentials + let already_checked = self.check_deposit_reuse(&runner, credential_id, deposit_tx)?; + if already_checked { + return Ok(()); + } + + // check the correctness of the deposit itself + self.validate_deposit(&issued_credential).await?; + + // check if the partial credential correctly verifies + self.verify_credential(vk, issued_credential.credential)?; + + Ok(()) + } + + fn sample_credential_ids(&self, first_id: i64, total_issued: i64) -> Vec { + let credential_range: Vec<_> = (first_id..first_id + total_issued).collect(); + let issued = credential_range.len(); + + let sampled = if issued <= self.config.min_validate_per_issuer { + credential_range + } else { + let mut rng = thread_rng(); + let sample_size = max( + self.config.min_validate_per_issuer, + (issued as f64 * self.config.sampling_rate) as usize, + ); + credential_range + .choose_multiple(&mut rng, sample_size) + .copied() + .collect::>() + }; + + sampled + } + + #[instrument(skip(self, issuer, epoch_id), fields(dkg_epoch = epoch_id, issuer = %issuer.operator_account, url = issuer.api_runner), err(Display))] + async fn check_issuer( + &mut self, + epoch_id: EpochId, + issuer: CredentialIssuer, + ) -> Result { + info!("checking the issuer's credentials..."); + debug!("checking the issuer's credentials..."); + + let api_client = api_client(&issuer)?; + + let epoch_credentials = api_client.epoch_credentials(epoch_id).await?; + let Some(first_id) = epoch_credentials.first_epoch_credential_id else { + // no point in doing anything more - if they haven't issued anything, there's nothing to verify + debug!("no credentials issued this epoch",); + return Ok(RawOperatorResult::new_empty( + issuer.operator_account, + issuer.api_runner, + )); + }; + trace!("issued credentials: {epoch_credentials:?}"); + + let sampled = self.sample_credential_ids(first_id, epoch_credentials.total_issued as i64); + let request_size = sampled.len(); + + trace!("sampled credentials to validate: {sampled:?}"); + + let credentials = api_client.issued_credentials(sampled.clone()).await?; + if credentials.credentials.len() != request_size { + // TODO: we need some signatures here to actually show the validator is cheating + return Err(NymRewarderError::IncompleteRequest { + runner_account: issuer.operator_account, + requested: request_size, + received: credentials.credentials.len(), + }); + } + + for (id, credential) in credentials.credentials { + trace!("checking credential {id}..."); + // TODO: insert the failure information, alongside the signature, to the evidence db + if let Err(err) = self + .validate_issued_credential( + issuer.operator_account.to_string(), + id, + credential, + &issuer.verification_key, + ) + .await + { + error!( + "failed to verify credential {id} from {}!!: {err}", + issuer.operator_account + ); + return Err(err); + } + } + + Ok(RawOperatorResult { + operator_account: issuer.operator_account, + api_runner: issuer.api_runner, + issued_credentials: epoch_credentials.total_issued, + validated_credentials: sampled, + }) + } + + async fn check_issuers(&mut self) -> Result<(), NymRewarderError> { + info!("checking credential issuers"); + let epoch = self.nyxd_client.dkg_epoch().await?; + let issuers = self + .nyxd_client + .get_credential_issuers(epoch.epoch_id) + .await?; + + let mut results = Vec::with_capacity(issuers.len()); + + for issuer in issuers { + let address = issuer.operator_account.clone(); + // we could parallelize it, but we're running the test so infrequently (relatively speaking) + // that doing it sequentially is fine + match self.check_issuer(epoch.epoch_id, issuer).await { + Ok(res) => results.push(res), + Err(err) => { + // TODO: insert info to the db + error!("failed to check credential issuance of {address}: {err}") + } + } + } + + self.monitoring_results + .append_run_results(epoch.epoch_id as u32, results) + .await; + + Ok(()) + } + + pub async fn run(&mut self, mut task_client: TaskClient) { + info!("starting"); + let mut run_interval = interval(self.config.run_interval); + + while !task_client.is_shutdown() { + tokio::select! { + biased; + _ = task_client.recv() => { + info!("received shutdown"); + break + } + _ = run_interval.tick() => { + if let Err(err) = self.check_issuers().await { + error!("failed to perform credential issuance check: {err}") + } + } + } + } + } +} diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs new file mode 100644 index 0000000000..e6f76a10ae --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/types.rs @@ -0,0 +1,349 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NymRewarderError; +use crate::rewarder::epoch::Epoch; +use crate::rewarder::helpers::api_client; +use crate::rewarder::nyxd_client::NyxdClient; +use cosmwasm_std::{Addr, Decimal, Uint128}; +use nym_coconut::{Base58, VerificationKey}; +use nym_coconut_dkg_common::verification_key::ContractVKShare; +use nym_validator_client::nym_api::NymApiClientExt; +use nym_validator_client::nyxd::{AccountId, Coin}; +use std::collections::{HashMap, HashSet}; +use std::mem; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{info, warn}; + +#[derive(Clone)] +pub struct MonitoringResults { + inner: Arc>, +} + +impl MonitoringResults { + pub(crate) async fn new_initial( + initial_epoch: Epoch, + nyxd_client: &NyxdClient, + ) -> Result { + let epoch = nyxd_client.dkg_epoch().await?; + let issuers = nyxd_client.get_credential_issuers(epoch.epoch_id).await?; + + let mut initial_results = HashMap::new(); + + for issuer in issuers { + let issuer_account = issuer.operator_account.to_string(); + let mut raw_issuer = RawOperatorIssuing { + api_runner: issuer.api_runner.clone(), + runner_account: issuer.operator_account.clone(), + per_epoch: Default::default(), + }; + + let Ok(api_client) = api_client(&issuer) else { + warn!("failed to create api client for {issuer_account}"); + initial_results.insert(issuer_account, raw_issuer); + continue; + }; + + let Ok(epoch_credentials) = api_client.epoch_credentials(epoch.epoch_id).await else { + warn!("failed to get initial epoch credentials from {issuer_account}"); + initial_results.insert(issuer_account, raw_issuer); + continue; + }; + + raw_issuer.per_epoch.insert( + epoch.epoch_id as u32, + IssuedEpochCredentials { + issued_since_monitor_started: 0, + validated_ids: Default::default(), + last_total_issued: epoch_credentials.total_issued, + }, + ); + initial_results.insert(issuer_account, raw_issuer); + } + + Ok(MonitoringResults { + inner: Arc::new(Mutex::new(MonitoringResultsInner::new( + initial_epoch, + epoch.epoch_id as u32, + initial_results, + ))), + }) + } + + pub(crate) async fn append_run_results(&self, dkg_epoch: u32, results: Vec) { + let mut guard = self.inner.lock().await; + + for result in results { + let Some(entry) = guard.operators.get_mut(result.operator_account.as_ref()) else { + // if this is the first time we're seeing this data, make sure to set the current results as the starting point + guard.operators.insert( + result.operator_account.to_string(), + RawOperatorIssuing::new_empty(dkg_epoch, result), + ); + + continue; + }; + + let Some(epoch_data) = entry.per_epoch.get_mut(&dkg_epoch) else { + // similar situation to the above, if we don't have the proper initial data, set it to what we got now + entry + .per_epoch + .insert(dkg_epoch, IssuedEpochCredentials::new_initial(&result)); + continue; + }; + + let issued = result.issued_credentials - epoch_data.last_total_issued; + epoch_data.last_total_issued = result.issued_credentials; + + for validated in result.validated_credentials { + epoch_data.validated_ids.insert(validated); + } + + epoch_data.issued_since_monitor_started += issued; + } + } + + pub(crate) async fn finish_epoch(&self) -> MonitoringResultsInner { + let mut guard = self.inner.lock().await; + let next_epoch = guard.epoch.next(); + + // safety: whenever the monitor results are constructed, we always have at least a single value there + #[allow(clippy::unwrap_used)] + let latest_dkg_epoch = guard.dkg_epochs.pop().unwrap(); + + // only keep results from the latest dkg epoch (but after resetting the counters) + let mut to_keep = HashMap::new(); + for (runner, result) in &guard.operators { + let mut kept_epoch = HashMap::new(); + if let Some(data) = result.per_epoch.get(&latest_dkg_epoch) { + kept_epoch.insert( + latest_dkg_epoch, + IssuedEpochCredentials { + issued_since_monitor_started: 0, + validated_ids: Default::default(), + last_total_issued: data.last_total_issued, + }, + ); + } + + to_keep.insert( + runner.clone(), + RawOperatorIssuing { + api_runner: result.api_runner.clone(), + runner_account: result.runner_account.clone(), + per_epoch: kept_epoch, + }, + ); + } + + let next_results = MonitoringResultsInner { + epoch: next_epoch, + dkg_epochs: vec![latest_dkg_epoch], + operators: to_keep, + }; + + mem::replace(&mut guard, next_results) + } +} + +pub(crate) struct MonitoringResultsInner { + pub(crate) epoch: Epoch, + pub(crate) dkg_epochs: Vec, + + // map from operator's account to their results + pub(crate) operators: HashMap, +} + +impl From for CredentialIssuanceResults { + fn from(value: MonitoringResultsInner) -> Self { + let total_issued = value + .operators + .values() + .map(|o| { + let operator_issued: u32 = o + .per_epoch + .values() + .map(|e| e.issued_since_monitor_started) + .sum(); + operator_issued + }) + .sum(); + + CredentialIssuanceResults { + total_issued_partial_credentials: total_issued, + dkg_epochs: value.dkg_epochs, + api_runners: value + .operators + .into_values() + .map(|runner| { + let issued_ratio = if total_issued == 0 { + Decimal::zero() + } else { + Decimal::from_ratio(runner.issued_credentials(), total_issued) + }; + OperatorIssuing { + issued_ratio, + issued_credentials: runner.issued_credentials(), + validated_credentials: runner.validated_credentials(), + api_runner: runner.api_runner, + runner_account: runner.runner_account, + } + }) + .collect(), + } + } +} + +impl MonitoringResultsInner { + fn new( + epoch: Epoch, + initial_dkg_epoch: u32, + initial_operators: HashMap, + ) -> MonitoringResultsInner { + MonitoringResultsInner { + epoch, + dkg_epochs: vec![initial_dkg_epoch], + operators: initial_operators, + } + } +} + +pub(crate) struct RawOperatorResult { + pub(crate) operator_account: AccountId, + pub(crate) api_runner: String, + + // how many credentials the operator claims to have issued in **TOTAL** in this **DKG** epoch + pub(crate) issued_credentials: u32, + pub(crate) validated_credentials: Vec, +} + +impl RawOperatorResult { + pub(crate) fn new_empty(operator_account: AccountId, api_runner: String) -> RawOperatorResult { + RawOperatorResult { + operator_account, + api_runner, + issued_credentials: 0, + validated_credentials: Default::default(), + } + } +} + +pub struct RawOperatorIssuing { + pub api_runner: String, + pub runner_account: AccountId, + + pub per_epoch: HashMap, +} + +impl RawOperatorIssuing { + pub fn new_empty(epoch: u32, raw_result: RawOperatorResult) -> RawOperatorIssuing { + let mut per_epoch = HashMap::new(); + per_epoch.insert(epoch, IssuedEpochCredentials::new_initial(&raw_result)); + RawOperatorIssuing { + api_runner: raw_result.api_runner, + runner_account: raw_result.operator_account, + per_epoch, + } + } + + pub fn issued_credentials(&self) -> u32 { + self.per_epoch + .values() + .map(|e| e.issued_since_monitor_started) + .sum() + } + + pub fn validated_credentials(&self) -> u32 { + let ids: usize = self.per_epoch.values().map(|e| e.validated_ids.len()).sum(); + ids as u32 + } +} + +pub struct IssuedEpochCredentials { + pub issued_since_monitor_started: u32, + pub validated_ids: HashSet, + pub last_total_issued: u32, +} + +impl IssuedEpochCredentials { + pub fn new_initial(raw: &RawOperatorResult) -> Self { + IssuedEpochCredentials { + issued_since_monitor_started: 0, + validated_ids: raw.validated_credentials.iter().copied().collect(), + last_total_issued: raw.issued_credentials, + } + } +} + +pub struct OperatorIssuing { + pub api_runner: String, + pub runner_account: AccountId, + + pub issued_ratio: Decimal, + pub issued_credentials: u32, + pub validated_credentials: u32, +} + +impl OperatorIssuing { + pub fn reward_amount(&self, signing_budget: &Coin) -> Coin { + let amount = Uint128::new(signing_budget.amount) * self.issued_ratio; + + Coin::new(amount.u128(), &signing_budget.denom) + } +} + +pub struct CredentialIssuanceResults { + pub total_issued_partial_credentials: u32, + pub dkg_epochs: Vec, + pub api_runners: Vec, +} + +impl CredentialIssuanceResults { + pub fn rewarding_amounts(&self, budget: &Coin) -> Vec<(AccountId, Vec)> { + self.api_runners + .iter() + .inspect(|a| { + info!( + "operator {} will receive {} at address {} for credential issuance work", + a.api_runner, + a.reward_amount(budget), + a.runner_account, + ); + }) + .map(|v| (v.runner_account.clone(), vec![v.reward_amount(budget)])) + .collect() + } +} + +#[derive(Debug)] +pub struct CredentialIssuer { + // pub public_key: identity::PublicKey, + pub operator_account: AccountId, + pub api_runner: String, + pub verification_key: VerificationKey, +} + +impl TryFrom for CredentialIssuer { + type Error = NymRewarderError; + + fn try_from(value: ContractVKShare) -> Result { + Ok(CredentialIssuer { + operator_account: addr_to_account_id(value.owner.clone()), + api_runner: value.announce_address, + verification_key: VerificationKey::try_from_bs58(value.share).map_err(|source| { + NymRewarderError::MalformedPartialVerificationKey { + runner: value.owner.to_string(), + source, + } + })?, + }) + } +} + +// safety: we're converting between different wrappers for bech32 addresses +// and we trust (reasonably so), the values coming out of registered dealers in the DKG contract +fn addr_to_account_id(addr: Addr) -> AccountId { + #[allow(clippy::unwrap_used)] + addr.as_str().parse().unwrap() +} diff --git a/nym-validator-rewarder/src/rewarder/epoch.rs b/nym-validator-rewarder/src/rewarder/epoch.rs new file mode 100644 index 0000000000..1b828da883 --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/epoch.rs @@ -0,0 +1,61 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymRewarderError; +use sqlx::FromRow; +use std::ops::Add; +use std::time::Duration; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; + +const HOUR: Duration = Duration::from_secs(60 * 60); + +#[derive(Debug, Clone, Copy, FromRow)] +pub struct Epoch { + pub id: i64, + + pub start_time: OffsetDateTime, + pub end_time: OffsetDateTime, +} + +impl Epoch { + pub fn first(epoch_duration: Duration) -> Result { + let start = OffsetDateTime::now_utc() + .add(HOUR) + .replace_nanosecond(0)? + .replace_microsecond(0)? + .replace_second(0)?; + + Ok(Epoch { + id: 0, + start_time: start, + end_time: start + epoch_duration, + }) + } + + pub fn until_end(&self) -> Duration { + let now = OffsetDateTime::now_utc(); + (self.end_time - now).try_into().unwrap_or_default() + } + + pub fn next(&self) -> Self { + let duration = self.end_time - self.start_time; + Epoch { + id: self.id + 1, + start_time: self.end_time, + end_time: self.end_time + duration, + } + } + + pub fn start_rfc3339(&self) -> String { + // safety: unwrap here is fine as we're using a predefined formatter + #[allow(clippy::unwrap_used)] + self.start_time.format(&Rfc3339).unwrap() + } + + pub fn end_rfc3339(&self) -> String { + // safety: unwrap here is fine as we're using a predefined formatter + #[allow(clippy::unwrap_used)] + self.end_time.format(&Rfc3339).unwrap() + } +} diff --git a/nym-validator-rewarder/src/rewarder/helpers.rs b/nym-validator-rewarder/src/rewarder/helpers.rs new file mode 100644 index 0000000000..f49d96e5fb --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/helpers.rs @@ -0,0 +1,50 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymRewarderError; +use crate::rewarder::credential_issuance::types::CredentialIssuer; +use nym_validator_client::nym_api; +use nym_validator_client::nyxd::{AccountId, PublicKey}; +use nyxd_scraper::constants::{BECH32_CONSENSUS_ADDRESS_PREFIX, BECH32_PREFIX}; +use sha2::{Digest, Sha256}; + +pub(crate) fn consensus_pubkey_to_address( + pubkey: PublicKey, +) -> Result { + let digest = Sha256::digest(pubkey.to_bytes()).to_vec(); + + // TODO: make those configurable, etc + AccountId::new(BECH32_CONSENSUS_ADDRESS_PREFIX, &digest[..20]).map_err(|source| { + NymRewarderError::MalformedConsensusPublicKey { + public_key: pubkey.to_string(), + source, + } + }) +} + +// it's just a matter of swapping bech32 prefixes and recalculating the checksum +pub(crate) fn operator_account_to_owner_account( + operator_address: &AccountId, +) -> Result { + AccountId::new(BECH32_PREFIX, &operator_address.to_bytes()).map_err(|source| { + NymRewarderError::MalformedBech32Address { + operator_address: operator_address.to_string(), + source, + } + }) +} + +pub(crate) fn api_client(issuer: &CredentialIssuer) -> Result { + let url = match issuer.api_runner.parse() { + Ok(url) => url, + Err(source) => { + return Err(NymRewarderError::MalformedApiUrl { + raw: issuer.api_runner.clone(), + runner_account: issuer.operator_account.clone(), + source, + }) + } + }; + + Ok(nym_api::Client::new(url, None)) +} diff --git a/nym-validator-rewarder/src/rewarder/mod.rs b/nym-validator-rewarder/src/rewarder/mod.rs new file mode 100644 index 0000000000..f58b3ce63f --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/mod.rs @@ -0,0 +1,287 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::Config; +use crate::error::{InsufficientBalance, NymRewarderError}; +use crate::rewarder::block_signing::types::EpochSigningResults; +use crate::rewarder::block_signing::EpochSigning; +use crate::rewarder::credential_issuance::types::CredentialIssuanceResults; +use crate::rewarder::credential_issuance::CredentialIssuance; +use crate::rewarder::epoch::Epoch; +use crate::rewarder::nyxd_client::NyxdClient; +use crate::rewarder::storage::RewarderStorage; +use nym_task::TaskManager; +use nym_validator_client::nyxd::{AccountId, Coin, Hash}; +use nyxd_scraper::NyxdScraper; +use std::ops::Add; +use tokio::pin; +use tokio::time::{interval_at, Instant}; +use tracing::{error, info, instrument}; + +mod block_signing; +mod credential_issuance; +mod epoch; +mod helpers; +mod nyxd_client; +mod storage; +mod tasks; + +pub struct EpochRewards { + pub epoch: Epoch, + pub signing: Option, + pub credentials: Option, + + pub total_budget: Coin, + pub signing_budget: Coin, + pub credentials_budget: Coin, +} + +impl EpochRewards { + pub fn amounts(&self) -> Vec<(AccountId, Vec)> { + let mut amounts = Vec::new(); + + if let Some(signing) = &self.signing { + for signing_amount in signing.rewarding_amounts(&self.signing_budget) { + if signing_amount.1[0].amount != 0 { + amounts.push(signing_amount) + } + } + } + + if let Some(credentials) = &self.credentials { + for credential_amount in credentials.rewarding_amounts(&self.credentials_budget) { + if credential_amount.1[0].amount != 0 { + amounts.push(credential_amount) + } + } + } + + amounts + } +} + +pub fn total_spent(amounts: &[(AccountId, Vec)], denom: &str) -> Coin { + let amount = amounts.iter().map(|(_, amount)| amount[0].amount).sum(); + Coin::new(amount, denom) +} + +pub struct Rewarder { + config: Config, + current_epoch: Epoch, + + storage: RewarderStorage, + nyxd_client: NyxdClient, + epoch_signing: Option, + credential_issuance: Option, +} + +impl Rewarder { + pub async fn new(config: Config) -> Result { + let nyxd_client = NyxdClient::new(&config)?; + let storage = RewarderStorage::init(&config.storage_paths.reward_history).await?; + let current_epoch = if let Some(last_epoch) = storage.load_last_rewarding_epoch().await? { + last_epoch.next() + } else { + Epoch::first(config.rewarding.epoch_duration)? + }; + + let epoch_signing = if config.block_signing.enabled { + let nyxd_scraper = NyxdScraper::new(config.scraper_config()).await?; + + Some(EpochSigning { + nyxd_scraper, + nyxd_client: nyxd_client.clone(), + }) + } else { + None + }; + + let credential_issuance = if config.issuance_monitor.enabled { + Some(CredentialIssuance::new(current_epoch, &nyxd_client).await?) + } else { + None + }; + + if config.issuance_monitor.enabled || config.block_signing.enabled { + let balance = nyxd_client + .balance(&config.rewarding.epoch_budget.denom) + .await?; + let minimum = Coin::new( + config.rewarding.epoch_budget.amount * 100, + &config.rewarding.epoch_budget.denom, + ); + + if balance.amount < minimum.amount { + return Err(NymRewarderError::InsufficientRewarderBalance(Box::new( + InsufficientBalance { + epoch_budget: config.rewarding.epoch_budget.clone(), + balance, + minimum, + }, + ))); + } + } + + Ok(Rewarder { + current_epoch, + credential_issuance, + epoch_signing, + nyxd_client, + storage, + config, + }) + } + + #[instrument(skip(self))] + async fn calculate_block_signing_rewards( + &mut self, + ) -> Result, NymRewarderError> { + info!("calculating reward shares"); + if let Some(epoch_signing) = &mut self.epoch_signing { + Some( + epoch_signing + .get_signed_blocks_results(self.current_epoch) + .await, + ) + } else { + None + } + .transpose() + } + + #[instrument(skip(self))] + async fn calculate_credential_rewards( + &mut self, + ) -> Result, NymRewarderError> { + info!("calculating reward shares"); + if let Some(credential_issuance) = &mut self.credential_issuance { + Some( + credential_issuance + .get_issued_credentials_results(self.current_epoch) + .await, + ) + } else { + None + } + .transpose() + } + + async fn determine_epoch_rewards(&mut self) -> Result { + let epoch_budget = self.config.rewarding.epoch_budget.clone(); + let denom = &epoch_budget.denom; + let signing_budget = Coin::new( + (self.config.rewarding.ratios.block_signing * epoch_budget.amount as f64) as u128, + denom, + ); + let credentials_budget = Coin::new( + (self.config.rewarding.ratios.credential_issuance * epoch_budget.amount as f64) as u128, + denom, + ); + + let signing_rewards = self.calculate_block_signing_rewards().await?; + let credential_rewards = self.calculate_credential_rewards().await?; + + Ok(EpochRewards { + epoch: self.current_epoch, + signing: signing_rewards, + credentials: credential_rewards, + total_budget: epoch_budget.clone(), + signing_budget, + credentials_budget, + }) + } + + #[instrument(skip(self))] + async fn send_rewards( + &self, + amounts: Vec<(AccountId, Vec)>, + ) -> Result { + info!("sending rewards"); + self.nyxd_client + .send_rewards(self.current_epoch, amounts) + .await + } + + async fn handle_epoch_end(&mut self) { + info!("handling the epoch end"); + + let rewards = match self.determine_epoch_rewards().await { + Ok(rewards) => rewards, + Err(err) => { + error!("failed to determine epoch rewards: {err}"); + return; + } + }; + + let rewarding_amounts = rewards.amounts(); + let total_spent = total_spent( + &rewarding_amounts, + &self.config.rewarding.epoch_budget.denom, + ); + + let rewarding_result = self.send_rewards(rewarding_amounts).await; + if let Err(err) = self + .storage + .save_rewarding_information(rewards, total_spent, rewarding_result) + .await + { + error!("failed to persist rewarding information: {err}") + } + + self.current_epoch = self.current_epoch.next(); + } + + pub async fn run(mut self) -> Result<(), NymRewarderError> { + info!("Starting nym validators rewarder"); + + // setup shutdowns + let mut task_manager = TaskManager::new(5); + + if let Some(ref credential_issuance) = self.credential_issuance { + credential_issuance.start_monitor( + self.config.issuance_monitor, + self.nyxd_client.clone(), + task_manager.subscribe(), + ); + } + + if let Some(epoch_signing) = &self.epoch_signing { + epoch_signing.nyxd_scraper.start().await?; + epoch_signing.nyxd_scraper.wait_for_startup_sync().await; + } + + let until_end = self.current_epoch.until_end(); + + info!( + "the first epoch will finish in {} secs", + until_end.as_secs() + ); + let mut epoch_ticker = interval_at( + Instant::now().add(until_end), + self.config.rewarding.epoch_duration, + ); + + let shutdown_future = task_manager.catch_interrupt(); + pin!(shutdown_future); + + loop { + tokio::select! { + biased; + interrupt_res = &mut shutdown_future => { + info!("received interrupt"); + if let Err(err) = interrupt_res { + error!("runtime interrupt failure: {err}") + } + break; + } + _ = epoch_ticker.tick() => self.handle_epoch_end().await + } + } + + if let Some(epoch_signing) = self.epoch_signing { + epoch_signing.nyxd_scraper.stop().await; + } + + Ok(()) + } +} diff --git a/nym-validator-rewarder/src/rewarder/nyxd_client.rs b/nym-validator-rewarder/src/rewarder/nyxd_client.rs new file mode 100644 index 0000000000..b430136930 --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/nyxd_client.rs @@ -0,0 +1,123 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::Config; +use crate::error::NymRewarderError; +use crate::rewarder::credential_issuance::types::CredentialIssuer; +use nym_coconut_bandwidth_contract_common::events::{ + COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO, DEPOSIT_VALUE, +}; +use nym_coconut_dkg_common::types::Epoch; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; +use nym_validator_client::nyxd::helpers::find_tx_attribute; +use nym_validator_client::nyxd::module_traits::staking::{ + QueryHistoricalInfoResponse, QueryValidatorsResponse, +}; +use nym_validator_client::nyxd::{ + AccountId, Coin, CosmWasmClient, Hash, PageRequest, StakingQueryClient, +}; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; +use std::ops::Deref; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[derive(Clone)] +pub struct NyxdClient { + inner: Arc>, +} + +impl NyxdClient { + pub(crate) fn new(config: &Config) -> Result { + let client_config = + nyxd::Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?; + let nyxd_url = config.base.upstream_nyxd.as_str(); + + let mnemonic = config.base.mnemonic.clone(); + + let inner = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + client_config, + nyxd_url, + mnemonic, + )?; + + Ok(NyxdClient { + inner: Arc::new(RwLock::new(inner)), + }) + } + + pub(crate) async fn balance(&self, denom: &str) -> Result { + let guard = self.inner.read().await; + let address = guard.address(); + Ok(guard + .get_balance(&address, denom.to_string()) + .await? + .unwrap_or(Coin::new(0, denom))) + } + + pub(crate) async fn send_rewards( + &self, + epoch: crate::rewarder::Epoch, + amounts: Vec<(AccountId, Vec)>, + ) -> Result { + self.inner + .write() + .await + .send_multiple(amounts, format!("sending rewards for {epoch:?}"), None) + .await + .map(|res| res.hash) + .map_err(Into::into) + } + + pub(crate) async fn historical_info( + &self, + height: i64, + ) -> Result { + Ok(self.inner.read().await.historical_info(height).await?) + } + + pub(crate) async fn validators( + &self, + pagination: Option, + ) -> Result { + let guard = self.inner.read().await; + Ok(StakingQueryClient::validators(guard.deref(), "".to_string(), pagination).await?) + } + + pub(crate) async fn dkg_epoch(&self) -> Result { + Ok(self.inner.read().await.get_current_epoch().await?) + } + + pub(crate) async fn get_credential_issuers( + &self, + dkg_epoch: u64, + ) -> Result, NymRewarderError> { + self.inner + .read() + .await + .get_all_verification_key_shares(dkg_epoch) + .await? + .into_iter() + .map(TryInto::try_into) + .collect() + } + + pub(crate) async fn get_deposit_transaction_attributes( + &self, + tx_hash: Hash, + ) -> Result<(String, String), NymRewarderError> { + let tx = self.inner.read().await.get_tx(tx_hash).await?; + + // todo: we need to make it more concrete that the first attribute is the deposit value + // and the second one is the deposit info + let deposit_value = + find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_VALUE) + .ok_or(NymRewarderError::DepositValueNotFound { tx_hash })?; + + let deposit_info = + find_tx_attribute(&tx, COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO) + .ok_or(NymRewarderError::DepositInfoNotFound { tx_hash })?; + + Ok((deposit_value, deposit_info)) + } +} diff --git a/nym-validator-rewarder/src/rewarder/storage/manager.rs b/nym-validator-rewarder/src/rewarder/storage/manager.rs new file mode 100644 index 0000000000..5d5ab191f8 --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/storage/manager.rs @@ -0,0 +1,178 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::rewarder::epoch::Epoch; + +#[derive(Clone)] +pub(crate) struct StorageManager { + pub(crate) connection_pool: sqlx::SqlitePool, +} + +impl StorageManager { + pub(crate) async fn load_last_rewarding_epoch(&self) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT id, start_time, end_time + FROM rewarding_epoch + ORDER BY id DESC + LIMIT 1 + "#, + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_rewarding_epoch( + &self, + epoch: Epoch, + rewarding_budget: String, + total_spent: String, + rewarding_tx: Option, + rewarding_error: Option, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO rewarding_epoch (id, start_time, end_time, budget, spent, rewarding_tx, rewarding_error) + VALUES (?, ?, ? ,?, ?, ?, ?) + "#, + epoch.id, + epoch.start_time, + epoch.end_time, + rewarding_budget, + total_spent: String, + rewarding_tx, + rewarding_error + ).execute(&self.connection_pool).await?; + + Ok(()) + } + + pub(crate) async fn insert_rewarding_epoch_block_signing( + &self, + epoch: i64, + total_voting_power_at_epoch_start: i64, + num_blocks: i64, + budget: String, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO epoch_block_signing (rewarding_epoch_id, total_voting_power_at_epoch_start, num_blocks, budget) + VALUES (?, ?, ?, ?) + "#, + epoch, + total_voting_power_at_epoch_start, + num_blocks, + budget, + ).execute(&self.connection_pool).await?; + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn insert_rewarding_epoch_block_signing_reward( + &self, + epoch: i64, + consensus_address: String, + operator_account: String, + amount: String, + voting_power: i64, + voting_power_share: String, + signed_blocks: i32, + signed_blocks_percent: String, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO block_signing_reward ( + rewarding_epoch_id, + validator_consensus_address, + operator_account, + amount, + voting_power, + voting_power_share, + signed_blocks, + signed_blocks_percent + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + "#, + epoch, + consensus_address, + operator_account, + amount, + voting_power, + voting_power_share, + signed_blocks, + signed_blocks_percent, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_rewarding_epoch_credential_issuance( + &self, + epoch: i64, + starting_dkg_epoch: u32, + ending_dkg_epoch: u32, + total_issued_partial_credentials: u32, + budget: String, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO epoch_credential_issuance ( + rewarding_epoch_id, + starting_dkg_epoch, + ending_dkg_epoch, + total_issued_partial_credentials, + budget + ) + VALUES (?, ?, ?, ?, ?) + "#, + epoch, + starting_dkg_epoch, + ending_dkg_epoch, + total_issued_partial_credentials, + budget, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn insert_rewarding_epoch_credential_issuance_reward( + &self, + epoch: i64, + operator_account: String, + amount: String, + api_endpoint: String, + issued_partial_credentials: u32, + issued_credentials_share: String, + validated_issued_credentials: u32, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO credential_issuance_reward ( + rewarding_epoch_id, + operator_account, + amount, + api_endpoint, + issued_partial_credentials, + issued_credentials_share, + validated_issued_credentials + ) VALUES (?, ?, ?, ?, ?, ?, ?) + "#, + epoch, + operator_account, + amount, + api_endpoint, + issued_partial_credentials, + issued_credentials_share, + validated_issued_credentials, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } +} diff --git a/nym-validator-rewarder/src/rewarder/storage/mod.rs b/nym-validator-rewarder/src/rewarder/storage/mod.rs new file mode 100644 index 0000000000..aada9ef5dd --- /dev/null +++ b/nym-validator-rewarder/src/rewarder/storage/mod.rs @@ -0,0 +1,167 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymRewarderError; +use crate::rewarder::epoch::Epoch; +use crate::rewarder::storage::manager::StorageManager; +use crate::rewarder::EpochRewards; +use nym_validator_client::nyxd::{Coin, Hash}; +use sqlx::ConnectOptions; +use std::fmt::Debug; +use std::path::Path; +use tracing::{error, info, instrument}; + +mod manager; + +pub struct RewarderStorage { + pub(crate) manager: StorageManager, +} + +impl RewarderStorage { + #[instrument] + pub async fn init + Debug>(database_path: P) -> Result { + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true); + + // TODO: do we want auto_vacuum ? + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to initialize SQLx database: {err}"); + return Err(err.into()); + } + + info!("Database migration finished!"); + + let manager = StorageManager { connection_pool }; + let storage = RewarderStorage { manager }; + + Ok(storage) + } + + pub(crate) async fn load_last_rewarding_epoch( + &self, + ) -> Result, NymRewarderError> { + Ok(self.manager.load_last_rewarding_epoch().await?) + } + + pub(crate) async fn save_rewarding_information( + &self, + reward: EpochRewards, + total_spent: Coin, + rewarding_tx: Result, + ) -> Result<(), NymRewarderError> { + info!("persisting reward details"); + let (reward_tx, reward_err) = match rewarding_tx { + Ok(hash) => (Some(hash.to_string()), None), + Err(err) => (None, Some(err.to_string())), + }; + + let epoch_id = reward.epoch.id; + + self.manager + .insert_rewarding_epoch( + reward.epoch, + reward.total_budget.to_string(), + total_spent.to_string(), + reward_tx, + reward_err, + ) + .await?; + + self.manager + .insert_rewarding_epoch_block_signing( + epoch_id, + reward + .signing + .as_ref() + .map(|s| s.total_voting_power_at_epoch_start) + .unwrap_or_default(), + reward + .signing + .as_ref() + .map(|s| s.blocks) + .unwrap_or_default(), + reward.signing_budget.to_string(), + ) + .await?; + + if let Some(signing) = reward.signing { + for validator in signing.validators { + let reward_amount = validator.reward_amount(&reward.signing_budget).to_string(); + self.manager + .insert_rewarding_epoch_block_signing_reward( + epoch_id, + validator.validator.consensus_address, + validator.operator_account.to_string(), + reward_amount, + validator.voting_power_at_epoch_start, + validator.voting_power_ratio.to_string(), + validator.signed_blocks, + validator.ratio_signed.to_string(), + ) + .await?; + } + } + + // safety: we must have at least a single value here + #[allow(clippy::unwrap_used)] + let dkg_epoch_start = reward + .credentials + .as_ref() + .and_then(|c| c.dkg_epochs.first().copied()) + .unwrap_or_default(); + #[allow(clippy::unwrap_used)] + let dkg_epoch_end = reward + .credentials + .as_ref() + .and_then(|c| c.dkg_epochs.last().copied()) + .unwrap_or_default(); + + self.manager + .insert_rewarding_epoch_credential_issuance( + epoch_id, + dkg_epoch_start, + dkg_epoch_end, + reward + .credentials + .as_ref() + .map(|c| c.total_issued_partial_credentials) + .unwrap_or_default(), + reward.credentials_budget.to_string(), + ) + .await?; + + if let Some(credentials) = reward.credentials { + for api_runner in credentials.api_runners { + let reward_amount = api_runner + .reward_amount(&reward.credentials_budget) + .to_string(); + + self.manager + .insert_rewarding_epoch_credential_issuance_reward( + epoch_id, + api_runner.runner_account.to_string(), + reward_amount, + api_runner.api_runner, + api_runner.issued_credentials, + api_runner.issued_ratio.to_string(), + api_runner.validated_credentials, + ) + .await?; + } + } + + Ok(()) + } +} 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 diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 89cfcaf991..d35a3c4c6a 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -808,6 +808,16 @@ dependencies = [ "tendermint-proto", ] +[[package]] +name = "cosmos-sdk-proto" +version = "0.20.0" +source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#67718aa27a96efb9881e927a8f998c94b885093c" +dependencies = [ + "prost", + "prost-types", + "tendermint-proto", +] + [[package]] name = "cosmrs" version = "0.15.0" @@ -815,7 +825,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" dependencies = [ "bip32", - "cosmos-sdk-proto", + "cosmos-sdk-proto 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ecdsa 0.16.8", + "eyre", + "k256 0.13.1", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature 2.1.0", + "subtle-encoding", + "tendermint", + "thiserror", +] + +[[package]] +name = "cosmrs" +version = "0.15.0" +source = "git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features#67718aa27a96efb9881e927a8f998c94b885093c" +dependencies = [ + "bip32", + "cosmos-sdk-proto 0.20.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "ecdsa 0.16.8", "eyre", "k256 0.13.1", @@ -3127,7 +3156,7 @@ name = "nym-api-requests" version = "0.1.0" dependencies = [ "bs58 0.4.0", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "cosmwasm-std", "getset", "nym-coconut-interface", @@ -3422,7 +3451,7 @@ name = "nym-types" version = "1.0.0" dependencies = [ "base64 0.21.4", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "cosmwasm-std", "eyre", "hmac 0.12.1", @@ -3455,7 +3484,7 @@ dependencies = [ "bip32", "bip39", "colored 2.0.4", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "cosmwasm-std", "cw-controllers", "cw-utils", @@ -3527,7 +3556,7 @@ dependencies = [ name = "nym-wallet-types" version = "1.0.0" dependencies = [ - "cosmrs", + "cosmrs 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "cosmwasm-std", "hex-literal", "nym-config", @@ -3564,7 +3593,7 @@ dependencies = [ "bip39", "cfg-if", "colored 2.0.4", - "cosmrs", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", "cosmwasm-std", "dirs 4.0.0", "dotenvy", diff --git a/nym-wallet/nym-wallet-types/src/network/qa.rs b/nym-wallet/nym-wallet-types/src/network/qa.rs index f036b51c4d..85aa00bd5e 100644 --- a/nym-wallet/nym-wallet-types/src/network/qa.rs +++ b/nym-wallet/nym-wallet-types/src/network/qa.rs @@ -35,6 +35,7 @@ pub(crate) fn validators() -> Vec { vec![ValidatorDetails::new( "https://qa-validator.qa.nymte.ch/", Some("https://qa-nym-api.qa.nymte.ch/api"), + Some("wss://qa-validator.qa.nymte.ch/websocket"), )] } diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index 1186c07eac..2d1bc9e0fa 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -32,6 +32,7 @@ pub(crate) fn validators() -> Vec { vec![ValidatorDetails::new( "https://rpc.sandbox.nymtech.net", Some("https://sandbox-nym-api1.nymtech.net/api"), + Some("wss://rpc.sandbox.nymtech.net/websocket"), )] } diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 1182ef2d1c..854aa1d57f 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -49,7 +49,7 @@ base64 = "0.13" zeroize = { version = "1.5", features = ["zeroize_derive", "serde"] } cosmwasm-std = "1.3.0" -cosmrs = "=0.15.0" +cosmrs = { git = "https://github.com/jstuczyn/cosmos-rust", branch ="nym-temp/all-validator-features" } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } diff --git a/service-providers/network-statistics/src/api/mod.rs b/service-providers/network-statistics/src/api/mod.rs index 74e47c7108..fc32cc8d7b 100644 --- a/service-providers/network-statistics/src/api/mod.rs +++ b/service-providers/network-statistics/src/api/mod.rs @@ -35,7 +35,7 @@ impl NetworkStatisticsAPI { pub async fn run(self) { let rocket_shutdown_handle = self.rocket.shutdown(); - let shutdown = TaskManager::new(10); + let mut shutdown = TaskManager::new(10); tokio::spawn(self.rocket.launch()); shutdown.catch_interrupt().await.ok();