most of the issuance monitoring logic
This commit is contained in:
committed by
Jon Häggblad
parent
668a255e0d
commit
cfc13671a4
Generated
+613
-659
File diff suppressed because it is too large
Load Diff
@@ -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, 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;
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<i64, IssuedCredentialInner>,
|
||||
pub credentials: BTreeMap<i64, IssuedCredentialBody>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IssuedCredentialResponse {
|
||||
pub credential: Option<IssuedCredentialInner>,
|
||||
pub credential: Option<IssuedCredentialBody>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IssuedCredentialInner {
|
||||
pub struct IssuedCredentialBody {
|
||||
pub credential: IssuedCredential,
|
||||
|
||||
pub signature: identity::Signature,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<IssuedCredential> 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<Scalar>
|
||||
// pub fn public_attributes(&self)
|
||||
}
|
||||
|
||||
pub fn join_attributes<I, M>(attrs: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = M>,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -31,9 +31,13 @@ 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-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]
|
||||
|
||||
@@ -9,6 +9,7 @@ CREATE TABLE rewarding_epoch
|
||||
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
|
||||
);
|
||||
@@ -51,12 +52,12 @@ CREATE TABLE malformed_credential
|
||||
|
||||
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,
|
||||
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)
|
||||
|
||||
@@ -255,7 +255,7 @@ pub struct NyxdScraper {
|
||||
// TODO: debug with everything that's currently hardcoded in the scraper
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Copy)]
|
||||
pub struct IssuanceMonitor {
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub run_interval: Duration,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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, Hash};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
@@ -85,4 +88,59 @@ pub enum NymRewarderError {
|
||||
|
||||
#[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("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<String>,
|
||||
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<String>,
|
||||
on_chain: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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 nym_validator_client::nyxd::AccountId;
|
||||
use std::time::Duration;
|
||||
use tracing::info;
|
||||
|
||||
@@ -16,29 +17,25 @@ pub mod types;
|
||||
pub struct CredentialIssuance {
|
||||
monitoring_run_interval: Duration,
|
||||
monitoring_results: MonitoringResults,
|
||||
dkg_contract_address: AccountId,
|
||||
}
|
||||
|
||||
impl CredentialIssuance {
|
||||
pub(crate) fn new(
|
||||
epoch: Epoch,
|
||||
monitoring_run_interval: Duration,
|
||||
dkg_contract_address: AccountId,
|
||||
) -> Self {
|
||||
pub(crate) fn new(epoch: Epoch, monitoring_run_interval: Duration) -> Self {
|
||||
CredentialIssuance {
|
||||
monitoring_run_interval,
|
||||
monitoring_results: MonitoringResults::new(epoch),
|
||||
dkg_contract_address,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start_monitor(&self, task_client: TaskClient) {
|
||||
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(
|
||||
self.monitoring_run_interval,
|
||||
self.dkg_contract_address.clone(),
|
||||
monitoring_results,
|
||||
);
|
||||
let mut monitor =
|
||||
CredentialIssuanceMonitor::new(monitor_config, nyxd_client, monitoring_results);
|
||||
|
||||
tokio::spawn(async move { monitor.run(task_client).await });
|
||||
}
|
||||
|
||||
@@ -1,42 +1,258 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config;
|
||||
use crate::error::NymRewarderError;
|
||||
use crate::rewarder::credential_issuance::types::MonitoringResults;
|
||||
use crate::rewarder::credential_issuance::types::{CredentialIssuer, MonitoringResults};
|
||||
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::nyxd::AccountId;
|
||||
use std::time::Duration;
|
||||
use nym_validator_client::nym_api;
|
||||
use nym_validator_client::nym_api::{IssuedCredentialBody, NymApiClientExt};
|
||||
use nym_validator_client::nyxd::Hash;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
use tokio::time::interval;
|
||||
use tracing::{error, info};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
pub(crate) fn bandwidth_voucher_params() -> &'static Parameters {
|
||||
static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock<Parameters> = OnceLock::new();
|
||||
BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(|| BandwidthVoucher::default_parameters())
|
||||
}
|
||||
|
||||
pub struct CredentialIssuanceMonitor {
|
||||
run_interval: Duration,
|
||||
dkg_contract_address: AccountId,
|
||||
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>>,
|
||||
}
|
||||
|
||||
impl CredentialIssuanceMonitor {
|
||||
pub fn new(
|
||||
run_interval: Duration,
|
||||
dkg_contract_address: AccountId,
|
||||
config: config::IssuanceMonitor,
|
||||
nyxd_client: NyxdClient,
|
||||
monitoring_results: MonitoringResults,
|
||||
) -> CredentialIssuanceMonitor {
|
||||
CredentialIssuanceMonitor {
|
||||
run_interval,
|
||||
dkg_contract_address,
|
||||
config,
|
||||
nyxd_client,
|
||||
monitoring_results,
|
||||
seen_deposits: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// 1. if not present -> go to DKG contract and grab the accounts + endpoints
|
||||
// TODO: currently we can't obtain public key of the runner in order to verify the signature
|
||||
async fn validate_issued_credential(
|
||||
&mut self,
|
||||
runner: String,
|
||||
credential_id: i64,
|
||||
issued_credential: IssuedCredentialBody,
|
||||
vk: &VerificationKey,
|
||||
) -> Result<(), NymRewarderError> {
|
||||
warn!("unimplemented: public key sharing mechanism");
|
||||
// let plaintext = issued_credential.credential.signable_plaintext();
|
||||
// if !operator_public_key.verify(&plaintext) {
|
||||
// ...
|
||||
// }
|
||||
let tx_hash = issued_credential.credential.tx_hash;
|
||||
|
||||
// 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(&tx_hash) {
|
||||
return if used != credential_id {
|
||||
Err(NymRewarderError::DuplicateDepositHash {
|
||||
tx_hash,
|
||||
first: used,
|
||||
other: credential_id,
|
||||
})
|
||||
} else {
|
||||
debug!("we have already verified this credential before");
|
||||
Ok(())
|
||||
};
|
||||
} else {
|
||||
known_runner.insert(tx_hash, credential_id);
|
||||
}
|
||||
}
|
||||
|
||||
// check if this deposit even exists
|
||||
let (deposit_value, deposit_info) = self
|
||||
.nyxd_client
|
||||
.get_deposit_transaction_attributes(tx_hash)
|
||||
.await?;
|
||||
|
||||
// check if the deposit values match
|
||||
let credential_value = issued_credential.credential.public_attributes.get(0);
|
||||
let credential_info = issued_credential.credential.public_attributes.get(1);
|
||||
|
||||
if credential_value != Some(&deposit_value) {
|
||||
return Err(NymRewarderError::InconsistentDepositValue {
|
||||
tx_hash,
|
||||
request: credential_value.cloned(),
|
||||
on_chain: deposit_value,
|
||||
});
|
||||
}
|
||||
|
||||
if credential_info != Some(&deposit_info) {
|
||||
return Err(NymRewarderError::InconsistentDepositInfo {
|
||||
tx_hash,
|
||||
request: credential_info.cloned(),
|
||||
on_chain: deposit_info,
|
||||
});
|
||||
}
|
||||
|
||||
let public_attributes = issued_credential
|
||||
.credential
|
||||
.public_attributes
|
||||
.iter()
|
||||
.map(hash_to_scalar)
|
||||
.collect::<Vec<_>>();
|
||||
#[allow(clippy::map_identity)]
|
||||
let attributes_refs = public_attributes.iter().collect::<Vec<_>>();
|
||||
|
||||
let mut public_attribute_commitments = Vec::with_capacity(
|
||||
issued_credential
|
||||
.credential
|
||||
.bs58_encoded_private_attributes_commitments
|
||||
.len(),
|
||||
);
|
||||
|
||||
for raw_cm in issued_credential
|
||||
.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,
|
||||
&issued_credential.credential.blinded_partial_credential,
|
||||
vk,
|
||||
) {
|
||||
return Err(NymRewarderError::BlindVerificationFailure);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_issuer(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
issuer: CredentialIssuer,
|
||||
) -> Result<(), NymRewarderError> {
|
||||
let url = match issuer.api_runner.parse() {
|
||||
Ok(url) => url,
|
||||
Err(source) => {
|
||||
return Err(NymRewarderError::MalformedApiUrl {
|
||||
raw: issuer.api_runner,
|
||||
runner_account: issuer.operator_account,
|
||||
source,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let api_client = nym_api::Client::new(url, None);
|
||||
|
||||
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!(
|
||||
"{} hasn't issued any credentials this epoch",
|
||||
issuer.operator_account
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let credential_range: Vec<_> =
|
||||
(first_id..first_id + epoch_credentials.total_issued as i64).collect();
|
||||
let issued = credential_range.len();
|
||||
|
||||
let sampled = if issued <= self.config.min_validate_per_issuer as usize {
|
||||
credential_range
|
||||
} else {
|
||||
let mut rng = thread_rng();
|
||||
let sample_size = (issued as f32 * self.config.sampling_rate) as usize;
|
||||
credential_range
|
||||
.choose_multiple(&mut rng, sample_size)
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let request_size = sampled.len();
|
||||
|
||||
let credentials = api_client.issued_credentials(sampled).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 {
|
||||
// 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(())
|
||||
}
|
||||
|
||||
async fn check_issuers(&mut self) -> Result<(), NymRewarderError> {
|
||||
let epoch = self.nyxd_client.dkg_epoch().await?;
|
||||
let issuers = self
|
||||
.nyxd_client
|
||||
.get_credential_issuers(epoch.epoch_id)
|
||||
.await?;
|
||||
|
||||
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
|
||||
if let Err(err) = self.check_issuer(epoch.epoch_id, issuer).await {
|
||||
// TODO: insert info to the db
|
||||
error!("failed to check credential issuance of {address}: {err}")
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(&mut self, mut task_client: TaskClient) {
|
||||
info!("starting");
|
||||
let mut run_interval = interval(self.run_interval);
|
||||
let mut run_interval = interval(self.config.run_interval);
|
||||
|
||||
while !task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::NymRewarderError;
|
||||
use crate::rewarder::epoch::Epoch;
|
||||
use cosmwasm_std::{Decimal, Uint128};
|
||||
use cosmwasm_std::{Addr, Decimal, Uint128};
|
||||
use nym_coconut::VerificationKey;
|
||||
use nym_coconut_dkg_common::verification_key::ContractVKShare;
|
||||
use nym_validator_client::nyxd::{AccountId, Coin};
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{error, info};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MonitoringResults {
|
||||
@@ -36,43 +38,46 @@ impl MonitoringResults {
|
||||
dkg_epoch: u32,
|
||||
operators: Vec<(String, AccountId)>,
|
||||
) {
|
||||
let mut guard = self.inner.lock().await;
|
||||
guard.operators = operators
|
||||
.into_iter()
|
||||
.map(|(api_runner, runner_account)| RawOperatorIssuing {
|
||||
api_runner,
|
||||
runner_account,
|
||||
issued_credentials: 0,
|
||||
validated_credentials: 0,
|
||||
})
|
||||
.collect();
|
||||
guard.dkg_epoch = Some(dkg_epoch)
|
||||
todo!()
|
||||
// let mut guard = self.inner.lock().await;
|
||||
// guard.operators = operators
|
||||
// .into_iter()
|
||||
// .map(|(api_runner, runner_account)| RawOperatorIssuing {
|
||||
// api_runner,
|
||||
// runner_account,
|
||||
// issued_credentials: 0,
|
||||
// validated_credentials: 0,
|
||||
// })
|
||||
// .collect();
|
||||
// guard.dkg_epoch = Some(dkg_epoch)
|
||||
}
|
||||
|
||||
pub(crate) async fn append_run_results(&self, results: Vec<(String, RawOperatorResult)>) {
|
||||
let mut guard = self.inner.lock().await;
|
||||
|
||||
// sure, a hashmap would have been quicker, but we'll have at most 30-40 runners so
|
||||
// performance overhead is negligible
|
||||
for (api_runner, results) in results {
|
||||
if let Some(entry) = guard
|
||||
.operators
|
||||
.iter_mut()
|
||||
.find(|o| o.api_runner == api_runner)
|
||||
{
|
||||
entry.issued_credentials += results.issued_credentials;
|
||||
entry.validated_credentials += results.validated_credentials;
|
||||
} else {
|
||||
error!("somehow could not find operator results for runner {api_runner}!")
|
||||
}
|
||||
}
|
||||
todo!()
|
||||
// let mut guard = self.inner.lock().await;
|
||||
//
|
||||
// // sure, a hashmap would have been quicker, but we'll have at most 30-40 runners so
|
||||
// // performance overhead is negligible
|
||||
// for (api_runner, results) in results {
|
||||
// if let Some(entry) = guard
|
||||
// .operators
|
||||
// .iter_mut()
|
||||
// .find(|o| o.api_runner == api_runner)
|
||||
// {
|
||||
// entry.issued_credentials += results.issued_credentials;
|
||||
// entry.validated_credentials += results.validated_credentials;
|
||||
// } else {
|
||||
// error!("somehow could not find operator results for runner {api_runner}!")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
pub(crate) async fn finish_epoch(&self) -> MonitoringResultsInner {
|
||||
let mut guard = self.inner.lock().await;
|
||||
let next_epoch = guard.epoch.next();
|
||||
let next_results = MonitoringResultsInner::new(next_epoch);
|
||||
mem::replace(&mut guard, next_results)
|
||||
todo!()
|
||||
// let mut guard = self.inner.lock().await;
|
||||
// let next_epoch = guard.epoch.next();
|
||||
// let next_results = MonitoringResultsInner::new(next_epoch);
|
||||
// mem::replace(&mut guard, next_results)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +143,8 @@ pub struct RawOperatorIssuing {
|
||||
|
||||
pub issued_credentials: u32,
|
||||
pub validated_credentials: u32,
|
||||
|
||||
pub(crate) starting_credential_id: u32,
|
||||
}
|
||||
|
||||
pub struct OperatorIssuing {
|
||||
@@ -180,3 +187,24 @@ impl CredentialIssuanceResults {
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CredentialIssuer {
|
||||
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> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::error::NymRewarderError;
|
||||
use cosmwasm_std::Decimal;
|
||||
use nym_validator_client::nyxd::{AccountId, PublicKey};
|
||||
use nyxd_scraper::constants::{BECH32_CONSENSUS_ADDRESS_PREFIX, BECH32_PREFIX};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
@@ -48,6 +48,15 @@ impl EpochRewards {
|
||||
|
||||
amounts
|
||||
}
|
||||
|
||||
pub fn total_spent(&self) -> Coin {
|
||||
let amount = self
|
||||
.amounts()
|
||||
.into_iter()
|
||||
.map(|(_, amount)| amount[0].amount)
|
||||
.sum();
|
||||
Coin::new(amount, &self.total_budget.denom)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rewarder {
|
||||
@@ -63,7 +72,6 @@ pub struct Rewarder {
|
||||
impl Rewarder {
|
||||
pub async fn new(config: Config) -> Result<Self, NymRewarderError> {
|
||||
let nyxd_scraper = NyxdScraper::new(config.scraper_config()).await?;
|
||||
let dkg_contract = config.dkg_contract_address();
|
||||
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? {
|
||||
@@ -77,7 +85,6 @@ impl Rewarder {
|
||||
credential_issuance: CredentialIssuance::new(
|
||||
current_epoch,
|
||||
config.issuance_monitor.run_interval,
|
||||
dkg_contract,
|
||||
),
|
||||
epoch_signing: EpochSigning {
|
||||
nyxd_scraper,
|
||||
@@ -176,8 +183,11 @@ impl Rewarder {
|
||||
// setup shutdowns
|
||||
let mut task_manager = TaskManager::new(5);
|
||||
|
||||
self.credential_issuance
|
||||
.start_monitor(task_manager.subscribe());
|
||||
self.credential_issuance.start_monitor(
|
||||
self.config.issuance_monitor,
|
||||
self.nyxd_client.clone(),
|
||||
task_manager.subscribe(),
|
||||
);
|
||||
self.epoch_signing.nyxd_scraper.start().await?;
|
||||
self.epoch_signing
|
||||
.nyxd_scraper
|
||||
|
||||
@@ -3,12 +3,23 @@
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::NymRewarderError;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::nyxd::module_traits::staking::{
|
||||
QueryHistoricalInfoResponse, QueryValidatorResponse, QueryValidatorsResponse,
|
||||
use crate::rewarder::credential_issuance::types::CredentialIssuer;
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut::VerificationKey;
|
||||
use nym_coconut_bandwidth_contract_common::events::{
|
||||
COSMWASM_DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_INFO, DEPOSIT_VALUE,
|
||||
};
|
||||
use nym_validator_client::nyxd::{AccountId, CosmWasmClient, PageRequest, StakingQueryClient};
|
||||
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
|
||||
use nym_coconut_dkg_common::types::Epoch;
|
||||
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, CosmWasmClient, Hash, PageRequest, StakingQueryClient,
|
||||
};
|
||||
use nym_validator_client::DirectSigningHttpRpcNyxdClient;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -51,11 +62,44 @@ impl NyxdClient {
|
||||
&self,
|
||||
pagination: Option<PageRequest>,
|
||||
) -> Result<QueryValidatorsResponse, NymRewarderError> {
|
||||
Ok(self
|
||||
.inner
|
||||
let guard = self.inner.read().await;
|
||||
Ok(StakingQueryClient::validators(guard.deref(), "".to_string(), pagination).await?)
|
||||
}
|
||||
|
||||
pub(crate) async fn dkg_epoch(&self) -> Result<Epoch, NymRewarderError> {
|
||||
Ok(self.inner.read().await.get_current_epoch().await?)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_credential_issuers(
|
||||
&self,
|
||||
dkg_epoch: u64,
|
||||
) -> Result<Vec<CredentialIssuer>, NymRewarderError> {
|
||||
self.inner
|
||||
.read()
|
||||
.await
|
||||
.validators("".to_string(), pagination)
|
||||
.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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,18 +29,20 @@ impl StorageManager {
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
rewarding_budget: String,
|
||||
total_spent: String,
|
||||
rewarding_tx: Option<String>,
|
||||
rewarding_error: Option<String>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO rewarding_epoch (id, start_time, end_time, budget, rewarding_tx, rewarding_error)
|
||||
VALUES (?, ?, ? ,?, ?, ?)
|
||||
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?;
|
||||
|
||||
@@ -72,6 +72,7 @@ impl RewarderStorage {
|
||||
.insert_rewarding_epoch(
|
||||
reward.epoch,
|
||||
reward.total_budget.to_string(),
|
||||
reward.total_spent().to_string(),
|
||||
reward_tx,
|
||||
reward_err,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user