Feature/rewarder voucher issuance (#4548)

* retrieve ed25519 identities of issuers

* signature verification on issued credentials

* wip

* persisting information about verified deposits, any failures and foul plays

* clippy
This commit is contained in:
Jędrzej Stuczyński
2024-05-16 09:15:24 +01:00
committed by GitHub
parent 1ded24dcfc
commit a6fda391ae
9 changed files with 418 additions and 109 deletions
@@ -0,0 +1,50 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
CREATE TABLE validated_deposit
(
operator_identity_bs58 TEXT NOT NULL,
credential_id INTEGER NOT NULL,
deposit_tx TEXT NOT NULL,
signed_plaintext BLOB NOT NULL,
signature_bs58 TEXT NOT NULL
);
-- evidence of attempting to re-use the same deposit tx for multiple credentials
CREATE TABLE double_signing_evidence
(
operator_identity_bs58 TEXT NOT NULL,
credential_id INTEGER NOT NULL,
original_credential_id INTEGER NOT NULL,
deposit_tx TEXT NOT NULL,
signed_plaintext BLOB NOT NULL,
signature_bs58 TEXT NOT NULL
);
-- evidence of foul play
CREATE TABLE issuance_evidence
(
operator_account TEXT NOT NULL,
operator_identity_bs58 TEXT NOT NULL,
credential_id INTEGER NOT NULL,
signed_plaintext BLOB NOT NULL,
signature_bs58 TEXT NOT NULL,
failure_message TEXT NOT NULL
);
-- does not necessarily imply foul play, but something has gone wrong
CREATE TABLE issuance_validation_failure
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
operator_account TEXT NOT NULL,
operator_identity_bs58 TEXT NOT NULL,
failure_message TEXT NOT NULL
)
+10
View File
@@ -3,6 +3,7 @@
use crate::config::RewardingRatios;
use nym_coconut::CoconutError;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nym_api::error::NymAPIError;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::tx::ErrorReport;
@@ -98,6 +99,12 @@ pub enum NymRewarderError {
source: url::ParseError,
},
#[error("the provided ed25519 identity key is malformed: {source}")]
MalformedIdentityKey {
#[from]
source: ed25519::Ed25519RecoveryError,
},
#[error("failed to resolve nym-api query: {0}")]
ApiQueryFailure(#[from] NymAPIError),
@@ -122,6 +129,9 @@ pub enum NymRewarderError {
source: CoconutError,
},
#[error("the signature on issued credential with id {credential_id} is invalid")]
SignatureVerificationFailure { credential_id: i64 },
#[error("could not verify the blinded credential")]
BlindVerificationFailure,
@@ -7,6 +7,7 @@ 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 crate::rewarder::storage::RewarderStorage;
use nym_task::TaskClient;
use nym_validator_client::nyxd::AccountId;
use tracing::info;
@@ -16,17 +17,20 @@ pub mod types;
pub struct CredentialIssuance {
monitoring_results: MonitoringResults,
storage: RewarderStorage,
}
impl CredentialIssuance {
pub(crate) async fn new(
epoch: Epoch,
storage: RewarderStorage,
nyxd_client: &NyxdClient,
whitelist: &[AccountId],
) -> Result<Self, NymRewarderError> {
Ok(CredentialIssuance {
monitoring_results: MonitoringResults::new_initial(epoch, nyxd_client, whitelist)
.await?,
storage,
})
}
@@ -37,8 +41,12 @@ impl CredentialIssuance {
task_client: TaskClient,
) {
let monitoring_results = self.monitoring_results.clone();
let mut monitor =
CredentialIssuanceMonitor::new(monitor_config, nyxd_client, monitoring_results);
let mut monitor = CredentialIssuanceMonitor::new(
monitor_config,
nyxd_client,
self.storage.clone(),
monitoring_results,
);
tokio::spawn(async move { monitor.run(task_client).await });
}
@@ -8,6 +8,7 @@ use crate::rewarder::credential_issuance::types::{
};
use crate::rewarder::helpers::api_client;
use crate::rewarder::nyxd_client::NyxdClient;
use crate::rewarder::storage::RewarderStorage;
use bip39::rand::prelude::SliceRandom;
use bip39::rand::thread_rng;
use nym_coconut::{
@@ -15,75 +16,88 @@ use nym_coconut::{
};
use nym_coconut_dkg_common::types::EpochId;
use nym_credentials::coconut::bandwidth::bandwidth_credential_params;
use nym_crypto::asymmetric::ed25519;
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 tokio::time::interval;
use tracing::{debug, error, info, instrument, trace, warn};
use tracing::{debug, error, info, instrument, trace};
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<String, HashMap<Hash, i64>>,
storage: RewarderStorage,
}
impl CredentialIssuanceMonitor {
pub fn new(
config: config::IssuanceMonitor,
nyxd_client: NyxdClient,
storage: RewarderStorage,
monitoring_results: MonitoringResults,
) -> CredentialIssuanceMonitor {
CredentialIssuanceMonitor {
config,
storage,
nyxd_client,
monitoring_results,
seen_deposits: Default::default(),
}
}
fn validate_credential_signature(
&mut self,
_issued_credential: &IssuedCredentialBody,
issued_credential: &IssuedCredentialBody,
identity_key: &ed25519::PublicKey,
) -> Result<(), NymRewarderError> {
warn!("unimplemented: public key sharing mechanism");
// let plaintext = issued_credential.credential.signable_plaintext();
// if !operator_public_key.verify(&plaintext) {
// ...
// }
Ok(())
let plaintext = issued_credential.credential.signable_plaintext();
if identity_key
.verify(plaintext, &issued_credential.signature)
.is_err()
{
Err(NymRewarderError::SignatureVerificationFailure {
credential_id: issued_credential.credential.id,
})
} else {
Ok(())
}
}
fn check_deposit_reuse(
async fn check_deposit_reuse(
&mut self,
runner: &str,
credential_id: i64,
deposit_tx: Hash,
issuer_identity: &str,
credential_info: &IssuedCredentialBody,
) -> Result<bool, NymRewarderError> {
// 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 {
let credential_id = credential_info.credential.id;
let deposit_tx = credential_info.credential.tx_hash;
let prior_id = self
.storage
.get_deposit_credential_id(issuer_identity.to_string(), deposit_tx.to_string())
.await?;
match prior_id {
None => Ok(false),
Some(prior) => {
if prior == credential_id {
debug!("we have already verified this credential before");
Ok(true)
};
} else {
known_runner.insert(deposit_tx, credential_id);
} else {
error!("double signing detected!! used deposit {deposit_tx} for credentials {prior} and {credential_id}!!");
self.storage
.insert_double_signing_evidence(
issuer_identity.to_string(),
prior,
credential_info,
)
.await?;
Err(NymRewarderError::DuplicateDepositHash {
tx_hash: deposit_tx,
first: prior,
other: credential_id,
})
}
}
}
Ok(false)
}
async fn validate_deposit(
@@ -126,7 +140,7 @@ impl CredentialIssuanceMonitor {
fn verify_credential(
&mut self,
vk: &VerificationKey,
credential: IssuedCredential,
credential: &IssuedCredential,
) -> Result<(), NymRewarderError> {
let public_attributes = credential
.public_attributes
@@ -140,12 +154,12 @@ impl CredentialIssuanceMonitor {
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) {
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,
raw: raw_cm.clone(),
source,
})
}
@@ -167,30 +181,34 @@ impl CredentialIssuanceMonitor {
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))]
#[instrument(skip_all, fields(credential_id = %issued_credential.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,
issuer: &CredentialIssuer,
issued_credential: &IssuedCredentialBody,
) -> Result<(), NymRewarderError> {
self.validate_credential_signature(&issued_credential)?;
let deposit_tx = issued_credential.credential.tx_hash;
// check if the issuer has actually signed that issued credential information
self.validate_credential_signature(issued_credential, &issuer.public_key)?;
let encoded_key = issuer.public_key.to_base58_string();
// 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)?;
let already_checked = self
.check_deposit_reuse(&encoded_key, issued_credential)
.await?;
if already_checked {
return Ok(());
}
// check the correctness of the deposit itself
self.validate_deposit(&issued_credential).await?;
self.validate_deposit(issued_credential).await?;
// insert validated deposit info into the storage
self.storage
.insert_validated_deposit(encoded_key, issued_credential)
.await?;
// check if the partial credential correctly verifies
self.verify_credential(vk, issued_credential.credential)?;
self.verify_credential(&issuer.verification_key, &issued_credential.credential)?;
Ok(())
}
@@ -220,12 +238,12 @@ impl CredentialIssuanceMonitor {
async fn check_issuer(
&mut self,
epoch_id: EpochId,
issuer: CredentialIssuer,
issuer: &CredentialIssuer,
) -> Result<RawOperatorResult, NymRewarderError> {
info!("checking the issuer's credentials...");
debug!("checking the issuer's credentials...");
let api_client = api_client(&issuer)?;
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);
@@ -234,8 +252,8 @@ impl CredentialIssuanceMonitor {
// 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,
issuer.operator_account.clone(),
issuer.api_runner.clone(),
whitelisted,
));
};
@@ -248,9 +266,9 @@ impl CredentialIssuanceMonitor {
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
error!("received an incomplete credential request! the issuer **MIGHT** be cheating!! but we're lacking sufficient signatures to be certain");
return Err(NymRewarderError::IncompleteRequest {
runner_account: issuer.operator_account,
runner_account: issuer.operator_account.clone(),
requested: request_size,
received: credentials.credentials.len(),
});
@@ -258,27 +276,21 @@ impl CredentialIssuanceMonitor {
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
{
if let Err(err) = self.validate_issued_credential(issuer, &credential).await {
error!(
"failed to verify credential {id} from {}!!: {err}",
issuer.operator_account
"failed to validate credential {id} from {} ({})!!: {err}",
issuer.public_key, issuer.operator_account
);
self.storage
.insert_issuance_foul_play_evidence(issuer, &credential, err.to_string())
.await?;
return Err(err);
}
}
Ok(RawOperatorResult {
operator_account: issuer.operator_account,
api_runner: issuer.api_runner,
operator_account: issuer.operator_account.clone(),
api_runner: issuer.api_runner.clone(),
whitelisted,
issued_credentials: epoch_credentials.total_issued,
validated_credentials: sampled,
@@ -296,14 +308,16 @@ impl CredentialIssuanceMonitor {
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 {
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}")
let address = &issuer.operator_account;
error!("failed to check credential issuance of {address}: {err}");
self.storage
.insert_issuance_validation_failure_info(&issuer, err.to_string())
.await?;
}
}
}
@@ -6,8 +6,8 @@ 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_coconut::VerificationKey;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::nyxd::{AccountId, Coin};
use std::collections::{HashMap, HashSet};
@@ -338,32 +338,15 @@ impl CredentialIssuanceResults {
#[derive(Debug)]
pub struct CredentialIssuer {
// pub public_key: identity::PublicKey,
pub public_key: ed25519::PublicKey,
pub operator_account: AccountId,
pub api_runner: String,
pub verification_key: VerificationKey,
}
impl TryFrom<ContractVKShare> for CredentialIssuer {
type Error = NymRewarderError;
fn try_from(value: ContractVKShare) -> Result<Self, Self::Error> {
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 {
pub(crate) fn addr_to_account_id(addr: Addr) -> AccountId {
#[allow(clippy::unwrap_used)]
addr.as_str().parse().unwrap()
}
+4 -1
View File
@@ -121,7 +121,10 @@ impl Rewarder {
return Err(NymRewarderError::EmptyCredentialIssuanceWhitelist);
}
Some(CredentialIssuance::new(current_epoch, &nyxd_client, whitelist).await?)
Some(
CredentialIssuance::new(current_epoch, storage.clone(), &nyxd_client, whitelist)
.await?,
)
} else {
None
};
@@ -3,11 +3,13 @@
use crate::config::Config;
use crate::error::NymRewarderError;
use crate::rewarder::credential_issuance::types::CredentialIssuer;
use crate::rewarder::credential_issuance::types::{addr_to_account_id, CredentialIssuer};
use nym_coconut::{Base58, VerificationKey};
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_crypto::asymmetric::ed25519;
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use nym_validator_client::nyxd::helpers::find_tx_attribute;
@@ -18,6 +20,7 @@ use nym_validator_client::nyxd::{
AccountId, Coin, CosmWasmClient, Hash, PageRequest, StakingQueryClient,
};
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -92,14 +95,32 @@ impl NyxdClient {
&self,
dkg_epoch: u64,
) -> Result<Vec<CredentialIssuer>, NymRewarderError> {
self.inner
.read()
.await
.get_all_verification_key_shares(dkg_epoch)
.await?
.into_iter()
.map(TryInto::try_into)
.collect()
let guard = self.inner.read().await;
let mut dealers_map = HashMap::new();
let dealers = guard.get_all_current_dealers().await?;
for dealer in dealers {
dealers_map.insert(dealer.address.to_string(), dealer);
}
let vk_shares = guard.get_all_verification_key_shares(dkg_epoch).await?;
let mut issuers = Vec::with_capacity(vk_shares.len());
for share in vk_shares {
if let Some(info) = dealers_map.remove(&share.owner.to_string()) {
issuers.push(CredentialIssuer {
public_key: ed25519::PublicKey::from_base58_string(&info.ed25519_identity)?,
operator_account: addr_to_account_id(share.owner),
api_runner: share.announce_address,
verification_key: VerificationKey::try_from_bs58(share.share).map_err(
|source| NymRewarderError::MalformedPartialVerificationKey {
runner: info.address.to_string(),
source,
},
)?,
})
}
}
Ok(issuers)
}
pub(crate) async fn get_deposit_transaction_attributes(
@@ -181,4 +181,139 @@ impl StorageManager {
Ok(())
}
pub(crate) async fn insert_validated_deposit(
&self,
operator_identity_bs58: String,
credential_id: i64,
deposit_tx: String,
signed_plaintext: Vec<u8>,
signature_bs58: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO validated_deposit (
operator_identity_bs58,
credential_id,
deposit_tx,
signed_plaintext,
signature_bs58
) VALUES (?, ?, ?, ?, ?)
"#,
operator_identity_bs58,
credential_id,
deposit_tx,
signed_plaintext,
signature_bs58
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_deposit_credential_id(
&self,
operator_identity_bs58: String,
deposit_tx: String,
) -> Result<Option<i64>, sqlx::Error> {
Ok(sqlx::query!(
r#"
SELECT credential_id
FROM validated_deposit
WHERE operator_identity_bs58 = ? AND deposit_tx = ?
"#,
operator_identity_bs58,
deposit_tx
)
.fetch_optional(&self.connection_pool)
.await?
.map(|record| record.credential_id))
}
pub(crate) async fn insert_double_signing_evidence(
&self,
operator_identity_bs58: String,
credential_id: i64,
original_credential_id: i64,
deposit_tx: String,
signed_plaintext: Vec<u8>,
signature_bs58: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO double_signing_evidence (
operator_identity_bs58,
credential_id,
original_credential_id,
deposit_tx,
signed_plaintext,
signature_bs58
) VALUES (?, ?, ?, ?, ?, ?)
"#,
operator_identity_bs58,
credential_id,
original_credential_id,
deposit_tx,
signed_plaintext,
signature_bs58
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn insert_foul_play_evidence(
&self,
operator_account: String,
operator_identity_bs58: String,
credential_id: i64,
signed_plaintext: Vec<u8>,
signature_bs58: String,
failure_message: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO issuance_evidence (
operator_account,
operator_identity_bs58,
credential_id,
signed_plaintext,
signature_bs58,
failure_message
) VALUES (?, ?, ?, ?, ?, ?)
"#,
operator_account,
operator_identity_bs58,
credential_id,
signed_plaintext,
signature_bs58,
failure_message,
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn insert_validation_failure_info(
&self,
operator_account: String,
operator_identity_bs58: String,
failure_message: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO issuance_validation_failure (
operator_account,
operator_identity_bs58,
failure_message
) VALUES (?, ?, ?)
"#,
operator_account,
operator_identity_bs58,
failure_message,
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
@@ -2,9 +2,11 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::NymRewarderError;
use crate::rewarder::credential_issuance::types::CredentialIssuer;
use crate::rewarder::epoch::Epoch;
use crate::rewarder::storage::manager::StorageManager;
use crate::rewarder::{EpochRewards, RewardingResult};
use nym_validator_client::nym_api::IssuedCredentialBody;
use nym_validator_client::nyxd::Coin;
use sqlx::ConnectOptions;
use std::fmt::Debug;
@@ -13,6 +15,7 @@ use tracing::{error, info, instrument};
mod manager;
#[derive(Clone)]
pub struct RewarderStorage {
pub(crate) manager: StorageManager,
}
@@ -77,11 +80,93 @@ impl RewarderStorage {
.await?)
}
pub(crate) async fn get_deposit_credential_id(
&self,
operator_identity_bs58: String,
deposit_tx: String,
) -> Result<Option<i64>, NymRewarderError> {
Ok(self
.manager
.get_deposit_credential_id(operator_identity_bs58, deposit_tx)
.await?)
}
pub(crate) async fn insert_validated_deposit(
&self,
operator_identity_bs58: String,
credential_info: &IssuedCredentialBody,
) -> Result<(), NymRewarderError> {
self.manager
.insert_validated_deposit(
operator_identity_bs58,
credential_info.credential.id,
credential_info.credential.tx_hash.to_string(),
credential_info.credential.signable_plaintext(),
credential_info.signature.to_base58_string(),
)
.await?;
Ok(())
}
pub(crate) async fn insert_double_signing_evidence(
&self,
operator_identity_bs58: String,
original_credential_id: i64,
credential_info: &IssuedCredentialBody,
) -> Result<(), NymRewarderError> {
self.manager
.insert_double_signing_evidence(
operator_identity_bs58,
credential_info.credential.id,
original_credential_id,
credential_info.credential.tx_hash.to_string(),
credential_info.credential.signable_plaintext(),
credential_info.signature.to_base58_string(),
)
.await?;
Ok(())
}
pub(crate) async fn insert_issuance_foul_play_evidence(
&self,
issuer: &CredentialIssuer,
credential_info: &IssuedCredentialBody,
error_message: String,
) -> Result<(), NymRewarderError> {
self.manager
.insert_foul_play_evidence(
issuer.operator_account.to_string(),
issuer.public_key.to_base58_string(),
credential_info.credential.id,
credential_info.credential.signable_plaintext(),
credential_info.signature.to_base58_string(),
error_message,
)
.await?;
Ok(())
}
pub(crate) async fn insert_issuance_validation_failure_info(
&self,
issuer: &CredentialIssuer,
error_message: String,
) -> Result<(), NymRewarderError> {
self.manager
.insert_validation_failure_info(
issuer.operator_account.to_string(),
issuer.public_key.to_base58_string(),
error_message,
)
.await?;
Ok(())
}
pub(crate) async fn save_rewarding_information(
&self,
reward: EpochRewards,
rewarding_result: Result<RewardingResult, NymRewarderError>, // total_spent: Coin,
// rewarding_tx: Result<Hash, NymRewarderError>,
rewarding_result: Result<RewardingResult, NymRewarderError>,
// total_spent: Coin,
// rewarding_tx: Result<Hash, NymRewarderError>,
) -> Result<(), NymRewarderError> {
info!("persisting reward details");
let denom = &reward.total_budget.denom;