Feature/spend coconut (#1261)
* Add coconut verifier structure for coconut protocol in gateway * Add endpoint for validator-api cred verification * Remove unused signature field * Register new endpoint * Improve validator-api config handling * Aggregate verif result from all apis * Simplify aggregate functions * Verify cred on apis correctly * Introduced coconut bandwidth contract to validator client * Fix rebase double import * Fix clippy on non-coconut * Add multisig contract address to validator client * Refactor Credential struct * Do bincode magic in the coconut interface * Implement serialization for credential and remove bindcode * Fix clippy and don't remove dkg * Client release funds proposal * Add wrapper for blinded serial number Also compare theta with a blinded serial number (in base 58 form) for future double spend protection. * Only post blinded serial number to blockchain * Validator api propose credential spending * Fix wallet * Gateway calls proposal creation * Query for proposal in verify coconut * Remove db from git * Verify against proposal description * Validator apis vote based on verification of cred * Fix wallet fmt * Execute the release of funds * Fix translation between token and bytes * Update CHANGELOG
This commit is contained in:
committed by
GitHub
parent
a5759ab227
commit
5f7b3db9a4
@@ -2,9 +2,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::coconut::error::Result;
|
||||
use validator_client::nymd::TxResponse;
|
||||
use multisig_contract_common::msg::ProposalResponse;
|
||||
use validator_client::nymd::{Fee, TxResponse};
|
||||
|
||||
#[async_trait]
|
||||
pub trait Client {
|
||||
async fn get_tx(&self, tx_hash: &str) -> Result<TxResponse>;
|
||||
async fn get_proposal(&self, proposal_id: u64) -> Result<ProposalResponse>;
|
||||
async fn propose_release_funds(
|
||||
&self,
|
||||
title: String,
|
||||
blinded_serial_number: String,
|
||||
voucher_value: u128,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<u64>;
|
||||
async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option<Fee>)
|
||||
-> Result<()>;
|
||||
async fn execute_proposal(&self, proposal_id: u64, fee: Option<Fee>) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -63,8 +63,22 @@ pub enum CoconutError {
|
||||
#[error("Error in coconut interface - {0}")]
|
||||
CoconutInterfaceError(#[from] coconut_interface::error::CoconutInterfaceError),
|
||||
|
||||
#[error("Could not create proposal for spending credential")]
|
||||
CreateProposalError,
|
||||
|
||||
#[error("Storage error - {0}")]
|
||||
StorageError(#[from] ValidatorApiStorageError),
|
||||
|
||||
#[error("Credentials error - {0}")]
|
||||
CredentialsError(#[from] credentials::error::Error),
|
||||
|
||||
#[error(
|
||||
"Incorrect credential proposal description. Expected blinded serial number in base 58"
|
||||
)]
|
||||
IncorrectProposal,
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError {
|
||||
|
||||
@@ -14,15 +14,19 @@ use crate::ValidatorApiStorage;
|
||||
|
||||
use coconut_interface::{
|
||||
Attribute, BlindSignRequest, BlindSignRequestBody, BlindedSignature, BlindedSignatureResponse,
|
||||
KeyPair, Parameters, VerificationKeyResponse,
|
||||
ExecuteReleaseFundsRequestBody, KeyPair, Parameters, ProposeReleaseFundsRequestBody,
|
||||
ProposeReleaseFundsResponse, VerificationKey, VerificationKeyResponse, VerifyCredentialBody,
|
||||
VerifyCredentialResponse,
|
||||
};
|
||||
use config::defaults::VALIDATOR_API_VERSION;
|
||||
use credentials::coconut::params::{
|
||||
ValidatorApiCredentialEncryptionAlgorithm, ValidatorApiCredentialHkdfAlgorithm,
|
||||
};
|
||||
use credentials::obtain_aggregate_verification_key;
|
||||
use crypto::asymmetric::encryption;
|
||||
use crypto::shared_key::new_ephemeral_shared_key;
|
||||
use crypto::symmetric::stream_cipher;
|
||||
use validator_client::validator_api::routes::{BANDWIDTH, COCONUT_ROUTES};
|
||||
|
||||
use getset::{CopyGetters, Getters};
|
||||
use rand_07::rngs::OsRng;
|
||||
@@ -30,26 +34,33 @@ use rocket::fairing::AdHoc;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State as RocketState;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use validator_client::validator_api::routes::{BANDWIDTH, COCONUT_ROUTES};
|
||||
use tokio::sync::Mutex;
|
||||
use url::Url;
|
||||
|
||||
pub struct State {
|
||||
client: Arc<RwLock<dyn LocalClient + Send + Sync>>,
|
||||
client: Arc<dyn LocalClient + Send + Sync>,
|
||||
key_pair: KeyPair,
|
||||
validator_apis: Vec<Url>,
|
||||
storage: ValidatorApiStorage,
|
||||
rng: Arc<Mutex<OsRng>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(crate) fn new<C>(client: C, key_pair: KeyPair, storage: ValidatorApiStorage) -> Self
|
||||
pub(crate) fn new<C>(
|
||||
client: C,
|
||||
key_pair: KeyPair,
|
||||
validator_apis: Vec<Url>,
|
||||
storage: ValidatorApiStorage,
|
||||
) -> Self
|
||||
where
|
||||
C: LocalClient + Send + Sync + 'static,
|
||||
{
|
||||
let client = Arc::new(RwLock::new(client));
|
||||
let client = Arc::new(client);
|
||||
let rng = Arc::new(Mutex::new(OsRng));
|
||||
Self {
|
||||
client,
|
||||
key_pair,
|
||||
validator_apis,
|
||||
storage,
|
||||
rng,
|
||||
}
|
||||
@@ -109,6 +120,10 @@ impl State {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verification_key(&self) -> Result<VerificationKey> {
|
||||
Ok(obtain_aggregate_verification_key(&self.validator_apis).await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Getters, CopyGetters, Debug)]
|
||||
@@ -135,11 +150,16 @@ impl InternalSignRequest {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stage<C>(client: C, key_pair: KeyPair, storage: ValidatorApiStorage) -> AdHoc
|
||||
pub fn stage<C>(
|
||||
client: C,
|
||||
key_pair: KeyPair,
|
||||
validator_apis: Vec<Url>,
|
||||
storage: ValidatorApiStorage,
|
||||
) -> AdHoc
|
||||
where
|
||||
C: LocalClient + Send + Sync + 'static,
|
||||
{
|
||||
let state = State::new(client, key_pair, storage);
|
||||
let state = State::new(client, key_pair, validator_apis, storage);
|
||||
AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async {
|
||||
rocket.manage(state).mount(
|
||||
// this format! is so ugly...
|
||||
@@ -150,7 +170,10 @@ impl InternalSignRequest {
|
||||
routes![
|
||||
post_blind_sign,
|
||||
get_verification_key,
|
||||
post_partial_bandwidth_credential
|
||||
post_partial_bandwidth_credential,
|
||||
verify_bandwidth_credential,
|
||||
post_propose_release_funds,
|
||||
post_execute_release_funds
|
||||
],
|
||||
)
|
||||
})
|
||||
@@ -183,8 +206,6 @@ pub async fn post_blind_sign(
|
||||
}
|
||||
let tx = state
|
||||
.client
|
||||
.read()
|
||||
.await
|
||||
.get_tx(blind_sign_request_body.tx_hash())
|
||||
.await?;
|
||||
let encryption_key = extract_encryption_key(&blind_sign_request_body, tx).await?;
|
||||
@@ -226,3 +247,69 @@ pub async fn get_verification_key(
|
||||
state.key_pair.verification_key(),
|
||||
)))
|
||||
}
|
||||
|
||||
#[post("/verify-bandwidth-credential", data = "<verify_credential_body>")]
|
||||
pub async fn verify_bandwidth_credential(
|
||||
verify_credential_body: Json<VerifyCredentialBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<VerifyCredentialResponse>> {
|
||||
let proposal_id = *verify_credential_body.0.proposal_id();
|
||||
let proposal = state.client.get_proposal(proposal_id).await?;
|
||||
// Proposal description is the blinded serial number
|
||||
if !verify_credential_body
|
||||
.0
|
||||
.credential()
|
||||
.has_blinded_serial_number(&proposal.description)?
|
||||
{
|
||||
return Err(CoconutError::IncorrectProposal);
|
||||
}
|
||||
let verification_key = state.verification_key().await?;
|
||||
let verification_result = verify_credential_body
|
||||
.0
|
||||
.credential()
|
||||
.verify(&verification_key);
|
||||
|
||||
// Vote yes or no on the proposal based on the verification result
|
||||
state
|
||||
.client
|
||||
.vote_proposal(proposal_id, verification_result, None)
|
||||
.await?;
|
||||
|
||||
Ok(Json(VerifyCredentialResponse::new(verification_result)))
|
||||
}
|
||||
|
||||
#[post("/propose-release-funds", data = "<propose_release_funds>")]
|
||||
pub async fn post_propose_release_funds(
|
||||
propose_release_funds: Json<ProposeReleaseFundsRequestBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<ProposeReleaseFundsResponse>> {
|
||||
let verification_key = state.verification_key().await?;
|
||||
if !propose_release_funds
|
||||
.0
|
||||
.credential()
|
||||
.verify(&verification_key)
|
||||
{
|
||||
return Err(CoconutError::CreateProposalError);
|
||||
}
|
||||
|
||||
let title = String::from("Create proposal to spend a coconut credential");
|
||||
let blinded_serial_number = propose_release_funds.0.credential().blinded_serial_number();
|
||||
let voucher_value = propose_release_funds.0.credential().voucher_value() as u128;
|
||||
let proposal_id = state
|
||||
.client
|
||||
.propose_release_funds(title, blinded_serial_number, voucher_value, None)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ProposeReleaseFundsResponse::new(proposal_id)))
|
||||
}
|
||||
|
||||
#[post("/execute-release-funds", data = "<execute_release_funds>")]
|
||||
pub async fn post_execute_release_funds(
|
||||
execute_release_funds: Json<ExecuteReleaseFundsRequestBody>,
|
||||
state: &RocketState<State>,
|
||||
) -> Result<Json<()>> {
|
||||
let proposal_id = *execute_release_funds.0.proposal_id();
|
||||
state.client.execute_proposal(proposal_id, None).await?;
|
||||
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ use credentials::coconut::params::{
|
||||
};
|
||||
use crypto::shared_key::recompute_shared_key;
|
||||
use crypto::symmetric::stream_cipher;
|
||||
use multisig_contract_common::msg::ProposalResponse;
|
||||
use nymcoconut::{
|
||||
prepare_blind_sign, ttp_keygen, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters,
|
||||
};
|
||||
use validator_client::nymd::{tx::Hash, DeliverTx, Event, Tag, TxResponse};
|
||||
use validator_client::nymd::{tx::Hash, DeliverTx, Event, Fee, Tag, TxResponse};
|
||||
use validator_client::validator_api::routes::{
|
||||
API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL,
|
||||
COCONUT_ROUTES, COCONUT_VERIFICATION_KEY,
|
||||
@@ -56,6 +57,37 @@ impl super::client::Client for DummyClient {
|
||||
.cloned()
|
||||
.ok_or(CoconutError::TxHashParseError)
|
||||
}
|
||||
|
||||
async fn get_proposal(&self, _proposal_id: u64) -> Result<ProposalResponse> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn propose_release_funds(
|
||||
&self,
|
||||
_title: String,
|
||||
_blinded_serial_number: String,
|
||||
_voucher_value: u128,
|
||||
_fee: Option<Fee>,
|
||||
) -> Result<u64> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn vote_proposal(
|
||||
&self,
|
||||
_proposal_id: u64,
|
||||
_vote_yes: bool,
|
||||
_fee: Option<Fee>,
|
||||
) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn execute_proposal(
|
||||
&self,
|
||||
proposal_id: u64,
|
||||
fee: Option<Fee>,
|
||||
) -> crate::coconut::error::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tx_entry_fixture(tx_hash: &str) -> TxResponse {
|
||||
@@ -87,7 +119,12 @@ async fn check_signer_verif_key(key_pair: KeyPair) {
|
||||
let nymd_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
let nymd_client = DummyClient::new(&nymd_db);
|
||||
|
||||
let rocket = rocket::build().attach(InternalSignRequest::stage(nymd_client, key_pair, storage));
|
||||
let rocket = rocket::build().attach(InternalSignRequest::stage(
|
||||
nymd_client,
|
||||
key_pair,
|
||||
vec![],
|
||||
storage,
|
||||
));
|
||||
|
||||
let client = Client::tracked(rocket)
|
||||
.await
|
||||
@@ -172,6 +209,7 @@ async fn signed_before() {
|
||||
let rocket = rocket::build().attach(InternalSignRequest::stage(
|
||||
nymd_client,
|
||||
key_pair,
|
||||
vec![],
|
||||
storage.clone(),
|
||||
));
|
||||
let client = Client::tracked(rocket)
|
||||
@@ -231,7 +269,7 @@ async fn state_functions() {
|
||||
let mut db_dir = std::env::temp_dir();
|
||||
db_dir.push(&key_pair.verification_key().to_bs58()[..8]);
|
||||
let storage = ValidatorApiStorage::init(db_dir).await.unwrap();
|
||||
let state = State::new(nymd_client, key_pair, storage.clone());
|
||||
let state = State::new(nymd_client, key_pair, vec![], storage.clone());
|
||||
|
||||
let tx_hash = String::from("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E");
|
||||
assert!(state.signed_before(&tx_hash).await.unwrap().is_none());
|
||||
@@ -393,6 +431,7 @@ async fn blind_sign_correct() {
|
||||
let rocket = rocket::build().attach(InternalSignRequest::stage(
|
||||
nymd_client,
|
||||
key_pair,
|
||||
vec![],
|
||||
storage.clone(),
|
||||
));
|
||||
let client = Client::tracked(rocket)
|
||||
@@ -465,6 +504,7 @@ async fn signature_test() {
|
||||
let rocket = rocket::build().attach(InternalSignRequest::stage(
|
||||
nymd_client,
|
||||
key_pair,
|
||||
vec![],
|
||||
storage.clone(),
|
||||
));
|
||||
let client = Client::tracked(rocket)
|
||||
|
||||
@@ -2,16 +2,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config::template::config_template;
|
||||
use config::defaults::{default_api_endpoints, DEFAULT_NETWORK};
|
||||
use config::defaults::DEFAULT_NETWORK;
|
||||
use config::NymConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::{Base58, KeyPair};
|
||||
|
||||
mod template;
|
||||
|
||||
pub const DEFAULT_LOCAL_VALIDATOR: &str = "http://localhost:26657";
|
||||
@@ -82,17 +79,20 @@ impl NymConfig for Config {
|
||||
}
|
||||
|
||||
fn config_directory(&self) -> PathBuf {
|
||||
self.root_directory().join("config")
|
||||
self.root_directory().join(self.get_id()).join("config")
|
||||
}
|
||||
|
||||
fn data_directory(&self) -> PathBuf {
|
||||
self.root_directory().join("data")
|
||||
self.root_directory().join(self.get_id()).join("data")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Base {
|
||||
/// ID specifies the human readable ID of this particular validator-api.
|
||||
id: String,
|
||||
|
||||
local_validator: Url,
|
||||
|
||||
/// Address of the validator contract managing the network
|
||||
@@ -105,6 +105,7 @@ pub struct Base {
|
||||
impl Default for Base {
|
||||
fn default() -> Self {
|
||||
Base {
|
||||
id: String::default(),
|
||||
local_validator: DEFAULT_LOCAL_VALIDATOR
|
||||
.parse()
|
||||
.expect("default local validator is malformed!"),
|
||||
@@ -128,11 +129,6 @@ pub struct NetworkMonitor {
|
||||
#[serde(default)]
|
||||
disabled_credentials_mode: bool,
|
||||
|
||||
/// Specifies list of all validators on the network issuing coconut credentials.
|
||||
/// A special care must be taken to ensure they are in correct order.
|
||||
/// The list must also contain THIS validator that is running the test
|
||||
all_validator_apis: Vec<Url>,
|
||||
|
||||
/// Specifies the interval at which the network monitor sends the test packets.
|
||||
#[serde(with = "humantime_serde")]
|
||||
run_interval: Duration,
|
||||
@@ -189,8 +185,10 @@ pub struct NetworkMonitor {
|
||||
}
|
||||
|
||||
impl NetworkMonitor {
|
||||
pub const DB_FILE: &'static str = "credentials_database.db";
|
||||
|
||||
fn default_credentials_database_path() -> PathBuf {
|
||||
Config::default_data_directory(None).join("credentials_database.db")
|
||||
Config::default_data_directory(None).join(Self::DB_FILE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +199,6 @@ impl Default for NetworkMonitor {
|
||||
min_gateway_reliability: DEFAULT_MIN_GATEWAY_RELIABILITY,
|
||||
enabled: false,
|
||||
disabled_credentials_mode: true,
|
||||
all_validator_apis: default_api_endpoints(),
|
||||
run_interval: DEFAULT_MONITOR_RUN_INTERVAL,
|
||||
gateway_ping_interval: DEFAULT_GATEWAY_PING_INTERVAL,
|
||||
gateway_sending_rate: DEFAULT_GATEWAY_SENDING_RATE,
|
||||
@@ -230,8 +227,10 @@ pub struct NodeStatusAPI {
|
||||
}
|
||||
|
||||
impl NodeStatusAPI {
|
||||
pub const DB_FILE: &'static str = "db.sqlite";
|
||||
|
||||
fn default_database_path() -> PathBuf {
|
||||
Config::default_data_directory(None).join("db.sqlite")
|
||||
Config::default_data_directory(None).join(Self::DB_FILE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,15 +278,31 @@ impl Default for Rewarding {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(default)]
|
||||
#[cfg(feature = "coconut")]
|
||||
pub struct CoconutSigner {
|
||||
/// Specifies whether rewarding service is enabled in this process.
|
||||
enabled: bool,
|
||||
|
||||
/// Base58 encoded signing keypair
|
||||
keypair_bs58: String,
|
||||
/// Path to the signing keypair
|
||||
keypair_path: PathBuf,
|
||||
|
||||
/// Specifies list of all validators on the network issuing coconut credentials.
|
||||
/// A special care must be taken to ensure they are in correct order.
|
||||
/// The list must also contain THIS validator that is running the test
|
||||
all_validator_apis: Vec<Url>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
impl Default for CoconutSigner {
|
||||
fn default() -> Self {
|
||||
CoconutSigner {
|
||||
enabled: false,
|
||||
keypair_path: PathBuf::default(),
|
||||
all_validator_apis: config::defaults::default_api_endpoints(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -295,9 +310,18 @@ impl Config {
|
||||
Config::default()
|
||||
}
|
||||
|
||||
pub fn with_id(mut self, id: &str) -> Self {
|
||||
self.base.id = id.to_string();
|
||||
self.node_status_api.database_path =
|
||||
Config::default_data_directory(Some(id)).join(NodeStatusAPI::DB_FILE);
|
||||
self.network_monitor.credentials_database_path =
|
||||
Config::default_data_directory(Some(id)).join(NetworkMonitor::DB_FILE);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
pub fn keypair(&self) -> KeyPair {
|
||||
KeyPair::try_from_bs58(self.coconut_signer.keypair_bs58.clone()).unwrap()
|
||||
pub fn keypair_path(&self) -> PathBuf {
|
||||
self.coconut_signer.keypair_path.clone()
|
||||
}
|
||||
|
||||
pub fn with_network_monitor_enabled(mut self, enabled: bool) -> Self {
|
||||
@@ -337,13 +361,14 @@ impl Config {
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
pub fn with_keypair<S: Into<String>>(mut self, keypair_bs58: S) -> Self {
|
||||
self.coconut_signer.keypair_bs58 = keypair_bs58.into();
|
||||
pub fn with_keypair_path(mut self, keypair_path: PathBuf) -> Self {
|
||||
self.coconut_signer.keypair_path = keypair_path;
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
pub fn with_custom_validator_apis(mut self, validator_api_urls: Vec<Url>) -> Self {
|
||||
self.network_monitor.all_validator_apis = validator_api_urls;
|
||||
self.coconut_signer.all_validator_apis = validator_api_urls;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -374,6 +399,10 @@ impl Config {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> String {
|
||||
self.base.id.clone()
|
||||
}
|
||||
|
||||
pub fn get_network_monitor_enabled(&self) -> bool {
|
||||
self.network_monitor.enabled
|
||||
}
|
||||
@@ -474,7 +503,7 @@ impl Config {
|
||||
// fix dead code warnings as this method is only ever used with coconut feature
|
||||
#[cfg(feature = "coconut")]
|
||||
pub fn get_all_validator_api_endpoints(&self) -> Vec<Url> {
|
||||
self.network_monitor.all_validator_apis.clone()
|
||||
self.coconut_signer.all_validator_apis.clone()
|
||||
}
|
||||
|
||||
// TODO: Remove if still unused
|
||||
|
||||
@@ -10,12 +10,18 @@ pub(crate) fn config_template() -> &'static str {
|
||||
|
||||
[base]
|
||||
|
||||
# ID specifies the human readable ID of this particular validator-api.
|
||||
id = '{{ base.id }}'
|
||||
|
||||
# Validator server to which the API will be getting information about the network.
|
||||
local_validator = '{{ base.local_validator }}'
|
||||
|
||||
# Address of the validator contract managing the network.
|
||||
mixnet_contract_address = '{{ base.mixnet_contract_address }}'
|
||||
|
||||
# Mnemonic used for rewarding and validator interaction
|
||||
mnemonic = '{{ base.mnemonic }}'
|
||||
|
||||
##### network monitor config options #####
|
||||
|
||||
[network_monitor]
|
||||
@@ -26,15 +32,6 @@ enabled = {{ network_monitor.enabled }}
|
||||
# to claim bandwidth without presenting bandwidth credentials.
|
||||
disabled_credentials_mode = {{ network_monitor.disabled_credentials_mode }}
|
||||
|
||||
# Specifies list of all validators on the network issuing coconut credentials.
|
||||
# A special care must be taken to ensure they are in correct order.
|
||||
# The list must also contain THIS validator that is running the test
|
||||
all_validator_apis = [
|
||||
{{#each network_monitor.all_validator_apis }}
|
||||
'{{this}}',
|
||||
{{/each}}
|
||||
]
|
||||
|
||||
# Specifies the interval at which the network monitor sends the test packets.
|
||||
run_interval = '{{ network_monitor.run_interval }}'
|
||||
|
||||
@@ -92,13 +89,27 @@ database_path = '{{ node_status_api.database_path }}'
|
||||
# Specifies whether rewarding service is enabled in this process.
|
||||
enabled = {{ rewarding.enabled }}
|
||||
|
||||
# Mnemonic (currently of the network monitor) used for rewarding
|
||||
mnemonic = '{{ rewarding.mnemonic }}'
|
||||
|
||||
# Specifies the minimum percentage of monitor test run data present in order to
|
||||
# distribute rewards for given interval.
|
||||
# Note, only values in range 0-100 are valid
|
||||
minimum_interval_monitor_threshold = {{ rewarding.minimum_interval_monitor_threshold }}
|
||||
|
||||
[coconut_signer]
|
||||
|
||||
# Specifies whether rewarding service is enabled in this process.
|
||||
enabled = {{ coconut_signer.enabled }}
|
||||
|
||||
# Path to the signing keypair
|
||||
keypair_path = '{{ coconut_signer.keypair_path }}'
|
||||
|
||||
# Specifies list of all validators on the network issuing coconut credentials.
|
||||
# A special care must be taken to ensure they are in correct order.
|
||||
# The list must also contain THIS validator that is running the test
|
||||
all_validator_apis = [
|
||||
{{#each coconut_signer.all_validator_apis }}
|
||||
'{{this}}',
|
||||
{{/each}}
|
||||
]
|
||||
|
||||
"#
|
||||
}
|
||||
|
||||
+45
-32
@@ -23,17 +23,18 @@ use rocket::{Ignite, Rocket};
|
||||
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors};
|
||||
use rocket_okapi::mount_endpoints_and_merged_docs;
|
||||
use rocket_okapi::swagger_ui::make_swagger_ui;
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{fs, process};
|
||||
use tokio::sync::Notify;
|
||||
use url::Url;
|
||||
// use validator_client::nymd::SigningNymdClient;
|
||||
// use validator_client::ValidatorClientError;
|
||||
|
||||
use crate::rewarded_set_updater::RewardedSetUpdater;
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut::InternalSignRequest;
|
||||
#[cfg(feature = "coconut")]
|
||||
use coconut_interface::{Base58, KeyPair};
|
||||
use validator_client::nymd::SigningNymdClient;
|
||||
|
||||
pub(crate) mod config;
|
||||
@@ -48,18 +49,19 @@ mod swagger;
|
||||
#[cfg(feature = "coconut")]
|
||||
mod coconut;
|
||||
|
||||
const ID: &str = "id";
|
||||
const MONITORING_ENABLED: &str = "enable-monitor";
|
||||
const REWARDING_ENABLED: &str = "enable-rewarding";
|
||||
const MIXNET_CONTRACT_ARG: &str = "mixnet-contract";
|
||||
const MNEMONIC_ARG: &str = "mnemonic";
|
||||
const WRITE_CONFIG_ARG: &str = "save-config";
|
||||
const NYMD_VALIDATOR_ARG: &str = "nymd-validator";
|
||||
const API_VALIDATORS_ARG: &str = "api-validators";
|
||||
const ENABLED_CREDENTIALS_MODE_ARG_NAME: &str = "enabled-credentials-mode";
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
const API_VALIDATORS_ARG: &str = "api-validators";
|
||||
#[cfg(feature = "coconut")]
|
||||
const KEYPAIR_ARG: &str = "keypair";
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
const COCONUT_ENABLED: &str = "enable-coconut";
|
||||
|
||||
@@ -73,7 +75,8 @@ const REWARDING_MONITOR_THRESHOLD_ARG: &str = "monitor-threshold";
|
||||
const MIN_MIXNODE_RELIABILITY_ARG: &str = "min_mixnode_reliability";
|
||||
const MIN_GATEWAY_RELIABILITY_ARG: &str = "min_gateway_reliability";
|
||||
|
||||
fn parse_validators(raw: &str) -> Vec<Url> {
|
||||
#[cfg(feature = "coconut")]
|
||||
fn parse_validators(raw: &str) -> Vec<url::Url> {
|
||||
raw.split(',')
|
||||
.map(|raw_validator| {
|
||||
raw_validator
|
||||
@@ -124,6 +127,12 @@ fn parse_args<'a>() -> ArgMatches<'a> {
|
||||
.version(crate_version!())
|
||||
.long_version(&*build_details)
|
||||
.author("Nymtech")
|
||||
.arg(
|
||||
Arg::with_name(ID)
|
||||
.help("Id of the validator-api we want to run")
|
||||
.long(ID)
|
||||
.takes_value(true)
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(MONITORING_ENABLED)
|
||||
.help("specifies whether a network monitoring is enabled on this API")
|
||||
@@ -159,12 +168,6 @@ fn parse_args<'a>() -> ArgMatches<'a> {
|
||||
.long(WRITE_CONFIG_ARG)
|
||||
.short("w")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(API_VALIDATORS_ARG)
|
||||
.help("specifies list of all validators on the network issuing coconut credentials. Ensure they are properly ordered")
|
||||
.long(API_VALIDATORS_ARG)
|
||||
.takes_value(true)
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(REWARDING_MONITOR_THRESHOLD_ARG)
|
||||
.help("Specifies the minimum percentage of monitor test run data present in order to distribute rewards for given interval.")
|
||||
@@ -185,10 +188,16 @@ fn parse_args<'a>() -> ArgMatches<'a> {
|
||||
.takes_value(true)
|
||||
.long(KEYPAIR_ARG),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(API_VALIDATORS_ARG)
|
||||
.help("specifies list of all validators on the network issuing coconut credentials. Ensure they are properly ordered")
|
||||
.long(API_VALIDATORS_ARG)
|
||||
.takes_value(true)
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name(COCONUT_ENABLED)
|
||||
.help("Flag to indicate whether coconut signer authority is enabled on this API")
|
||||
.requires_all(&[KEYPAIR_ARG, MNEMONIC_ARG])
|
||||
.requires_all(&[KEYPAIR_ARG, MNEMONIC_ARG, API_VALIDATORS_ARG])
|
||||
.long(COCONUT_ENABLED),
|
||||
);
|
||||
|
||||
@@ -236,12 +245,18 @@ fn setup_logging() {
|
||||
.filter_module("sled", log::LevelFilter::Warn)
|
||||
.filter_module("tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("_", log::LevelFilter::Warn)
|
||||
.filter_module("rocket::server", log::LevelFilter::Warn)
|
||||
.init();
|
||||
}
|
||||
|
||||
fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config {
|
||||
if let Some(id) = matches.value_of(ID) {
|
||||
fs::create_dir_all(Config::default_config_directory(Some(id)))
|
||||
.expect("Could not create config directory");
|
||||
fs::create_dir_all(Config::default_data_directory(Some(id)))
|
||||
.expect("Could not create data directory");
|
||||
config = config.with_id(id);
|
||||
}
|
||||
|
||||
if matches.is_present(MONITORING_ENABLED) {
|
||||
config = config.with_network_monitor_enabled(true)
|
||||
}
|
||||
@@ -255,6 +270,7 @@ fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config {
|
||||
config = config.with_coconut_signer_enabled(true)
|
||||
}
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
if let Some(raw_validators) = matches.value_of(API_VALIDATORS_ARG) {
|
||||
config = config.with_custom_validator_apis(parse_validators(raw_validators));
|
||||
}
|
||||
@@ -311,11 +327,7 @@ fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config {
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
if let Some(keypair_path) = matches.value_of(KEYPAIR_ARG) {
|
||||
let keypair_bs58 = std::fs::read_to_string(keypair_path)
|
||||
.unwrap()
|
||||
.trim()
|
||||
.to_string();
|
||||
config = config.with_keypair(keypair_bs58)
|
||||
config = config.with_keypair_path(keypair_path.into())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
@@ -402,7 +414,7 @@ fn expected_monitor_test_runs(config: &Config, interval_length: Duration) -> usi
|
||||
async fn setup_rocket(
|
||||
config: &Config,
|
||||
liftoff_notify: Arc<Notify>,
|
||||
_nymd_client: Option<Client<SigningNymdClient>>,
|
||||
_nymd_client: Client<SigningNymdClient>,
|
||||
) -> Result<Rocket<Ignite>> {
|
||||
let openapi_settings = rocket_okapi::settings::OpenApiSettings::default();
|
||||
let mut rocket = rocket::build();
|
||||
@@ -434,9 +446,14 @@ async fn setup_rocket(
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
let rocket = if config.get_coconut_signer_enabled() {
|
||||
let keypair_bs58 = fs::read_to_string(config.keypair_path())?
|
||||
.trim()
|
||||
.to_string();
|
||||
let keypair = KeyPair::try_from_bs58(keypair_bs58)?;
|
||||
rocket.attach(InternalSignRequest::stage(
|
||||
_nymd_client.expect("Should have a signing client here"),
|
||||
config.keypair(),
|
||||
_nymd_client,
|
||||
keypair,
|
||||
config.get_all_validator_api_endpoints(),
|
||||
storage.clone().unwrap(),
|
||||
))
|
||||
} else {
|
||||
@@ -486,10 +503,11 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
let system_version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
// try to load config from the file, if it doesn't exist, use default values
|
||||
let config = match Config::load_from_file(None) {
|
||||
let id = matches.value_of(ID);
|
||||
let config = match Config::load_from_file(id) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(_) => {
|
||||
let config_path = Config::default_config_file_path(None)
|
||||
let config_path = Config::default_config_file_path(id)
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.unwrap();
|
||||
@@ -507,11 +525,7 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let signing_nymd_client = if matches.is_present(MNEMONIC_ARG) {
|
||||
Some(Client::new_signing(&config))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let signing_nymd_client = Client::new_signing(&config);
|
||||
|
||||
let liftoff_notify = Arc::new(Notify::new());
|
||||
|
||||
@@ -529,9 +543,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
// if network monitor is disabled, we're not going to be sending any rewarding hence
|
||||
// we're not starting signing client
|
||||
if config.get_network_monitor_enabled() {
|
||||
let nymd_client = signing_nymd_client.expect("We should have a signing client here");
|
||||
let validator_cache_refresher = ValidatorCacheRefresher::new(
|
||||
nymd_client.clone(),
|
||||
signing_nymd_client.clone(),
|
||||
config.get_caching_interval(),
|
||||
validator_cache.clone(),
|
||||
);
|
||||
@@ -546,7 +559,7 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
tokio::spawn(async move { uptime_updater.run().await });
|
||||
|
||||
let mut rewarded_set_updater =
|
||||
RewardedSetUpdater::new(nymd_client, validator_cache.clone(), storage).await?;
|
||||
RewardedSetUpdater::new(signing_nymd_client, validator_cache.clone(), storage).await?;
|
||||
|
||||
// spawn rewarded set updater
|
||||
tokio::spawn(async move { rewarded_set_updater.run().await.unwrap() });
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::rewarded_set_updater::error::RewardingError;
|
||||
#[cfg(feature = "coconut")]
|
||||
use async_trait::async_trait;
|
||||
use config::defaults::{DEFAULT_NETWORK, DEFAULT_VALIDATOR_API_PORT};
|
||||
use mixnet_contract_common::Interval;
|
||||
use mixnet_contract_common::{
|
||||
reward_params::EpochRewardParams, ContractStateParams, Delegation, ExecuteMsg, GatewayBond,
|
||||
IdentityKey, MixNodeBond, MixnodeRewardingStatusResponse, RewardedSetNodeStatus,
|
||||
};
|
||||
use serde::Serialize;
|
||||
#[cfg(feature = "coconut")]
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use config::defaults::{DEFAULT_NETWORK, DEFAULT_VALIDATOR_API_PORT};
|
||||
use mixnet_contract_common::{
|
||||
reward_params::EpochRewardParams, ContractStateParams, Delegation, ExecuteMsg, GatewayBond,
|
||||
IdentityKey, Interval, MixNodeBond, MixnodeRewardingStatusResponse, RewardedSetNodeStatus,
|
||||
};
|
||||
#[cfg(feature = "coconut")]
|
||||
use multisig_contract_common::msg::ProposalResponse;
|
||||
#[cfg(feature = "coconut")]
|
||||
use validator_client::nymd::{
|
||||
cosmwasm_client::logs::find_attribute,
|
||||
traits::{MultisigSigningClient, QueryClient},
|
||||
};
|
||||
use validator_client::nymd::{
|
||||
hash::{Hash, SHA256_HASH_SIZE},
|
||||
Coin, CosmWasmClient, Fee, QueryNymdClient, SigningCosmWasmClient, SigningNymdClient,
|
||||
@@ -23,6 +30,11 @@ use validator_client::nymd::{
|
||||
};
|
||||
use validator_client::ValidatorClientError;
|
||||
|
||||
#[cfg(feature = "coconut")]
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::config::Config;
|
||||
use crate::rewarded_set_updater::error::RewardingError;
|
||||
|
||||
pub(crate) struct Client<C>(pub(crate) Arc<RwLock<validator_client::Client<C>>>);
|
||||
|
||||
impl<C> Clone for Client<C> {
|
||||
@@ -46,14 +58,8 @@ impl Client<QueryNymdClient> {
|
||||
.parse()
|
||||
.expect("the mixnet contract address is invalid!");
|
||||
|
||||
let client_config = validator_client::Config::new(
|
||||
network,
|
||||
nymd_url,
|
||||
api_url,
|
||||
Some(mixnet_contract),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let client_config = validator_client::Config::new(network, nymd_url, api_url)
|
||||
.with_mixnode_contract_address(mixnet_contract);
|
||||
let inner =
|
||||
validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!");
|
||||
|
||||
@@ -80,14 +86,8 @@ impl Client<SigningNymdClient> {
|
||||
.parse()
|
||||
.expect("the mnemonic is invalid!");
|
||||
|
||||
let client_config = validator_client::Config::new(
|
||||
network,
|
||||
nymd_url,
|
||||
api_url,
|
||||
Some(mixnet_contract),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let client_config = validator_client::Config::new(network, nymd_url, api_url)
|
||||
.with_mixnode_contract_address(mixnet_contract);
|
||||
let inner = validator_client::Client::new_signing(client_config, mnemonic)
|
||||
.expect("Failed to connect to nymd!");
|
||||
|
||||
@@ -366,12 +366,7 @@ impl<C> Client<C> {
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
M: Serialize + Clone + Send,
|
||||
{
|
||||
let contract = self
|
||||
.0
|
||||
.read()
|
||||
.await
|
||||
.get_mixnet_contract_address()
|
||||
.ok_or(RewardingError::UnspecifiedContractAddress)?;
|
||||
let contract = self.0.read().await.get_mixnet_contract_address();
|
||||
|
||||
// grab the write lock here so we're sure nothing else is executing anything on the contract
|
||||
// in the meantime
|
||||
@@ -420,7 +415,7 @@ impl<C> Client<C> {
|
||||
#[cfg(feature = "coconut")]
|
||||
impl<C> crate::coconut::client::Client for Client<C>
|
||||
where
|
||||
C: CosmWasmClient + Sync + Send,
|
||||
C: SigningCosmWasmClient + Sync + Send,
|
||||
{
|
||||
async fn get_tx(
|
||||
&self,
|
||||
@@ -428,7 +423,71 @@ where
|
||||
) -> crate::coconut::error::Result<validator_client::nymd::TxResponse> {
|
||||
let tx_hash = tx_hash
|
||||
.parse::<validator_client::nymd::tx::Hash>()
|
||||
.map_err(|_| crate::coconut::error::CoconutError::TxHashParseError)?;
|
||||
.map_err(|_| CoconutError::TxHashParseError)?;
|
||||
Ok(self.0.read().await.nymd.get_tx(tx_hash).await?)
|
||||
}
|
||||
|
||||
async fn get_proposal(
|
||||
&self,
|
||||
proposal_id: u64,
|
||||
) -> crate::coconut::error::Result<ProposalResponse> {
|
||||
Ok(self.0.read().await.nymd.get_proposal(proposal_id).await?)
|
||||
}
|
||||
|
||||
async fn propose_release_funds(
|
||||
&self,
|
||||
title: String,
|
||||
blinded_serial_number: String,
|
||||
voucher_value: u128,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<u64, CoconutError> {
|
||||
let res = self
|
||||
.0
|
||||
.read()
|
||||
.await
|
||||
.nymd
|
||||
.propose_release_funds(title, blinded_serial_number, voucher_value, fee)
|
||||
.await?;
|
||||
let proposal_id = u64::from_str(
|
||||
&find_attribute(&res.logs, "wasm", "proposal_id")
|
||||
.ok_or_else(|| {
|
||||
CoconutError::InternalError("No attribute with proposal_id as key".to_string())
|
||||
})?
|
||||
.value,
|
||||
)
|
||||
.map_err(|_| {
|
||||
CoconutError::InternalError("proposal_id could not be parsed to u64".to_string())
|
||||
})?;
|
||||
|
||||
Ok(proposal_id)
|
||||
}
|
||||
|
||||
async fn vote_proposal(
|
||||
&self,
|
||||
proposal_id: u64,
|
||||
vote_yes: bool,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<(), CoconutError> {
|
||||
self.0
|
||||
.read()
|
||||
.await
|
||||
.nymd
|
||||
.vote_proposal(proposal_id, vote_yes, fee)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_proposal(
|
||||
&self,
|
||||
proposal_id: u64,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<(), CoconutError> {
|
||||
self.0
|
||||
.read()
|
||||
.await
|
||||
.nymd
|
||||
.execute_proposal(proposal_id, fee)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@ use validator_client::ValidatorClientError;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RewardingError {
|
||||
#[error("Could not distribute rewards as the contract address was unspecified")]
|
||||
UnspecifiedContractAddress,
|
||||
|
||||
// #[error("There were no mixnodes to reward (network is dead)")]
|
||||
// NoMixnodesToReward,
|
||||
#[error("Failed to execute the smart contract - {0}")]
|
||||
|
||||
Reference in New Issue
Block a user