Merge pull request #4346 from nymtech/feature/rewarder-whitelist

Feature/rewarder whitelist
This commit is contained in:
Jędrzej Stuczyński
2024-01-30 10:08:21 +00:00
committed by GitHub
13 changed files with 159 additions and 46 deletions
@@ -27,6 +27,7 @@ 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,
whitelisted BOOLEAN NOT NULL,
amount TEXT NOT NULL,
voting_power BIGINT NOT NULL,
voting_power_share TEXT NOT NULL,
@@ -38,11 +39,11 @@ CREATE TABLE block_signing_reward
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,
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
budget TEXT NOT NULL
);
CREATE TABLE malformed_credential
@@ -56,6 +57,7 @@ CREATE TABLE credential_issuance_reward
rewarding_epoch_id INTEGER NOT NULL REFERENCES rewarding_epoch (id),
operator_account TEXT NOT NULL,
amount TEXT NOT NULL,
whitelisted BOOLEAN NOT NULL,
api_endpoint TEXT NOT NULL,
issued_partial_credentials INTEGER NOT NULL,
issued_credentials_share TEXT NOT NULL,
+18 -6
View File
@@ -9,7 +9,7 @@ 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 nym_validator_client::nyxd::{AccountId, Coin};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use std::io;
@@ -225,7 +225,7 @@ impl Default for RewardingRatios {
impl RewardingRatios {
pub fn ensure_is_valid(&self) -> Result<(), NymRewarderError> {
if self.block_signing + self.credential_verification + self.credential_issuance != 1.0 {
todo!()
return Err(NymRewarderError::InvalidRewardingRatios { ratios: *self });
}
Ok(())
}
@@ -238,19 +238,26 @@ pub struct NyxdScraper {
// TODO: debug with everything that's currently hardcoded in the scraper
}
#[derive(Debug, Clone, Deserialize, Serialize, Copy)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BlockSigning {
/// Specifies whether credential issuance for block signing is enabled.
pub enabled: bool,
/// List of validators that will receive rewards for block signing.
/// If not on the list, the validator will be treated as if it had 0 voting power.
pub whitelist: Vec<AccountId>,
}
impl Default for BlockSigning {
fn default() -> Self {
BlockSigning { enabled: true }
BlockSigning {
enabled: true,
whitelist: vec![],
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, Copy)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct IssuanceMonitor {
/// Specifies whether credential issuance monitoring (and associated rewards) are enabled.
pub enabled: bool,
@@ -264,15 +271,20 @@ pub struct IssuanceMonitor {
/// The sampling rate of the issued credentials
pub sampling_rate: f64,
/// List of validators that will receive rewards for credential issuance.
/// If not on the list, the validator will be treated as if it hadn't issued a single credential.
pub whitelist: Vec<AccountId>,
}
impl Default for IssuanceMonitor {
fn default() -> Self {
IssuanceMonitor {
enabled: true,
enabled: false,
run_interval: DEFAULT_MONITOR_RUN_INTERVAL,
min_validate_per_issuer: DEFAULT_MONITOR_MIN_VALIDATE,
sampling_rate: DEFAULT_MONITOR_SAMPLING_RATE,
whitelist: vec![],
}
}
}
+14 -1
View File
@@ -40,7 +40,14 @@ credential_verification = {{ rewarding.ratios.credential_verification }}
[block_signing]
# Specifies whether credential issuance for block signing is enabled.
enabled = {{ block_signing.enabled }}
# List of validators that will receive rewards for block signing.
# If not on the list, the validator will be treated as if it had 0 voting power.
whitelist = [
# needs to be manually populated; expects nvalcons1... addresses.
# you can get them from, for example, `/cosmos/base/tendermint/v1beta1/validatorsets/latest` endpoint
]
[issuance_monitor]
# Specifies whether credential issuance monitoring (and associated rewards) are enabled.
@@ -54,6 +61,12 @@ min_validate_per_issuer = {{ issuance_monitor.min_validate_per_issuer }}
# The sampling rate of the issued credentials
sampling_rate = {{ issuance_monitor.sampling_rate }}
# List of validators that will receive rewards for credential issuance.
# If not on the list, the validator will be treated as if it hadn't issued a single credential.
whitelist = [
# needs to be manually populated; expects n1... addresses
]
[nyxd_scraper]
# Url to the websocket endpoint of a validator, for example `wss://rpc.nymtech.net/websocket`
+8 -1
View File
@@ -1,6 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::config::RewardingRatios;
use nym_coconut::CoconutError;
use nym_validator_client::nym_api::error::NymAPIError;
use nym_validator_client::nyxd::error::NyxdError;
@@ -50,7 +51,7 @@ pub enum NymRewarderError {
NyxdFailure(#[from] NyxdError),
#[error("the provided rewarding ratios don't add up to 1. ratios: {ratios:?}")]
InvalidRewardingRatios { ratios: Vec<f32> },
InvalidRewardingRatios { ratios: RewardingRatios },
#[error("chain scraping failure: {source}")]
ScraperFailure {
@@ -156,6 +157,12 @@ pub enum NymRewarderError {
#[error("the scraper websocket endpoint hasn't been provided")]
UnavailableWebsocketUrl,
#[error("block signing rewarding is enabled, but the validator whitelist is empty")]
EmptyBlockSigningWhitelist,
#[error("credential issuance rewarding is enabled, but the validator whitelist is empty")]
EmptyCredentialIssuanceWhitelist,
}
#[derive(Debug)]
+3 -1
View File
@@ -1,8 +1,10 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::todo)]
#![warn(clippy::dbg_macro)]
use crate::cli::Cli;
use clap::{crate_name, crate_version, Parser};
@@ -6,18 +6,19 @@ use crate::rewarder::block_signing::types::{EpochSigningResults, RawValidatorRes
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 nym_validator_client::nyxd::{AccountId, PageRequest};
use nyxd_scraper::NyxdScraper;
use std::cmp::min;
use std::collections::HashMap;
use std::ops::Range;
use tracing::info;
use tracing::{debug, info, warn};
pub(crate) mod types;
pub struct EpochSigning {
pub(crate) nyxd_client: NyxdClient,
pub(crate) nyxd_scraper: NyxdScraper,
pub(crate) whitelist: Vec<AccountId>,
}
impl EpochSigning {
@@ -116,14 +117,29 @@ impl EpochSigning {
else {
continue;
};
total_vp += vp;
let cons_address = &validator.consensus_address;
// if this validator is NOT whitelisted, do not increase the total VP
let whitelisted = if let Ok(parsed) = cons_address.parse() {
if self.whitelist.contains(&parsed) {
debug!("{cons_address} is on the whitelist");
total_vp += vp;
true
} else {
warn!("{cons_address} is not a valid consensus address");
false
}
} else {
debug!("{cons_address} is not on the whitelist");
false
};
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));
signed_in_epoch.insert(validator, RawValidatorResult::new(signed, vp, whitelisted));
}
let total = self
@@ -15,6 +15,7 @@ pub struct ValidatorSigning {
pub validator: models::Validator,
pub staking_details: staking::Validator,
pub operator_account: AccountId,
pub whitelisted: bool,
pub voting_power_at_epoch_start: i64,
pub voting_power_ratio: Decimal,
@@ -33,6 +34,10 @@ impl ValidatorSigning {
}
pub fn reward_amount(&self, signing_budget: &Coin) -> Coin {
if !self.whitelisted {
return Coin::new(0, &signing_budget.denom);
}
let amount =
Uint128::new(signing_budget.amount) * self.ratio_signed * self.voting_power_ratio;
@@ -51,13 +56,15 @@ pub struct EpochSigningResults {
pub struct RawValidatorResult {
pub signed_blocks: i32,
pub voting_power: i64,
pub whitelisted: bool,
}
impl RawValidatorResult {
pub fn new(signed_blocks: i32, voting_power: i64) -> Self {
pub fn new(signed_blocks: i32, voting_power: i64, whitelisted: bool) -> Self {
Self {
signed_blocks,
voting_power,
whitelisted,
}
}
}
@@ -93,7 +100,11 @@ impl EpochSigningResults {
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);
let voting_power_ratio = if raw_results.whitelisted {
Decimal::from_ratio(vp, total_vp_u64)
} else {
Decimal::zero()
};
debug_assert!(signed <= blocks_u64);
let ratio_signed = Decimal::from_ratio(signed, blocks_u64);
@@ -110,6 +121,7 @@ impl EpochSigningResults {
validator,
staking_details,
operator_account,
whitelisted: raw_results.whitelisted,
voting_power_at_epoch_start: raw_results.voting_power,
voting_power_ratio,
signed_blocks: raw_results.signed_blocks,
@@ -129,10 +141,11 @@ impl EpochSigningResults {
.iter()
.inspect(|v| {
info!(
"validator {} will receive {} at address {} for block signing work",
"validator {} will receive {} at address {} for block signing work (whitelisted: {})",
v.moniker(),
v.reward_amount(budget),
v.operator_account
v.operator_account,
v.whitelisted
);
})
.map(|v| (v.operator_account.clone(), vec![v.reward_amount(budget)]))
@@ -8,6 +8,7 @@ use crate::rewarder::credential_issuance::types::{CredentialIssuanceResults, Mon
use crate::rewarder::epoch::Epoch;
use crate::rewarder::nyxd_client::NyxdClient;
use nym_task::TaskClient;
use nym_validator_client::nyxd::AccountId;
use tracing::info;
mod monitor;
@@ -21,9 +22,11 @@ impl CredentialIssuance {
pub(crate) async fn new(
epoch: Epoch,
nyxd_client: &NyxdClient,
whitelist: &[AccountId],
) -> Result<Self, NymRewarderError> {
Ok(CredentialIssuance {
monitoring_results: MonitoringResults::new_initial(epoch, nyxd_client).await?,
monitoring_results: MonitoringResults::new_initial(epoch, nyxd_client, whitelist)
.await?,
})
}
@@ -235,12 +235,15 @@ impl CredentialIssuanceMonitor {
let api_client = api_client(&issuer)?;
let epoch_credentials = api_client.epoch_credentials(epoch_id).await?;
let whitelisted = self.config.whitelist.contains(&issuer.operator_account);
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,
whitelisted,
));
};
trace!("issued credentials: {epoch_credentials:?}");
@@ -283,6 +286,7 @@ impl CredentialIssuanceMonitor {
Ok(RawOperatorResult {
operator_account: issuer.operator_account,
api_runner: issuer.api_runner,
whitelisted,
issued_credentials: epoch_credentials.total_issued,
validated_credentials: sampled,
})
@@ -25,6 +25,7 @@ impl MonitoringResults {
pub(crate) async fn new_initial(
initial_epoch: Epoch,
nyxd_client: &NyxdClient,
whitelist: &[AccountId],
) -> Result<Self, NymRewarderError> {
let epoch = nyxd_client.dkg_epoch().await?;
let issuers = nyxd_client.get_credential_issuers(epoch.epoch_id).await?;
@@ -36,6 +37,7 @@ impl MonitoringResults {
let mut raw_issuer = RawOperatorIssuing {
api_runner: issuer.api_runner.clone(),
runner_account: issuer.operator_account.clone(),
whitelisted: whitelist.contains(&issuer.operator_account),
per_epoch: Default::default(),
};
@@ -132,6 +134,7 @@ impl MonitoringResults {
RawOperatorIssuing {
api_runner: result.api_runner.clone(),
runner_account: result.runner_account.clone(),
whitelisted: result.whitelisted,
per_epoch: kept_epoch,
},
);
@@ -157,18 +160,20 @@ pub(crate) struct MonitoringResultsInner {
impl From<MonitoringResultsInner> for CredentialIssuanceResults {
fn from(value: MonitoringResultsInner) -> Self {
let total_issued = value
.operators
.values()
.map(|o| {
let operator_issued: u32 = o
let mut total_issued = 0;
for operator in value.operators.values() {
// if this validator is NOT whitelisted, do not increase the total issued credentials
if operator.whitelisted {
let operator_issued: u32 = operator
.per_epoch
.values()
.map(|e| e.issued_since_monitor_started)
.sum();
operator_issued
})
.sum();
total_issued += operator_issued
}
}
CredentialIssuanceResults {
total_issued_partial_credentials: total_issued,
@@ -177,7 +182,7 @@ impl From<MonitoringResultsInner> for CredentialIssuanceResults {
.operators
.into_values()
.map(|runner| {
let issued_ratio = if total_issued == 0 {
let issued_ratio = if total_issued == 0 || !runner.whitelisted {
Decimal::zero()
} else {
Decimal::from_ratio(runner.issued_credentials(), total_issued)
@@ -187,6 +192,7 @@ impl From<MonitoringResultsInner> for CredentialIssuanceResults {
issued_credentials: runner.issued_credentials(),
validated_credentials: runner.validated_credentials(),
api_runner: runner.api_runner,
whitelisted: runner.whitelisted,
runner_account: runner.runner_account,
}
})
@@ -212,6 +218,7 @@ impl MonitoringResultsInner {
pub(crate) struct RawOperatorResult {
pub(crate) operator_account: AccountId,
pub(crate) api_runner: String,
pub(crate) whitelisted: bool,
// how many credentials the operator claims to have issued in **TOTAL** in this **DKG** epoch
pub(crate) issued_credentials: u32,
@@ -219,10 +226,15 @@ pub(crate) struct RawOperatorResult {
}
impl RawOperatorResult {
pub(crate) fn new_empty(operator_account: AccountId, api_runner: String) -> RawOperatorResult {
pub(crate) fn new_empty(
operator_account: AccountId,
api_runner: String,
whitelisted: bool,
) -> RawOperatorResult {
RawOperatorResult {
operator_account,
api_runner,
whitelisted,
issued_credentials: 0,
validated_credentials: Default::default(),
}
@@ -232,6 +244,7 @@ impl RawOperatorResult {
pub struct RawOperatorIssuing {
pub api_runner: String,
pub runner_account: AccountId,
pub whitelisted: bool,
pub per_epoch: HashMap<u32, IssuedEpochCredentials>,
}
@@ -243,6 +256,7 @@ impl RawOperatorIssuing {
RawOperatorIssuing {
api_runner: raw_result.api_runner,
runner_account: raw_result.operator_account,
whitelisted: raw_result.whitelisted,
per_epoch,
}
}
@@ -278,6 +292,7 @@ impl IssuedEpochCredentials {
pub struct OperatorIssuing {
pub api_runner: String,
pub whitelisted: bool,
pub runner_account: AccountId,
pub issued_ratio: Decimal,
@@ -286,10 +301,14 @@ pub struct OperatorIssuing {
}
impl OperatorIssuing {
pub fn reward_amount(&self, signing_budget: &Coin) -> Coin {
let amount = Uint128::new(signing_budget.amount) * self.issued_ratio;
pub fn reward_amount(&self, issuance_budget: &Coin) -> Coin {
if !self.whitelisted {
return Coin::new(0, &issuance_budget.denom);
}
Coin::new(amount.u128(), &signing_budget.denom)
let amount = Uint128::new(issuance_budget.amount) * self.issued_ratio;
Coin::new(amount.u128(), &issuance_budget.denom)
}
}
@@ -305,10 +324,11 @@ impl CredentialIssuanceResults {
.iter()
.inspect(|a| {
info!(
"operator {} will receive {} at address {} for credential issuance work",
"operator {} will receive {} at address {} for credential issuance work (whitelisted: {})",
a.api_runner,
a.reward_amount(budget),
a.runner_account,
a.whitelisted
);
})
.map(|v| (v.runner_account.clone(), vec![v.reward_amount(budget)]))
+21 -8
View File
@@ -41,17 +41,19 @@ impl EpochRewards {
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)
for (account, signing_amount) in signing.rewarding_amounts(&self.signing_budget) {
if signing_amount[0].amount != 0 {
amounts.push((account, 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)
for (account, credential_amount) in
credentials.rewarding_amounts(&self.credentials_budget)
{
if credential_amount[0].amount != 0 {
amounts.push((account, credential_amount))
}
}
}
@@ -86,18 +88,29 @@ impl Rewarder {
};
let epoch_signing = if config.block_signing.enabled {
let whitelist = config.block_signing.whitelist.clone();
if whitelist.is_empty() {
return Err(NymRewarderError::EmptyBlockSigningWhitelist);
}
let nyxd_scraper = NyxdScraper::new(config.scraper_config()).await?;
Some(EpochSigning {
nyxd_scraper,
nyxd_client: nyxd_client.clone(),
whitelist,
})
} else {
None
};
let credential_issuance = if config.issuance_monitor.enabled {
Some(CredentialIssuance::new(current_epoch, &nyxd_client).await?)
let whitelist = &config.issuance_monitor.whitelist;
if whitelist.is_empty() {
return Err(NymRewarderError::EmptyCredentialIssuanceWhitelist);
}
Some(CredentialIssuance::new(current_epoch, &nyxd_client, whitelist).await?)
} else {
None
};
@@ -239,7 +252,7 @@ impl Rewarder {
if let Some(ref credential_issuance) = self.credential_issuance {
credential_issuance.start_monitor(
self.config.issuance_monitor,
self.config.issuance_monitor.clone(),
self.nyxd_client.clone(),
task_manager.subscribe(),
);
@@ -74,6 +74,7 @@ impl StorageManager {
epoch: i64,
consensus_address: String,
operator_account: String,
whitelisted: bool,
amount: String,
voting_power: i64,
voting_power_share: String,
@@ -86,16 +87,18 @@ impl StorageManager {
rewarding_epoch_id,
validator_consensus_address,
operator_account,
whitelisted,
amount,
voting_power,
voting_power_share,
signed_blocks,
signed_blocks_percent
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
epoch,
consensus_address,
operator_account,
whitelisted,
amount,
voting_power,
voting_power_share,
@@ -144,6 +147,7 @@ impl StorageManager {
&self,
epoch: i64,
operator_account: String,
whitelisted: bool,
amount: String,
api_endpoint: String,
issued_partial_credentials: u32,
@@ -155,15 +159,17 @@ impl StorageManager {
INSERT INTO credential_issuance_reward (
rewarding_epoch_id,
operator_account,
whitelisted,
amount,
api_endpoint,
issued_partial_credentials,
issued_credentials_share,
validated_issued_credentials
) VALUES (?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"#,
epoch,
operator_account,
whitelisted,
amount,
api_endpoint,
issued_partial_credentials,
@@ -104,6 +104,7 @@ impl RewarderStorage {
epoch_id,
validator.validator.consensus_address,
validator.operator_account.to_string(),
validator.whitelisted,
reward_amount,
validator.voting_power_at_epoch_start,
validator.voting_power_ratio.to_string(),
@@ -152,6 +153,7 @@ impl RewarderStorage {
.insert_rewarding_epoch_credential_issuance_reward(
epoch_id,
api_runner.runner_account.to_string(),
api_runner.whitelisted,
reward_amount,
api_runner.api_runner,
api_runner.issued_credentials,