Feature/spend coconut (#1210)

* Import cw3-flex-multisig and cw4-group contracts

* Add release_funds to coconut-bandwidth-contract

* Create contract.rs file

* Add cw multi test and a test that uses it

* Use mnemonic for coconut mode too

* Stricter access to config file, which contains mnemonic

* Update tests

* Remove signed deposits dir after merging that into sql db

* Clippy nits

* More clippy

* Remove backtraces features to pass clippy tests

* Merge the same mnemonic for rewarding and coconut

* Simplify things, letting network monitor use testnet-mode with gateways

* Unify the nymd clients

* Sqlx common storage for buying/consuming credentials

* Link credential storage to credential client

* Trigger rewarded_set update on bootstrap error

* Fix bug on message signing

* Simplify coconut feature in code and set it in validator-api

* Update some local consts

* Link clients to credential storage

* Simplify sql query and change socks5 too

* Update attr handling such that public ones are usable

* Normalize test addresses

* Fix clippy

* Merge storages for (non)coconut creds

* Fmt miss

* Disable wasm client support for now

Co-authored-by: durch <durch@users.noreply.github.com>
This commit is contained in:
Bogdan-Ștefan Neacşu
2022-04-14 14:25:21 +02:00
committed by GitHub
parent 94be4c71a4
commit 76a61cb3ae
83 changed files with 4293 additions and 553 deletions
Generated
+19 -1
View File
@@ -617,6 +617,7 @@ dependencies = [
name = "coconut-bandwidth-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
@@ -904,6 +905,7 @@ dependencies = [
"clap 3.1.8",
"coconut-bandwidth-contract-common",
"coconut-interface",
"credential-storage",
"credentials",
"crypto",
"network-defaults",
@@ -917,6 +919,18 @@ dependencies = [
"validator-client",
]
[[package]]
name = "credential-storage"
version = "0.1.0"
dependencies = [
"async-trait",
"log",
"nymcoconut",
"sqlx",
"thiserror",
"tokio",
]
[[package]]
name = "credentials"
version = "0.1.0"
@@ -1929,8 +1943,9 @@ dependencies = [
name = "gateway-client"
version = "0.1.0"
dependencies = [
"async-trait",
"coconut-interface",
"cosmrs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"credential-storage",
"credentials",
"crypto",
"fluvio-wasm-timer",
@@ -3034,6 +3049,7 @@ dependencies = [
"client-core",
"coconut-interface",
"config",
"credential-storage",
"credentials",
"crypto",
"dirs",
@@ -3173,6 +3189,7 @@ dependencies = [
"client-core",
"coconut-interface",
"config",
"credential-storage",
"credentials",
"crypto",
"dirs",
@@ -3213,6 +3230,7 @@ dependencies = [
"coconut-interface",
"config",
"console-subscriber",
"credential-storage",
"credentials",
"crypto",
"dirs",
+1
View File
@@ -22,6 +22,7 @@ members = [
"common/client-libs/gateway-client",
"common/client-libs/mixnet-client",
"common/client-libs/validator-client",
"common/credential-storage",
"common/coconut-interface",
"common/config",
"common/credentials",
+9 -23
View File
@@ -103,15 +103,8 @@ impl<T: NymConfig> Config<T> {
self::Client::<T>::default_reply_encryption_key_store_path(&id);
}
#[cfg(not(feature = "coconut"))]
if self
.client
.backup_bandwidth_token_keys_dir
.as_os_str()
.is_empty()
{
self.client.backup_bandwidth_token_keys_dir =
self::Client::<T>::default_backup_bandwidth_token_keys_dir(&id);
if self.client.database_path.as_os_str().is_empty() {
self.client.database_path = self::Client::<T>::default_database_path(&id);
}
self.client.id = id;
@@ -213,9 +206,8 @@ impl<T: NymConfig> Config<T> {
self.client.gateway_endpoint.gateway_listener.clone()
}
#[cfg(not(feature = "coconut"))]
pub fn get_backup_bandwidth_token_keys_dir(&self) -> PathBuf {
self.client.backup_bandwidth_token_keys_dir.clone()
pub fn get_database_path(&self) -> PathBuf {
self.client.database_path.clone()
}
#[cfg(not(feature = "coconut"))]
@@ -337,11 +329,8 @@ pub struct Client<T> {
/// Information regarding how the client should send data to gateway.
gateway_endpoint: GatewayEndpoint,
/// Path to directory containing public/private keys used for bandwidth token purchase.
/// Those are saved in case of emergency, to be able to reclaim bandwidth tokens.
/// The public key is the name of the file, while the private key is the content.
#[cfg(not(feature = "coconut"))]
backup_bandwidth_token_keys_dir: PathBuf,
/// Path to the database containing bandwidth credentials of this client.
database_path: PathBuf,
/// Ethereum private key.
#[cfg(not(feature = "coconut"))]
@@ -375,8 +364,7 @@ impl<T: NymConfig> Default for Client<T> {
ack_key_file: Default::default(),
reply_encryption_key_store_path: Default::default(),
gateway_endpoint: Default::default(),
#[cfg(not(feature = "coconut"))]
backup_bandwidth_token_keys_dir: Default::default(),
database_path: Default::default(),
#[cfg(not(feature = "coconut"))]
eth_private_key: "".to_string(),
#[cfg(not(feature = "coconut"))]
@@ -415,10 +403,8 @@ impl<T: NymConfig> Client<T> {
fn default_reply_encryption_key_store_path(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("reply_key_store")
}
#[cfg(not(feature = "coconut"))]
fn default_backup_bandwidth_token_keys_dir(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("backup_bandwidth_token_keys")
fn default_database_path(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("db.sqlite")
}
}
+1
View File
@@ -20,6 +20,7 @@ tokio = { version = "1.4", features = ["rt-multi-thread", "net", "signal", "macr
coconut-bandwidth-contract-common = { path = "../../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" }
coconut-interface = { path = "../../common/coconut-interface" }
credentials = { path = "../../common/credentials" }
credential-storage = { path = "../../common/credential-storage" }
crypto = { path = "../../common/crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] }
network-defaults = { path = "../../common/network-defaults" }
pemstore = { path = "../../common/pemstore" }
+15 -18
View File
@@ -9,6 +9,8 @@ use std::str::FromStr;
use url::Url;
use coconut_interface::{Attribute, Base58, BlindSignRequest, Bytable, Parameters};
use credential_storage::storage::Storage;
use credential_storage::PersistentStorage;
use credentials::coconut::bandwidth::{BandwidthVoucher, TOTAL_ATTRIBUTES};
use credentials::coconut::utils::obtain_aggregate_signature;
use crypto::asymmetric::{encryption, identity};
@@ -32,7 +34,7 @@ pub(crate) enum Commands {
#[async_trait]
pub(crate) trait Execute {
async fn execute(&self, db: &mut PickleDb) -> Result<()>;
async fn execute(&self, db: &mut PickleDb, shared_storage: PersistentStorage) -> Result<()>;
}
#[derive(Args, Clone)]
@@ -44,7 +46,7 @@ pub(crate) struct Deposit {
#[async_trait]
impl Execute for Deposit {
async fn execute(&self, db: &mut PickleDb) -> Result<()> {
async fn execute(&self, db: &mut PickleDb, _shared_storage: PersistentStorage) -> Result<()> {
let mut rng = OsRng;
let signing_keypair = KeyPair::from(identity::KeyPair::new(&mut rng));
let encryption_keypair = KeyPair::from(encryption::KeyPair::new(&mut rng));
@@ -80,7 +82,7 @@ pub(crate) struct ListDeposits {}
#[async_trait]
impl Execute for ListDeposits {
async fn execute(&self, db: &mut PickleDb) -> Result<()> {
async fn execute(&self, db: &mut PickleDb, _shared_storage: PersistentStorage) -> Result<()> {
for kv in db.iter() {
println!("{:?}", kv.get_value::<State>());
}
@@ -102,7 +104,7 @@ pub(crate) struct GetCredential {
#[async_trait]
impl Execute for GetCredential {
async fn execute(&self, db: &mut PickleDb) -> Result<()> {
async fn execute(&self, db: &mut PickleDb, shared_storage: PersistentStorage) -> Result<()> {
let mut state = db
.get::<State>(&self.tx_hash)
.ok_or(CredentialClientError::NoDeposit)?;
@@ -162,6 +164,15 @@ impl Execute for GetCredential {
let signature =
obtain_aggregate_signature(&params, &bandwidth_credential_attributes, &urls).await?;
shared_storage
.insert_coconut_credential(
state.amount.to_string(),
VOUCHER_INFO.to_string(),
bandwidth_credential_attributes.get_private_attributes()[0].to_bs58(),
bandwidth_credential_attributes.get_private_attributes()[1].to_bs58(),
signature.to_bs58(),
)
.await?;
state.signature = Some(signature.to_bs58());
db.set(&self.tx_hash, &state).unwrap();
@@ -170,17 +181,3 @@ impl Execute for GetCredential {
Ok(())
}
}
#[derive(Args, Clone)]
pub(crate) struct SpendCredential {
/// Spend one of the acquired credentials
#[clap(long)]
id: usize,
}
#[async_trait]
impl Execute for SpendCredential {
async fn execute(&self, _db: &mut PickleDb) -> Result<()> {
Ok(())
}
}
+4
View File
@@ -3,6 +3,7 @@
use thiserror::Error;
use credential_storage::error::StorageError;
use credentials::error::Error as CredentialError;
use crypto::asymmetric::encryption::KeyRecoveryError;
use crypto::asymmetric::identity::Ed25519RecoveryError;
@@ -38,4 +39,7 @@ pub enum CredentialClientError {
#[error("Could not parse X25519 data")]
X25519ParseError(#[from] KeyRecoveryError),
#[error("Could not use shared storage")]
SharedStorageError(#[from] StorageError),
}
+7 -5
View File
@@ -15,9 +15,9 @@ cfg_if::cfg_if! {
use clap::Parser;
use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
pub const MNEMONIC: &str = "sun surge soon stomach flavor country gorilla dress oblige stamp attract hip soldier agree steel prize nuclear know enjoy arm bargain always theme matter";
pub const MNEMONIC: &str = "jazz fatigue diagram account outer wrist slide cherry mother grid network pause wolf pig round answer mail junior better hair dismiss toward access end";
pub const NYMD_URL: &str = "http://127.0.0.1:26657";
pub const CONTRACT_ADDRESS: &str = "nymt1vhjnzk9ly03dugffvzfcwgry4dgc8x0sscmfl2";
pub const CONTRACT_ADDRESS: &str = "nymt1nc5tatafv6eyq7llkr2gv50ff9e22mnfp9pc5s";
pub const SIGNER_AUTHORITIES: [&str; 1] = [
"http://127.0.0.1:8080",
];
@@ -32,6 +32,8 @@ cfg_if::cfg_if! {
#[tokio::main]
async fn main() -> Result<()> {
let args = Cli::parse();
let shared_storage = credential_storage::initialise_storage(std::path::PathBuf::from("/tmp/credential.db")).await;
let mut db = match PickleDb::load(
"credential.db",
PickleDbDumpPolicy::AutoDump,
@@ -46,9 +48,9 @@ cfg_if::cfg_if! {
};
match &args.command {
Commands::Deposit(m) => m.execute(&mut db).await?,
Commands::ListDeposits(m) => m.execute(&mut db).await?,
Commands::GetCredential(m) => m.execute(&mut db).await?,
Commands::Deposit(m) => m.execute(&mut db, shared_storage).await?,
Commands::ListDeposits(m) => m.execute(&mut db, shared_storage).await?,
Commands::GetCredential(m) => m.execute(&mut db, shared_storage).await?,
}
Ok(())
+3 -2
View File
@@ -34,6 +34,7 @@ tokio-tungstenite = "0.14" # websocket
client-core = { path = "../client-core" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
credentials = { path = "../../common/credentials", optional = true }
credential-storage = { path = "../../common/credential-storage" }
config = { path = "../../common/config" }
crypto = { path = "../../common/crypto" }
gateway-client = { path = "../../common/client-libs/gateway-client" }
@@ -42,12 +43,12 @@ nymsphinx = { path = "../../common/nymsphinx" }
pemstore = { path = "../../common/pemstore" }
topology = { path = "../../common/topology" }
websocket-requests = { path = "websocket-requests" }
validator-client = { path = "../../common/client-libs/validator-client" }
validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] }
version-checker = { path = "../../common/version-checker" }
network-defaults = { path = "../../common/network-defaults" }
[features]
coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut"]
coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut", "client-core/coconut"]
eth = []
[dev-dependencies]
+2 -4
View File
@@ -46,10 +46,8 @@ public_encryption_key_file = '{{ client.public_encryption_key_file }}'
# sent but not received back.
reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}'
# Path to directory containing public/private keys used for bandwidth token purchase.
# Those are saved in case of emergency, to be able to reclaim bandwidth tokens.
# The public key is the name of the file, while the private key is the content.
backup_bandwidth_token_keys_dir = '{{ client.backup_bandwidth_token_keys_dir }}'
# Path to the database containing bandwidth credentials
database_path = '{{ client.database_path }}'
# Ethereum private key.
eth_private_key = '{{ client.eth_private_key }}'
+4 -2
View File
@@ -174,14 +174,16 @@ impl NymClient {
#[cfg(feature = "coconut")]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_validator_api_endpoints(),
*self.key_manager.identity_keypair().public_key(),
);
#[cfg(not(feature = "coconut"))]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_eth_endpoint(),
self.config.get_base().get_eth_private_key(),
self.config.get_base().get_backup_bandwidth_token_keys_dir(),
)
.expect("Could not create bandwidth controller");
-53
View File
@@ -4,21 +4,10 @@
use clap::{App, Arg, ArgMatches};
use client_core::client::key_manager::KeyManager;
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
#[cfg(feature = "coconut")]
use coconut_interface::{Credential, Parameters};
use config::NymConfig;
#[cfg(feature = "coconut")]
use credentials::coconut::{
bandwidth::prepare_for_spending, bandwidth::BandwidthVoucher, bandwidth::TOTAL_ATTRIBUTES,
utils::obtain_aggregate_signature,
};
#[cfg(feature = "coconut")]
use credentials::obtain_aggregate_verification_key;
use crypto::asymmetric::{encryption, identity};
use gateway_client::GatewayClient;
use gateway_requests::registration::handshake::SharedKeys;
#[cfg(feature = "coconut")]
use network_defaults::{BANDWIDTH_VALUE, VOUCHER_INFO};
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use rand::rngs::OsRng;
@@ -29,8 +18,6 @@ use std::sync::Arc;
use std::time::Duration;
use topology::{filter::VersionFilterable, gateway};
use url::Url;
#[cfg(feature = "coconut")]
use validator_client::nymd::tx::Hash;
use crate::client::config::Config;
use crate::commands::override_config;
@@ -101,46 +88,6 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
app
}
// this behaviour should definitely be changed, we shouldn't
// need to get bandwidth credential for registration
#[cfg(feature = "coconut")]
async fn _prepare_temporary_credential(validators: &[Url], raw_identity: &[u8]) -> Credential {
let verification_key = obtain_aggregate_verification_key(validators)
.await
.expect("could not obtain aggregate verification key of validators");
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
let mut rng = OsRng;
let bandwidth_credential_attributes = BandwidthVoucher::new(
&params,
BANDWIDTH_VALUE.to_string(),
VOUCHER_INFO.to_string(),
Hash::new([0; 32]),
// workaround for putting a valid value here, without deriving clone for the private
// key, until we have actual useful values
identity::PrivateKey::from_base58_string(
identity::KeyPair::new(&mut rng)
.private_key()
.to_base58_string(),
)
.unwrap(),
encryption::KeyPair::new(&mut rng).private_key().clone(),
);
let bandwidth_credential =
obtain_aggregate_signature(&params, &bandwidth_credential_attributes, validators)
.await
.expect("could not obtain bandwidth credential");
prepare_for_spending(
raw_identity,
&bandwidth_credential,
&bandwidth_credential_attributes,
&verification_key,
)
.expect("could not prepare out bandwidth credential for spending")
}
async fn register_with_gateway(
gateway: &gateway::Node,
our_identity: Arc<identity::KeyPair>,
+3 -2
View File
@@ -27,6 +27,7 @@ url = "2.2"
client-core = { path = "../client-core" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
credentials = { path = "../../common/credentials", optional = true }
credential-storage = { path = "../../common/credential-storage" }
config = { path = "../../common/config" }
crypto = { path = "../../common/crypto" }
gateway-client = { path = "../../common/client-libs/gateway-client" }
@@ -37,12 +38,12 @@ socks5-requests = { path = "../../common/socks5/requests" }
topology = { path = "../../common/topology" }
pemstore = { path = "../../common/pemstore" }
proxy-helpers = { path = "../../common/socks5/proxy-helpers" }
validator-client = { path = "../../common/client-libs/validator-client" }
validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] }
version-checker = { path = "../../common/version-checker" }
network-defaults = { path = "../../common/network-defaults" }
[features]
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut"]
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut", "client-core/coconut"]
eth = []
[build-dependencies]
+2 -4
View File
@@ -46,10 +46,8 @@ public_encryption_key_file = '{{ client.public_encryption_key_file }}'
# sent but not received back.
reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}'
# Path to directory containing public/private keys used for bandwidth token purchase.
# Those are saved in case of emergency, to be able to reclaim bandwidth tokens.
# The public key is the name of the file, while the private key is the content.
backup_bandwidth_token_keys_dir = '{{ client.backup_bandwidth_token_keys_dir }}'
# Path to the database containing bandwidth credentials
database_path = '{{ client.database_path }}'
# Ethereum private key.
eth_private_key = '{{ client.eth_private_key }}'
+4 -2
View File
@@ -162,14 +162,16 @@ impl NymClient {
#[cfg(feature = "coconut")]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_validator_api_endpoints(),
*self.key_manager.identity_keypair().public_key(),
);
#[cfg(not(feature = "coconut"))]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_eth_endpoint(),
self.config.get_base().get_eth_private_key(),
self.config.get_base().get_backup_bandwidth_token_keys_dir(),
)
.expect("Could not create bandwidth controller");
-53
View File
@@ -4,21 +4,10 @@
use clap::{App, Arg, ArgMatches};
use client_core::client::key_manager::KeyManager;
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
#[cfg(feature = "coconut")]
use coconut_interface::{Credential, Parameters};
use config::NymConfig;
#[cfg(feature = "coconut")]
use credentials::coconut::{
bandwidth::prepare_for_spending, bandwidth::BandwidthVoucher, bandwidth::TOTAL_ATTRIBUTES,
utils::obtain_aggregate_signature,
};
#[cfg(feature = "coconut")]
use credentials::obtain_aggregate_verification_key;
use crypto::asymmetric::{encryption, identity};
use gateway_client::GatewayClient;
use gateway_requests::registration::handshake::SharedKeys;
#[cfg(feature = "coconut")]
use network_defaults::BANDWIDTH_VALUE;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use rand::{prelude::SliceRandom, rngs::OsRng, thread_rng};
@@ -27,8 +16,6 @@ use std::sync::Arc;
use std::time::Duration;
use topology::{filter::VersionFilterable, gateway};
use url::Url;
#[cfg(feature = "coconut")]
use validator_client::nymd::tx::Hash;
use crate::client::config::Config;
use crate::commands::override_config;
@@ -101,46 +88,6 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
app
}
// this behaviour should definitely be changed, we shouldn't
// need to get bandwidth credential for registration
#[cfg(feature = "coconut")]
async fn _prepare_temporary_credential(validators: &[Url], raw_identity: &[u8]) -> Credential {
let verification_key = obtain_aggregate_verification_key(validators)
.await
.expect("could not obtain aggregate verification key of validators");
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
let mut rng = OsRng;
let bandwidth_credential_attributes = BandwidthVoucher::new(
&params,
BANDWIDTH_VALUE.to_string(),
network_defaults::VOUCHER_INFO.to_string(),
Hash::new([0; 32]),
// workaround for putting a valid value here, without deriving clone for the private
// key, until we have actual useful values
identity::PrivateKey::from_base58_string(
identity::KeyPair::new(&mut rng)
.private_key()
.to_base58_string(),
)
.unwrap(),
encryption::KeyPair::new(&mut rng).private_key().clone(),
);
let bandwidth_credential =
obtain_aggregate_signature(&params, &bandwidth_credential_attributes, validators)
.await
.expect("could not obtain bandwidth credential");
prepare_for_spending(
raw_identity,
&bandwidth_credential,
&bandwidth_credential_attributes,
&verification_key,
)
.expect("could not prepare out bandwidth credential for spending")
}
async fn register_with_gateway(
gateway: &gateway::Node,
our_identity: Arc<identity::KeyPair>,
-6
View File
@@ -109,12 +109,6 @@ impl NymClient {
pub async fn initial_setup(self) -> Self {
let testnet_mode = self.testnet_mode;
#[cfg(feature = "coconut")]
let bandwidth_controller = Some(gateway_client::bandwidth::BandwidthController::new(
vec![self.validator_server.clone()],
*self.identity.public_key(),
));
#[cfg(not(feature = "coconut"))]
let bandwidth_controller = None;
let mut client = self.get_and_update_topology().await;
+5 -2
View File
@@ -17,9 +17,9 @@ url = "2.2"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
secp256k1 = "0.20.3"
web3 = { version = "0.17.0", default-features = false }
async-trait = { version = "0.1.51" }
# internal
cosmrs = { version = "0.4.1", optional = true }
credentials = { path = "../../credentials" }
crypto = { path = "../../crypto" }
gateway-requests = { path = "../../../gateway/gateway-requests" }
@@ -41,6 +41,9 @@ features = ["macros", "rt", "net", "sync", "time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
version = "0.14"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.credential-storage]
path = "../../credential-storage"
# wasm-only dependencies
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
version = "0.2"
@@ -69,6 +72,6 @@ features = ["js"]
#url = "2.1"
[features]
coconut = ["gateway-requests/coconut", "coconut-interface", "validator-client", "credentials/coconut", "cosmrs"]
coconut = ["gateway-requests/coconut", "coconut-interface", "validator-client", "credentials/coconut"]
wasm = ["web3/wasm", "web3/http", "web3/signing"]
default = ["web3/default"]
@@ -1,33 +1,35 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(target_arch = "wasm32")]
use crate::wasm_storage::{Storage, StorageError};
#[cfg(feature = "coconut")]
use cosmrs::tx::Hash;
use coconut_interface::Base58;
#[cfg(feature = "coconut")]
#[cfg(not(target_arch = "wasm32"))]
use credential_storage::error::StorageError;
#[cfg(not(target_arch = "wasm32"))]
use credential_storage::storage::Storage;
#[cfg(feature = "coconut")]
use credentials::coconut::{
bandwidth::{prepare_for_spending, BandwidthVoucher, TOTAL_ATTRIBUTES},
utils::{obtain_aggregate_signature, obtain_aggregate_verification_key},
bandwidth::prepare_for_spending, utils::obtain_aggregate_verification_key,
};
#[cfg(not(feature = "coconut"))]
use credentials::token::bandwidth::TokenCredential;
#[cfg(feature = "coconut")]
use crypto::asymmetric::encryption;
#[cfg(not(feature = "coconut"))]
use crypto::asymmetric::identity;
use network_defaults::BANDWIDTH_VALUE;
#[cfg(not(feature = "coconut"))]
use network_defaults::{
eth_contract::ETH_ERC20_JSON_ABI, eth_contract::ETH_JSON_ABI, ETH_BURN_FUNCTION_NAME,
ETH_CONTRACT_ADDRESS, ETH_ERC20_APPROVE_FUNCTION_NAME, ETH_ERC20_CONTRACT_ADDRESS,
ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN, UTOKENS_TO_BURN,
eth_contract::ETH_ERC20_JSON_ABI, eth_contract::ETH_JSON_ABI, BANDWIDTH_VALUE,
ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS, ETH_ERC20_APPROVE_FUNCTION_NAME,
ETH_ERC20_CONTRACT_ADDRESS, ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN, UTOKENS_TO_BURN,
};
#[cfg(not(feature = "coconut"))]
use pemstore::traits::PemStorableKeyPair;
#[cfg(not(feature = "coconut"))]
use rand::rngs::OsRng;
#[cfg(not(feature = "coconut"))]
use secp256k1::SecretKey;
#[cfg(not(feature = "coconut"))]
use std::io::{Read, Write};
#[cfg(not(feature = "coconut"))]
use std::str::FromStr;
#[cfg(not(feature = "coconut"))]
use web3::{
@@ -68,35 +70,35 @@ pub fn eth_erc20_contract(web3: Web3<Http>) -> Contract<Http> {
}
#[derive(Clone)]
pub struct BandwidthController {
pub struct BandwidthController<St: Storage> {
storage: St,
#[cfg(feature = "coconut")]
validator_endpoints: Vec<url::Url>,
#[cfg(feature = "coconut")]
identity: identity::PublicKey,
#[cfg(not(feature = "coconut"))]
contract: Contract<Http>,
#[cfg(not(feature = "coconut"))]
erc20_contract: Contract<Http>,
#[cfg(not(feature = "coconut"))]
eth_private_key: SecretKey,
#[cfg(not(feature = "coconut"))]
backup_bandwidth_token_keys_dir: std::path::PathBuf,
}
impl BandwidthController {
impl<St> BandwidthController<St>
where
St: Storage + Clone + 'static,
{
#[cfg(feature = "coconut")]
pub fn new(validator_endpoints: Vec<url::Url>, identity: identity::PublicKey) -> Self {
pub fn new(storage: St, validator_endpoints: Vec<url::Url>) -> Self {
BandwidthController {
storage,
validator_endpoints,
identity,
}
}
#[cfg(not(feature = "coconut"))]
pub fn new(
storage: St,
eth_endpoint: String,
eth_private_key: String,
backup_bandwidth_token_keys_dir: std::path::PathBuf,
) -> Result<Self, GatewayClientError> {
// Fail early, on invalid url
let transport =
@@ -109,60 +111,42 @@ impl BandwidthController {
.map_err(|_| GatewayClientError::InvalidEthereumPrivateKey)?;
Ok(BandwidthController {
storage,
contract,
erc20_contract,
eth_private_key,
backup_bandwidth_token_keys_dir,
})
}
#[cfg(not(feature = "coconut"))]
fn backup_keypair(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> {
std::fs::create_dir_all(&self.backup_bandwidth_token_keys_dir)?;
let file_path = self
.backup_bandwidth_token_keys_dir
.join(keypair.public_key().to_base58_string());
let mut file = std::fs::File::create(file_path)?;
file.write_all(&keypair.private_key().to_bytes())?;
async fn backup_keypair(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> {
self.storage
.insert_erc20_credential(
keypair.public_key().to_base58_string(),
keypair.private_key().to_base58_string(),
)
.await?;
Ok(())
}
#[cfg(not(feature = "coconut"))]
fn restore_keypair(&self) -> Result<identity::KeyPair, GatewayClientError> {
std::fs::create_dir_all(&self.backup_bandwidth_token_keys_dir)?;
let file = std::fs::read_dir(&self.backup_bandwidth_token_keys_dir)?
.find(|entry| {
entry
.as_ref()
.map(|entry| entry.path().is_file())
.unwrap_or(false)
})
.unwrap_or_else(|| Err(std::io::Error::from(std::io::ErrorKind::NotFound)))?;
let file_path = file.path();
let pub_key = file_path
.file_name()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?
.to_str()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?;
let mut priv_key = vec![];
std::fs::File::open(file_path.clone())?.read_to_end(&mut priv_key)?;
Ok(identity::KeyPair::from_keys(
identity::PrivateKey::from_bytes(&priv_key).unwrap(),
identity::PublicKey::from_base58_string(pub_key).unwrap(),
))
async fn restore_keypair(&self) -> Result<identity::KeyPair, GatewayClientError> {
let data = self.storage.get_next_erc20_credential().await?;
let public_key = identity::PublicKey::from_base58_string(data.public_key).unwrap();
let private_key = identity::PrivateKey::from_base58_string(data.private_key).unwrap();
Ok(identity::KeyPair::from_keys(private_key, public_key))
}
#[cfg(not(feature = "coconut"))]
fn mark_keypair_as_spent(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> {
let mut spent_dir = self.backup_bandwidth_token_keys_dir.clone();
spent_dir.push("spent");
std::fs::create_dir_all(&spent_dir)?;
let file_path_old = self
.backup_bandwidth_token_keys_dir
.join(keypair.public_key().to_base58_string());
let file_path_new = spent_dir.join(keypair.public_key().to_base58_string());
std::fs::rename(file_path_old, file_path_new)?;
async fn mark_keypair_as_spent(
&self,
keypair: &identity::KeyPair,
) -> Result<(), GatewayClientError> {
self.storage
.consume_erc20_credential(keypair.public_key().to_base58_string())
.await?;
Ok(())
}
@@ -172,39 +156,24 @@ impl BandwidthController {
&self,
) -> Result<coconut_interface::Credential, GatewayClientError> {
let verification_key = obtain_aggregate_verification_key(&self.validator_endpoints).await?;
let params = coconut_interface::Parameters::new(TOTAL_ATTRIBUTES).unwrap();
let mut rng = OsRng;
// TODO: Decide what is the value and additional info associated with the bandwidth voucher
let bandwidth_credential_attributes = BandwidthVoucher::new(
&params,
BANDWIDTH_VALUE.to_string(),
network_defaults::VOUCHER_INFO.to_string(),
Hash::new([0; 32]),
// workaround for putting a valid value here, without deriving clone for the private
// key, until we have actual useful values
identity::PrivateKey::from_base58_string(
identity::KeyPair::new(&mut rng)
.private_key()
.to_base58_string(),
)
.unwrap(),
encryption::KeyPair::new(&mut rng).private_key().clone(),
);
let bandwidth_credential = obtain_aggregate_signature(
&params,
&bandwidth_credential_attributes,
&self.validator_endpoints,
)
.await?;
// the above would presumably be loaded from a file
let bandwidth_credential = self.storage.get_next_coconut_credential().await?;
let voucher_value = u64::from_str(&bandwidth_credential.voucher_value)
.map_err(|_| StorageError::InconsistentData)?;
let voucher_info = bandwidth_credential.voucher_info.clone();
let serial_number =
coconut_interface::Attribute::try_from_bs58(bandwidth_credential.serial_number)?;
let binding_number =
coconut_interface::Attribute::try_from_bs58(bandwidth_credential.binding_number)?;
let signature =
coconut_interface::Signature::try_from_bs58(bandwidth_credential.signature)?;
// the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff)
Ok(prepare_for_spending(
&self.identity.to_bytes(),
&bandwidth_credential,
&bandwidth_credential_attributes,
voucher_value,
voucher_info,
serial_number,
binding_number,
&signature,
&verification_key,
)?)
}
@@ -215,12 +184,12 @@ impl BandwidthController {
gateway_identity: identity::PublicKey,
gateway_owner: String,
) -> Result<TokenCredential, GatewayClientError> {
let kp = match self.restore_keypair() {
let kp = match self.restore_keypair().await {
Ok(kp) => kp,
Err(_) => {
let mut rng = OsRng;
let kp = identity::KeyPair::new(&mut rng);
self.backup_keypair(&kp)?;
self.backup_keypair(&kp).await?;
kp
}
};
@@ -230,7 +199,7 @@ impl BandwidthController {
self.buy_token_credential(verification_key, signed_verification_key, gateway_owner)
.await?;
self.mark_keypair_as_spent(&kp)?;
self.mark_keypair_as_spent(&kp).await?;
let message: Vec<u8> = verification_key
.to_bytes()
@@ -9,8 +9,12 @@ pub use crate::packet_router::{
AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender,
};
use crate::socket_state::{PartiallyDelegated, SocketState};
#[cfg(target_arch = "wasm32")]
use crate::wasm_storage::PersistentStorage;
#[cfg(feature = "coconut")]
use coconut_interface::Credential;
#[cfg(not(target_arch = "wasm32"))]
use credential_storage::PersistentStorage;
#[cfg(not(feature = "coconut"))]
use credentials::token::bandwidth::TokenCredential;
use crypto::asymmetric::identity;
@@ -51,7 +55,7 @@ pub struct GatewayClient {
connection: SocketState,
packet_router: PacketRouter,
response_timeout_duration: Duration,
bandwidth_controller: Option<BandwidthController>,
bandwidth_controller: Option<BandwidthController<PersistentStorage>>,
// reconnection related variables
/// Specifies whether client should try to reconnect to gateway on connection failure.
@@ -75,7 +79,7 @@ impl GatewayClient {
mixnet_message_sender: MixnetMessageSender,
ack_sender: AcknowledgementSender,
response_timeout_duration: Duration,
bandwidth_controller: Option<BandwidthController>,
bandwidth_controller: Option<BandwidthController<PersistentStorage>>,
) -> Self {
GatewayClient {
authenticated: false,
+14 -4
View File
@@ -1,6 +1,10 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(target_arch = "wasm32")]
use crate::wasm_storage::StorageError;
#[cfg(not(target_arch = "wasm32"))]
use credential_storage::error::StorageError;
use gateway_requests::registration::handshake::error::HandshakeError;
use std::io;
use thiserror::Error;
@@ -21,15 +25,18 @@ pub enum GatewayClientError {
#[error("There was a network error - {0}")]
NetworkError(#[from] WsError),
#[error("There was a credential storage error - {0}")]
CredentialStorageError(#[from] StorageError),
#[cfg(feature = "coconut")]
#[error("Coconut error - {0}")]
CoconutError(#[from] coconut_interface::CoconutError),
// TODO: see if `JsValue` is a reasonable type for this
#[cfg(target_arch = "wasm32")]
#[error("There was a network error")]
NetworkErrorWasm(JsValue),
#[cfg(not(feature = "coconut"))]
#[error("Keypair IO error - {0}")]
IOError(#[from] std::io::Error),
#[cfg(not(feature = "coconut"))]
#[error("Could not burn ERC20 token in Ethereum smart contract - {0}")]
BurnTokenError(#[from] Web3Error),
@@ -69,6 +76,9 @@ pub enum GatewayClientError {
#[error("Client does not have enough bandwidth: estimated {0}, remaining: {1}")]
NotEnoughBandwidth(i64, i64),
#[error("There are no more bandwidth credentials acquired. Please buy some more if you want to use the mixnet")]
NoMoreBandwidthCredentials,
#[error("Received an unexpected response")]
UnexpectedResponse,
@@ -13,6 +13,8 @@ pub mod client;
pub mod error;
pub mod packet_router;
pub mod socket_state;
#[cfg(feature = "wasm")]
mod wasm_storage;
/// Helper method for reading from websocket stream. Helps to flatten the structure.
pub(crate) fn cleanup_socket_message(
@@ -0,0 +1,98 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StorageError {
#[error("Wasm client is not yet supported")]
WasmNotSupported,
#[allow(dead_code)]
#[error("Code shouldn't reach this point")]
InconsistentData,
}
#[derive(Clone)]
pub struct PersistentStorage {}
pub struct CoconutCredential {
pub id: i64,
pub voucher_value: String,
pub voucher_info: String,
pub serial_number: String,
pub binding_number: String,
pub signature: String,
}
pub struct ERC20Credential {
pub id: i64,
pub public_key: String,
pub private_key: String,
pub consumed: bool,
}
#[async_trait]
pub trait Storage: Send + Sync {
async fn insert_coconut_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
) -> Result<(), StorageError>;
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError>;
async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError>;
async fn insert_erc20_credential(
&self,
public_key: String,
private_key: String,
) -> Result<(), StorageError>;
async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, StorageError>;
async fn consume_erc20_credential(&self, public_key: String) -> Result<(), StorageError>;
}
#[async_trait]
impl Storage for PersistentStorage {
async fn insert_coconut_credential(
&self,
_voucher_value: String,
_voucher_info: String,
_serial_number: String,
_binding_number: String,
_signature: String,
) -> Result<(), StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn remove_coconut_credential(&self, _id: i64) -> Result<(), StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn insert_erc20_credential(
&self,
_public_key: String,
_private_key: String,
) -> Result<(), StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, StorageError> {
Err(StorageError::WasmNotSupported)
}
async fn consume_erc20_credential(&self, _public_key: String) -> Result<(), StorageError> {
Err(StorageError::WasmNotSupported)
}
}
+6
View File
@@ -10,4 +10,10 @@ pub enum CoconutInterfaceError {
#[error("Could not decode base 58 string - {0}")]
MalformedString(#[from] bs58::decode::Error),
#[error("Not enough public attributes were specified")]
NotEnoughPublicAttributes,
#[error("Could not recover bandwidth value")]
InvalidBandwidth,
}
+16 -3
View File
@@ -5,6 +5,7 @@ pub mod error;
use getset::{CopyGetters, Getters};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use error::CoconutInterfaceError;
@@ -24,9 +25,11 @@ impl Credential {
pub fn new(
n_params: u32,
theta: Theta,
public_attributes: Vec<Vec<u8>>,
voucher_value: String,
voucher_info: String,
signature: &Signature,
) -> Credential {
let public_attributes = vec![voucher_value.into_bytes(), voucher_info.into_bytes()];
Credential {
n_params,
theta,
@@ -35,8 +38,18 @@ impl Credential {
}
}
pub fn public_attributes(&self) -> Vec<Vec<u8>> {
self.public_attributes.clone()
pub fn voucher_value(&self) -> Result<u64, CoconutInterfaceError> {
let bandwidth_vec = self
.public_attributes
.get(0)
.ok_or(CoconutInterfaceError::NotEnoughPublicAttributes)?
.to_owned();
let bandwidth_str = String::from_utf8(bandwidth_vec)
.map_err(|_| CoconutInterfaceError::InvalidBandwidth)?;
let value =
u64::from_str(&bandwidth_str).map_err(|_| CoconutInterfaceError::InvalidBandwidth)?;
Ok(value)
}
pub fn verify(&self, verification_key: &VerificationKey) -> bool {
+15 -5
View File
@@ -4,6 +4,8 @@
use handlebars::Handlebars;
use serde::de::DeserializeOwned;
use serde::Serialize;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::{fs, io};
@@ -64,11 +66,19 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
None => fs::create_dir_all(self.config_directory()),
}?;
fs::write(
custom_location
.unwrap_or_else(|| self.config_directory().join(Self::config_file_name())),
templated_config,
)
let location = custom_location
.unwrap_or_else(|| self.config_directory().join(Self::config_file_name()));
fs::write(location.clone(), templated_config)?;
#[cfg(unix)]
let mut perms = fs::metadata(location.clone())?.permissions();
#[cfg(unix)]
perms.set_mode(0o600);
#[cfg(unix)]
fs::set_permissions(location, perms)?;
Ok(())
}
fn load_from_file(id: Option<&str>) -> io::Result<Self> {
@@ -6,5 +6,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = "1.0.0-beta6"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
@@ -1,18 +1,23 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Coin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::deposit::DepositData;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {}
pub struct InstantiateMsg {
pub multisig_addr: String,
pub pool_addr: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
DepositFunds { data: DepositData },
ReleaseFunds { funds: Coin },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "credential-storage"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { version = "0.1.51" }
nymcoconut = { path = "../nymcoconut" }
log = "0.4"
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]}
thiserror = "1.0"
tokio = { version = "1.4", features = [ "rt-multi-thread", "net", "signal", "fs" ] }
[build-dependencies]
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
tokio = { version = "1.4", features = ["rt-multi-thread", "macros"] }
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
use sqlx::{Connection, SqliteConnection};
use std::env;
#[tokio::main]
async fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let database_path = format!("{}/coconut-credential-example.sqlite", out_dir);
let mut conn = SqliteConnection::connect(&*format!("sqlite://{}?mode=rwc", database_path))
.await
.expect("Failed to create SQLx database connection");
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
#[cfg(target_family = "windows")]
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
}
@@ -0,0 +1,22 @@
/*
* Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE coconut_credentials
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
voucher_value TEXT NOT NULL,
voucher_info TEXT NOT NULL,
serial_number TEXT NOT NULL,
binding_number TEXT NOT NULL,
signature TEXT NOT NULL UNIQUE
);
CREATE TABLE erc20_credentials
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL,
consumed BOOLEAN NOT NULL
);
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CoconutCredential;
#[derive(Clone)]
pub(crate) struct CoconutCredentialManager {
connection_pool: sqlx::SqlitePool,
}
impl CoconutCredentialManager {
/// Creates new instance of the `CoconutCredentialManager` with the provided sqlite connection pool.
///
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self {
CoconutCredentialManager { connection_pool }
}
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `voucher_value`: Plaintext bandwidth value of the credential.
/// * `voucher_info`: Plaintext information of the credential.
/// * `serial_number`: Base58 representation of the serial number attribute.
/// * `binding_number`: Base58 representation of the binding number attribute.
/// * `signature`: Coconut credential in the form of a signature.
pub(crate) async fn insert_coconut_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO coconut_credentials(voucher_value, voucher_info, serial_number, binding_number, signature) VALUES (?, ?, ?, ?, ?)",
voucher_value, voucher_info, serial_number, binding_number, signature
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Tries to retrieve one of the stored, unused credentials.
pub(crate) async fn get_next_coconut_credential(
&self,
) -> Result<CoconutCredential, sqlx::Error> {
sqlx::query_as!(CoconutCredential, "SELECT * FROM coconut_credentials")
.fetch_one(&self.connection_pool)
.await
}
/// Removes from the database the specified credential.
///
/// # Arguments
///
/// * `id`: Database id.
pub(crate) async fn remove_coconut_credential(&self, id: i64) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM coconut_credentials WHERE id = ?", id)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::ERC20Credential;
#[derive(Clone)]
pub(crate) struct ERC20CredentialManager {
connection_pool: sqlx::SqlitePool,
}
impl ERC20CredentialManager {
/// Creates new instance of the `ERC20CredentialManager` with the provided sqlite connection pool.
///
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self {
ERC20CredentialManager { connection_pool }
}
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `public_key`: Base58 representation of a public key.
/// * `private_key`: Base58 representation of a private key.
pub(crate) async fn insert_erc20_credential(
&self,
public_key: String,
private_key: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO erc20_credentials(public_key, private_key, consumed) VALUES (?, ?, ?)",
public_key,
private_key,
false,
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Tries to retrieve one of the stored, unused credentials.
pub(crate) async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, sqlx::Error> {
sqlx::query_as!(
ERC20Credential,
"SELECT * FROM erc20_credentials WHERE consumed = false"
)
.fetch_one(&self.connection_pool)
.await
}
/// Mark a credential as being consumed.
pub(crate) async fn consume_erc20_credential(
&self,
public_key: String,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE erc20_credentials
SET consumed = true
WHERE public_key = ?
"#,
public_key
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Error, Debug)]
pub enum StorageError {
#[error("Database experienced an internal error - {0}")]
InternalDatabaseError(#[from] sqlx::Error),
#[error("Failed to perform database migration - {0}")]
MigrationError(#[from] sqlx::migrate::MigrateError),
#[error("Inconsistent data in database")]
InconsistentData,
}
+144
View File
@@ -0,0 +1,144 @@
/*
* Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
use crate::coconut::CoconutCredentialManager;
use crate::erc20::ERC20CredentialManager;
use crate::error::StorageError;
use crate::storage::Storage;
use crate::models::{CoconutCredential, ERC20Credential};
use async_trait::async_trait;
use log::{debug, error};
use sqlx::ConnectOptions;
use std::path::{Path, PathBuf};
mod coconut;
mod erc20;
pub mod error;
mod models;
pub mod storage;
// note that clone here is fine as upon cloning the same underlying pool will be used
#[derive(Clone)]
pub struct PersistentStorage {
coconut_credential_manager: CoconutCredentialManager,
erc20_credential_manager: ERC20CredentialManager,
}
impl PersistentStorage {
/// Initialises `PersistentStorage` using the provided path.
///
/// # Arguments
///
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StorageError> {
debug!(
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
let mut opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(database_path)
.create_if_missing(true);
opts.disable_statement_logging();
let connection_pool = match sqlx::SqlitePool::connect_with(opts).await {
Ok(db) => db,
Err(err) => {
error!("Failed to connect to SQLx database: {}", err);
return Err(err.into());
}
};
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
error!("Failed to perform migration on the SQLx database: {}", err);
return Err(err.into());
}
Ok(PersistentStorage {
coconut_credential_manager: CoconutCredentialManager::new(connection_pool.clone()),
erc20_credential_manager: ERC20CredentialManager::new(connection_pool),
})
}
}
#[async_trait]
impl Storage for PersistentStorage {
async fn insert_coconut_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.insert_coconut_credential(
voucher_value,
voucher_info,
serial_number,
binding_number,
signature,
)
.await?;
Ok(())
}
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError> {
let credential = self
.coconut_credential_manager
.get_next_coconut_credential()
.await?;
Ok(credential)
}
async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError> {
self.coconut_credential_manager
.remove_coconut_credential(id)
.await?;
Ok(())
}
async fn insert_erc20_credential(
&self,
public_key: String,
private_key: String,
) -> Result<(), StorageError> {
self.erc20_credential_manager
.insert_erc20_credential(public_key, private_key)
.await?;
Ok(())
}
async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, StorageError> {
let credential = self
.erc20_credential_manager
.get_next_erc20_credential()
.await?;
Ok(credential)
}
async fn consume_erc20_credential(&self, public_key: String) -> Result<(), StorageError> {
let credential = self
.erc20_credential_manager
.consume_erc20_credential(public_key)
.await?;
Ok(credential)
}
}
pub async fn initialise_storage(path: PathBuf) -> PersistentStorage {
match PersistentStorage::init(path).await {
Err(err) => panic!("failed to initialise credential storage - {}", err),
Ok(storage) => storage,
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub struct CoconutCredential {
#[allow(dead_code)]
pub id: i64,
pub voucher_value: String,
pub voucher_info: String,
pub serial_number: String,
pub binding_number: String,
pub signature: String,
}
pub struct ERC20Credential {
#[allow(dead_code)]
pub id: i64,
pub public_key: String,
pub private_key: String,
pub consumed: bool,
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use crate::models::{CoconutCredential, ERC20Credential};
use crate::StorageError;
#[async_trait]
pub trait Storage: Send + Sync {
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `signature`: Coconut credential in the form of a signature.
async fn insert_coconut_credential(
&self,
voucher_value: String,
voucher_info: String,
serial_number: String,
binding_number: String,
signature: String,
) -> Result<(), StorageError>;
/// Tries to retrieve one of the stored, unused credentials.
async fn get_next_coconut_credential(&self) -> Result<CoconutCredential, StorageError>;
/// Removes from the database the specified credential.
///
/// # Arguments
///
/// * `signature`: Coconut credential in the form of a signature.
async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError>;
/// Inserts provided signature into the database.
///
/// # Arguments
///
/// * `public_key`: Base58 representation of a public key.
/// * `private_key`: Base58 representation of a private key.
async fn insert_erc20_credential(
&self,
public_key: String,
private_key: String,
) -> Result<(), StorageError>;
/// Tries to retrieve one of the stored, unused credential data.
async fn get_next_erc20_credential(&self) -> Result<ERC20Credential, StorageError>;
/// Mark a credential as being consumed.
async fn consume_erc20_credential(&self, public_key: String) -> Result<(), StorageError>;
}
+9 -12
View File
@@ -11,7 +11,6 @@ use coconut_interface::{
PrivateAttribute, PublicAttribute, Signature, VerificationKey,
};
use crypto::asymmetric::{encryption, identity};
use network_defaults::BANDWIDTH_VALUE;
use cosmrs::tx::Hash;
@@ -159,29 +158,27 @@ impl BandwidthVoucher {
pub fn sign(&self, request: &BlindSignRequest) -> identity::Signature {
let mut message = request.to_bytes();
message.extend_from_slice(self.tx_hash.as_bytes());
message.extend_from_slice(self.tx_hash.to_string().as_bytes());
self.signing_key.sign(&message)
}
}
pub fn prepare_for_spending(
raw_identity: &[u8],
voucher_value: u64,
voucher_info: String,
serial_number: PrivateAttribute,
binding_number: PrivateAttribute,
signature: &Signature,
attributes: &BandwidthVoucher,
verification_key: &VerificationKey,
) -> Result<Credential, Error> {
let public_attributes = vec![
raw_identity.to_vec(),
BANDWIDTH_VALUE.to_be_bytes().to_vec(),
];
let params = Parameters::new(TOTAL_ATTRIBUTES)?;
prepare_credential_for_spending(
&params,
public_attributes,
attributes.serial_number,
attributes.binding_number,
voucher_value,
voucher_info,
serial_number,
binding_number,
signature,
verification_key,
)
+6 -4
View File
@@ -11,7 +11,7 @@ use crypto::shared_key::recompute_shared_key;
use crypto::symmetric::stream_cipher;
use url::Url;
use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES};
use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES};
use crate::coconut::params::{
ValidatorApiCredentialEncryptionAlgorithm, ValidatorApiCredentialHkdfAlgorithm,
};
@@ -175,7 +175,8 @@ pub async fn obtain_aggregate_signature(
// TODO: better type flow
pub fn prepare_credential_for_spending(
params: &Parameters,
public_attributes: Vec<Vec<u8>>,
voucher_value: u64,
voucher_info: String,
serial_number: Attribute,
binding_number: Attribute,
signature: &Signature,
@@ -190,9 +191,10 @@ pub fn prepare_credential_for_spending(
)?;
Ok(Credential::new(
(public_attributes.len() + PRIVATE_ATTRIBUTES as usize) as u32,
PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES,
theta,
public_attributes,
voucher_value.to_string(),
voucher_info,
signature,
))
}
+7 -9
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "coconut")]
use coconut_interface::CoconutError;
use coconut_interface::{error::CoconutInterfaceError, CoconutError};
use crypto::asymmetric::encryption::KeyRecoveryError;
use validator_client::ValidatorClientError;
@@ -17,18 +17,16 @@ pub enum Error {
NoValidatorsAvailable,
#[cfg(feature = "coconut")]
#[error("Run into a coconut error - {0}")]
#[error("Ran into a coconut error - {0}")]
CoconutError(#[from] CoconutError),
#[error("Run into a validato client error - {0}")]
#[cfg(feature = "coconut")]
#[error("Ran into a coconut interface error - {0}")]
CoconutInterfaceError(#[from] CoconutInterfaceError),
#[error("Ran into a validator client error - {0}")]
ValidatorClientError(#[from] ValidatorClientError),
#[error("Not enough public attributes were specified")]
NotEnoughPublicAttributes,
#[error("Bandwidth is expected to be represented on 8 bytes")]
InvalidBandwidthSize,
#[error("Bandwidth operation overflowed. {0}")]
BandwidthOverflow(String),
+193 -2
View File
@@ -144,6 +144,12 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cc"
version = "1.0.72"
@@ -200,6 +206,9 @@ dependencies = [
"config",
"cosmwasm-std",
"cosmwasm-storage",
"cw-controllers",
"cw-multi-test",
"cw-storage-plus",
"schemars",
"serde",
"thiserror",
@@ -209,6 +218,7 @@ dependencies = [
name = "coconut-bandwidth-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
@@ -263,9 +273,9 @@ dependencies = [
[[package]]
name = "cosmwasm-schema"
version = "1.0.0-beta3"
version = "1.0.0-beta7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "818b928263c09a3269c2bed22494a62107a43ef87900e273af8ad2cb9f7e4440"
checksum = "63f79866e7b2190b6b6cb06959e308183c8d9511a8530f7292073f3cddc963db"
dependencies = [
"schemars",
"serde_json",
@@ -381,6 +391,39 @@ dependencies = [
"zeroize",
]
[[package]]
name = "cw-controllers"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc6d042b14823b0e9f33f5cdd67a1eb9b16a7d79f7547b1a73c8870b518b97b"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"cw-utils",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw-multi-test"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbea57e5be4a682268a5eca1a57efece57a54ff216bfd87603d5e864aad40e12"
dependencies = [
"anyhow",
"cosmwasm-std",
"cosmwasm-storage",
"cw-storage-plus",
"cw-utils",
"derivative",
"itertools",
"prost",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw-storage-plus"
version = "0.13.2"
@@ -392,6 +435,105 @@ dependencies = [
"serde",
]
[[package]]
name = "cw-utils"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "babd2c090f39d07ce5bf2556962305e795daa048ce20a93709eb591476e4a29e"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw2"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686da2d2b3b646bea15d448dc25c3e132097a7c40ef9ba7b4db741375b6181f"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"schemars",
"serde",
]
[[package]]
name = "cw3"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b7c7a87aed0637432c9ec39a691e533e18006dd400c67bce9f0ad643de7d52c"
dependencies = [
"cosmwasm-std",
"cw-utils",
"schemars",
"serde",
]
[[package]]
name = "cw3-fixed-multisig"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "666afbb3bcaefc697047a0be8c4c5015be053a8456d27dd26e0b8ab114f6f5dd"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"cw-utils",
"cw2",
"cw3",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw3-flex-multisig"
version = "0.13.1"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-multi-test",
"cw-storage-plus",
"cw-utils",
"cw2",
"cw3",
"cw3-fixed-multisig",
"cw4",
"cw4-group",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "cw4"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d51b6e05094bfb91029f5baf5d1f39ee10f16fd61f8bf0e6f6632a5dbfb7f9"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"schemars",
"serde",
]
[[package]]
name = "cw4-group"
version = "0.13.1"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-storage-plus",
"cw-utils",
"cw2",
"cw4",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "der"
version = "0.4.5"
@@ -401,6 +543,17 @@ dependencies = [
"const-oid",
]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.8.1"
@@ -475,6 +628,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "elliptic-curve"
version = "0.10.6"
@@ -714,6 +873,15 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "itertools"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.1"
@@ -1061,6 +1229,29 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "prost"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "quick-error"
version = "2.0.1"
+1 -1
View File
@@ -1,5 +1,5 @@
[workspace]
members = ["bandwidth-claim", "coconut-bandwidth", "mixnet", "vesting"]
members = ["bandwidth-claim", "coconut-bandwidth", "mixnet", "vesting", "multisig/cw3-flex-multisig", "multisig/cw4-group"]
[profile.release]
opt-level = 3
+7 -2
View File
@@ -13,9 +13,14 @@ bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" }
coconut-bandwidth-contract-common = { path = "../../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" }
config = { path = "../../common/config"}
cosmwasm-std = "1.0.0-beta3"
cosmwasm-storage = "1.0.0-beta3"
cosmwasm-std = "1.0.0-beta8"
cosmwasm-storage = "1.0.0-beta8"
cw-storage-plus = "0.13.2"
cw-controllers = "0.13.2"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = "1.0.23"
[dev-dependencies]
cw-multi-test = { version = "0.13.2" }
+163
View File
@@ -0,0 +1,163 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use coconut_bandwidth_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
use crate::error::ContractError;
use crate::state::{Config, ADMIN, CONFIG};
use crate::transactions;
/// Instantiate the contract.
///
/// `deps` contains Storage, API and Querier
/// `msg` is the contract initialization message, sort of like a constructor call.
#[entry_point]
pub fn instantiate(
mut deps: DepsMut<'_>,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let multisig_addr = deps.api.addr_validate(&msg.multisig_addr)?;
let pool_addr = deps.api.addr_validate(&msg.pool_addr)?;
ADMIN.set(deps.branch(), Some(multisig_addr.clone()))?;
let cfg = Config {
multisig_addr,
pool_addr,
};
CONFIG.save(deps.storage, &cfg)?;
Ok(Response::default())
}
/// Handle an incoming message
#[entry_point]
pub fn execute(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::DepositFunds { data } => transactions::deposit_funds(deps, env, info, data),
ExecuteMsg::ReleaseFunds { funds } => transactions::release_funds(deps, env, info, funds),
}
}
#[entry_point]
pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult<Binary> {
unimplemented!();
}
#[entry_point]
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::support::tests::helpers::*;
use coconut_bandwidth_contract_common::deposit::DepositData;
use config::defaults::DENOM;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coins, Addr};
use cw_multi_test::Executor;
use serde::de::Unexpected::Str;
#[test]
fn initialize_contract() {
let mut deps = mock_dependencies();
let env = mock_env();
let msg = InstantiateMsg {
multisig_addr: String::from(MULTISIG_CONTRACT),
pool_addr: String::from(POOL_CONTRACT),
};
let info = mock_info("creator", &[]);
let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
// Contract balance should be 0
assert_eq!(
coins(0, DENOM),
vec![deps
.as_ref()
.querier
.query_balance(env.contract.address, DENOM)
.unwrap()]
);
}
#[test]
fn deposit_and_release() {
let init_funds = coins(10, DENOM);
let deposit_funds = coins(1, DENOM);
let release_funds = coins(2, DENOM);
let mut app = mock_app(&init_funds);
let multisig_addr = String::from(MULTISIG_CONTRACT);
let pool_addr = String::from(POOL_CONTRACT);
let code_id = app.store_code(contract_bandwidth());
let msg = InstantiateMsg {
multisig_addr: multisig_addr.clone(),
pool_addr: pool_addr.clone(),
};
let contract_addr = app
.instantiate_contract(
code_id,
Addr::unchecked(OWNER),
&msg,
&[],
"bandwidth",
None,
)
.unwrap();
let msg = ExecuteMsg::DepositFunds {
data: DepositData::new(
String::from("info"),
String::from("id"),
String::from("enc"),
),
};
app.execute_contract(
Addr::unchecked(OWNER),
contract_addr.clone(),
&msg,
&deposit_funds,
)
.unwrap();
// try to release more then it's in the contract
let msg = ExecuteMsg::ReleaseFunds {
funds: release_funds[0].clone(),
};
let err = app
.execute_contract(
Addr::unchecked(multisig_addr.clone()),
contract_addr.clone(),
&msg,
&[],
)
.unwrap_err();
assert_eq!(ContractError::NotEnoughFunds, err.downcast().unwrap());
let msg = ExecuteMsg::ReleaseFunds {
funds: deposit_funds[0].clone(),
};
app.execute_contract(
Addr::unchecked(multisig_addr),
contract_addr.clone(),
&msg,
&[],
)
.unwrap();
let pool_bal = app.wrap().query_balance(pool_addr, DENOM).unwrap();
assert_eq!(pool_bal, deposit_funds[0]);
}
}
+7
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::StdError;
use cw_controllers::AdminError;
use thiserror::Error;
use config::defaults::DENOM;
@@ -23,4 +24,10 @@ pub enum ContractError {
#[error("Wrong coin denomination, you must send {}", DENOM)]
WrongDenom,
#[error("There aren't enough funds in the contract")]
NotEnoughFunds,
#[error("{0}")]
Admin(#[from] AdminError),
}
+2 -67
View File
@@ -1,73 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod contract;
mod error;
mod state;
mod support;
mod transactions;
use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response};
use crate::error::ContractError;
use coconut_bandwidth_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg};
/// Instantiate the contract.
///
/// `deps` contains Storage, API and Querier
/// `env` contains block, message and contract info
/// `msg` is the contract initialization message, sort of like a constructor call.
#[entry_point]
pub fn instantiate(
_deps: DepsMut<'_>,
_env: Env,
_info: MessageInfo,
_msg: InstantiateMsg,
) -> Result<Response, ContractError> {
Ok(Response::default())
}
/// Handle an incoming message
#[entry_point]
pub fn execute(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::DepositFunds { data } => transactions::deposit_funds(deps, env, info, data),
}
}
#[entry_point]
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
use config::defaults::DENOM;
use cosmwasm_std::coins;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
#[test]
fn initialize_contract() {
let mut deps = mock_dependencies();
let env = mock_env();
let msg = InstantiateMsg {};
let info = mock_info("creator", &[]);
let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
// Contract balance should be 0
assert_eq!(
coins(0, DENOM),
vec![deps
.as_ref()
.querier
.query_balance(env.contract.address, DENOM)
.unwrap()]
);
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw_controllers::Admin;
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const ADMIN: Admin = Admin::new("admin");
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Config {
pub multisig_addr: Addr,
pub pool_addr: Addr,
}
pub const CONFIG: Item<Config> = Item::new("config");
@@ -3,17 +3,44 @@
#[cfg(test)]
pub mod helpers {
use crate::instantiate;
pub const OWNER: &str = "admin0001";
pub const SOMEBODY: &str = "somebody";
pub const MULTISIG_CONTRACT: &str = "multisig contract address";
pub const POOL_CONTRACT: &str = "mix pool contract address";
use crate::contract::instantiate;
use coconut_bandwidth_contract_common::msg::InstantiateMsg;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
use cosmwasm_std::{Empty, MemoryStorage, OwnedDeps};
use cosmwasm_std::{Addr, Coin, Empty, MemoryStorage, OwnedDeps};
use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper};
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
let mut deps = mock_dependencies();
let msg = InstantiateMsg {};
let msg = InstantiateMsg {
multisig_addr: String::from(MULTISIG_CONTRACT),
pool_addr: String::from(POOL_CONTRACT),
};
let env = mock_env();
let info = mock_info("creator", &[]);
instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
return deps;
}
pub fn mock_app(init_funds: &[Coin]) -> App {
AppBuilder::new().build(|router, _, storage| {
router
.bank
.init_balance(storage, &Addr::unchecked(OWNER), init_funds.to_vec())
.unwrap();
})
}
pub fn contract_bandwidth() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new(
crate::contract::execute,
crate::contract::instantiate,
crate::contract::query,
);
Box::new(contract)
}
}
@@ -1,9 +1,11 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{DepsMut, Env, Event, MessageInfo, Response};
use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Response};
use crate::error::ContractError;
use crate::state::{ADMIN, CONFIG};
use coconut_bandwidth_contract_common::deposit::DepositData;
use coconut_bandwidth_contract_common::events::{
DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO,
@@ -37,12 +39,40 @@ pub(crate) fn deposit_funds(
Ok(Response::new().add_event(event))
}
pub(crate) fn release_funds(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
funds: Coin,
) -> Result<Response, ContractError> {
if funds.denom != DENOM {
return Err(ContractError::WrongDenom);
}
let current_balance = deps.querier.query_balance(env.contract.address, DENOM)?;
if funds.amount > current_balance.amount {
return Err(ContractError::NotEnoughFunds);
}
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
let cfg = CONFIG.load(deps.storage)?;
let return_tokens = BankMsg::Send {
to_address: cfg.pool_addr.into(),
amount: vec![funds],
};
let response = Response::new().add_message(return_tokens);
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::support::tests::helpers;
use crate::support::tests::helpers::{MULTISIG_CONTRACT, POOL_CONTRACT};
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::Coin;
use cosmwasm_std::{Coin, CosmosMsg};
use cw_controllers::AdminError;
#[test]
fn invalid_deposit() {
@@ -133,4 +163,65 @@ mod tests {
.unwrap();
assert_eq!(encryption_key_attr.value, encryption_key);
}
#[test]
fn invalid_release() {
let mut deps = helpers::init_contract();
let env = mock_env();
let invalid_admin = "invalid admin";
let funds = Coin::new(1, DENOM);
let err = release_funds(
deps.as_mut(),
env.clone(),
mock_info(invalid_admin, &[]),
Coin::new(1, "invalid denom"),
)
.unwrap_err();
assert_eq!(err, ContractError::WrongDenom);
let err = release_funds(
deps.as_mut(),
env.clone(),
mock_info(invalid_admin, &[]),
funds.clone(),
)
.unwrap_err();
assert_eq!(err, ContractError::NotEnoughFunds);
deps.querier
.update_balance(env.contract.address.clone(), vec![funds.clone()]);
let err = release_funds(
deps.as_mut(),
env.clone(),
mock_info(invalid_admin, &[]),
funds.clone(),
)
.unwrap_err();
assert_eq!(err, ContractError::Admin(AdminError::NotAdmin {}));
}
#[test]
fn valid_release() {
let mut deps = helpers::init_contract();
let env = mock_env();
let coin = Coin::new(1, DENOM);
deps.querier
.update_balance(env.contract.address.clone(), vec![coin.clone()]);
let res = release_funds(
deps.as_mut(),
env,
mock_info(MULTISIG_CONTRACT, &[]),
coin.clone(),
)
.unwrap();
assert_eq!(
res.messages[0].msg,
CosmosMsg::Bank(BankMsg::Send {
to_address: String::from(POOL_CONTRACT),
amount: vec![coin]
})
);
}
}
@@ -0,0 +1,6 @@
[alias]
wasm = "build --release --target wasm32-unknown-unknown"
wasm-debug = "build --target wasm32-unknown-unknown"
unit-test = "test --lib"
integration-test = "test --test integration"
schema = "run --example schema"
@@ -0,0 +1,34 @@
[package]
name = "cw3-flex-multisig"
version = "0.13.1"
authors = ["Ethan Frey <ethanfrey@users.noreply.github.com>"]
edition = "2018"
description = "Implementing cw3 with multiple voting patterns and dynamic groups"
license = "Apache-2.0"
repository = "https://github.com/CosmWasm/cw-plus"
homepage = "https://cosmwasm.com"
documentation = "https://docs.cosmwasm.com"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# use library feature to disable all instantiate/execute/query exports
library = []
[dependencies]
cw-utils = { version = "0.13.1" }
cw2 = { version = "0.13.1" }
cw3 = { version = "0.13.1" }
cw3-fixed-multisig = { version = "0.13.1", features = ["library"] }
cw4 = { version = "0.13.1" }
cw-storage-plus = { version = "0.13.1" }
cosmwasm-std = { version = "1.0.0-beta6" }
schemars = "0.8.1"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.23" }
[dev-dependencies]
cosmwasm-schema = { version = "1.0.0-beta6" }
cw4-group = { path = "../cw4-group", version = "0.13.1" }
cw-multi-test = { version = "0.13.1" }
@@ -0,0 +1,14 @@
CW3-Flex-Whitelist: Implementing cw3 with multiple voting patterns and dynamic groups
Copyright (C) 2020 Confio OÜ
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,85 @@
# CW3 Flexible Multisig
This builds on [cw3-fixed-multisig](../cw3-fixed-multisig) with a more
powerful implementation of the [cw3 spec](../../packages/cw3/README.md).
It is a multisig contract that is backed by a
[cw4 (group)](../../packages/cw4/README.md) contract, which independently
maintains the voter set.
This provides 2 main advantages:
* You can create two different multisigs with different voting thresholds
backed by the same group. Thus, you can have a 50% vote, and a 67% vote
that always use the same voter set, but can take other actions.
* TODO: It allows dynamic multisig groups. Since the group can change,
we can set one of the multisigs as the admin of the group contract,
and the
In addition to the dynamic voting set, the main difference with the native
Cosmos SDK multisig, is that it aggregates the signatures on chain, with
visible proposals (like `x/gov` in the Cosmos SDK), rather than requiring
signers to share signatures off chain.
## Instantiation
The first step to create such a multisig is to instantiate a cw4 contract
with the desired member set. For now, this only is supported by
[cw4-group](../cw4-group), but we will add a token-weighted group contract
(TODO).
If you create a `cw4-group` contract and want a multisig to be able
to modify its own group, do the following in multiple transactions:
* instantiate cw4-group, with your personal key as admin
* instantiate a multisig pointing to the group
* `AddHook{multisig}` on the group contract
* `UpdateAdmin{multisig}` on the group contract
This is the current practice to create such circular dependencies,
and depends on an external driver (hard to impossible to script such a
self-deploying contract on-chain). (TODO: document better).
When creating the multisig, you must set the required weight to pass a vote
as well as the max/default voting period. (TODO: allow more threshold types)
## Execution Process
First, a registered voter must submit a proposal. This also includes the
first "Yes" vote on the proposal by the proposer. The proposer can set
an expiration time for the voting process, or it defaults to the limit
provided when creating the contract (so proposals can be closed after several
days).
Before the proposal has expired, any voter with non-zero weight can add their
vote. Only "Yes" votes are tallied. If enough "Yes" votes were submitted before
the proposal expiration date, the status is set to "Passed".
Once a proposal is "Passed", anyone may submit an "Execute" message. This will
trigger the proposal to send all stored messages from the proposal and update
it's state to "Executed", so it cannot run again. (Note if the execution fails
for any reason - out of gas, insufficient funds, etc - the state update will
be reverted, and it will remain "Passed", so you can try again).
Once a proposal has expired without passing, anyone can submit a "Close"
message to mark it closed. This has no effect beyond cleaning up the UI/database.
## Running this contract
You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed.
You can run unit tests on this via:
`cargo test`
Once you are happy with the content, you can compile it to wasm via:
```
RUSTFLAGS='-C link-arg=-s' cargo wasm
cp ../../target/wasm32-unknown-unknown/release/cw3_fixed_multisig.wasm .
ls -l cw3_fixed_multisig.wasm
sha256sum cw3_fixed_multisig.wasm
```
Or for a production-ready (optimized) build, run a build command in
the repository root: https://github.com/CosmWasm/cw-plus#compiling.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
use cosmwasm_std::StdError;
use cw_utils::ThresholdError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("{0}")]
Threshold(#[from] ThresholdError),
#[error("Group contract invalid address '{addr}'")]
InvalidGroup { addr: String },
#[error("Unauthorized")]
Unauthorized {},
#[error("Proposal is not open")]
NotOpen {},
#[error("Proposal voting period has expired")]
Expired {},
#[error("Proposal must expire before you can close it")]
NotExpired {},
#[error("Wrong expiration option")]
WrongExpiration {},
#[error("Already voted on this proposal")]
AlreadyVoted {},
#[error("Proposal must have passed and not yet been executed")]
WrongExecuteStatus {},
#[error("Cannot close completed or passed proposals")]
WrongCloseStatus {},
}
@@ -0,0 +1,6 @@
pub mod contract;
pub mod error;
pub mod msg;
pub mod state;
pub use crate::error::ContractError;
@@ -0,0 +1,75 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::{CosmosMsg, Empty};
use cw3::Vote;
use cw4::MemberChangedHookMsg;
use cw_utils::{Duration, Expiration, Threshold};
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct InstantiateMsg {
// this is the group contract that contains the member list
pub group_addr: String,
pub threshold: Threshold,
pub max_voting_period: Duration,
}
// TODO: add some T variants? Maybe good enough as fixed Empty for now
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
Propose {
title: String,
description: String,
msgs: Vec<CosmosMsg<Empty>>,
// note: we ignore API-spec'd earliest if passed, always opens immediately
latest: Option<Expiration>,
},
Vote {
proposal_id: u64,
vote: Vote,
},
Execute {
proposal_id: u64,
},
Close {
proposal_id: u64,
},
/// Handles update hook messages from the group contract
MemberChangedHook(MemberChangedHookMsg),
}
// We can also add this as a cw3 extension
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
/// Return ThresholdResponse
Threshold {},
/// Returns ProposalResponse
Proposal { proposal_id: u64 },
/// Returns ProposalListResponse
ListProposals {
start_after: Option<u64>,
limit: Option<u32>,
},
/// Returns ProposalListResponse
ReverseProposals {
start_before: Option<u64>,
limit: Option<u32>,
},
/// Returns VoteResponse
Vote { proposal_id: u64, voter: String },
/// Returns VoteListResponse
ListVotes {
proposal_id: u64,
start_after: Option<String>,
limit: Option<u32>,
},
/// Returns VoterInfo
Voter { address: String },
/// Returns VoterListResponse
ListVoters {
start_after: Option<String>,
limit: Option<u32>,
},
}
@@ -0,0 +1,17 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cw4::Cw4Contract;
use cw_storage_plus::Item;
use cw_utils::{Duration, Threshold};
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Config {
pub threshold: Threshold,
pub max_voting_period: Duration,
// Total weight and voters are queried from this contract
pub group_addr: Cw4Contract,
}
// unique items
pub const CONFIG: Item<Config> = Item::new("config");
@@ -0,0 +1,5 @@
[alias]
wasm = "build --release --target wasm32-unknown-unknown"
wasm-debug = "build --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --example schema"
+38
View File
@@ -0,0 +1,38 @@
[package]
name = "cw4-group"
version = "0.13.1"
authors = ["Ethan Frey <ethanfrey@users.noreply.github.com>"]
edition = "2018"
description = "Simple cw4 implementation of group membership controlled by admin "
license = "Apache-2.0"
repository = "https://github.com/CosmWasm/cw-plus"
homepage = "https://cosmwasm.com"
documentation = "https://docs.cosmwasm.com"
exclude = [
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
"artifacts/*",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# use library feature to disable all instantiate/execute/query exports
library = []
[dependencies]
cw-utils = { version = "0.13.1" }
cw2 = { version = "0.13.1" }
cw4 = { version = "0.13.1" }
cw-controllers = { version = "0.13.1" }
cw-storage-plus = { version = "0.13.1" }
cosmwasm-std = { version = "1.0.0-beta6" }
schemars = "0.8.1"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.23" }
[dev-dependencies]
cosmwasm-schema = { version = "1.0.0-beta6" }
+14
View File
@@ -0,0 +1,14 @@
Cw4_group
Copyright (C) 2020 Confio OÜ
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+51
View File
@@ -0,0 +1,51 @@
# CW4 Group
This is a basic implementation of the [cw4 spec](../../packages/cw4/README.md).
It fulfills all elements of the spec, including the raw query lookups,
and it designed to be used as a backing storage for
[cw3 compliant contracts](../../packages/cw3/README.md).
It stores a set of members along with an admin, and allows the admin to
update the state. Raw queries (intended for cross-contract queries)
can check a given member address and the total weight. Smart queries (designed
for client API) can do the same, and also query the admin address as well as
paginate over all members.
## Init
To create it, you must pass in a list of members, as well as an optional
`admin`, if you wish it to be mutable.
```rust
pub struct InitMsg {
pub admin: Option<HumanAddr>,
pub members: Vec<Member>,
}
pub struct Member {
pub addr: HumanAddr,
pub weight: u64,
}
```
Members are defined by an address and a weight. This is transformed
and stored under their `CanonicalAddr`, in a format defined in
[cw4 raw queries](../../packages/cw4/README.md#raw).
Note that 0 *is an allowed weight*. This doesn't give any voting rights, but
it does define this address is part of the group. This could be used in
e.g. a KYC whitelist to say they are allowed, but cannot participate in
decision-making.
## Messages
Basic update messages, queries, and hooks are defined by the
[cw4 spec](../../packages/cw4/README.md). Please refer to it for more info.
`cw4-group` adds one message to control the group membership:
`UpdateMembers{add, remove}` - takes a membership diff and adds/updates the
members, as well as removing any provided addresses. If an address is on both
lists, it will be removed. If it appears multiple times in `add`, only the
last occurrence will be used.
@@ -0,0 +1,556 @@
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
attr, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult,
SubMsg,
};
use cw2::set_contract_version;
use cw4::{
Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse,
TotalWeightResponse,
};
use cw_storage_plus::Bound;
use cw_utils::maybe_addr;
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{ADMIN, HOOKS, MEMBERS, TOTAL};
// version info for migration info
const CONTRACT_NAME: &str = "crates.io:cw4-group";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
// Note, you can use StdResult in some functions where you do not
// make use of the custom errors
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
create(deps, msg.admin, msg.members, env.block.height)?;
Ok(Response::default())
}
// create is the instantiation logic with set_contract_version removed so it can more
// easily be imported in other contracts
pub fn create(
mut deps: DepsMut,
admin: Option<String>,
members: Vec<Member>,
height: u64,
) -> Result<(), ContractError> {
let admin_addr = admin
.map(|admin| deps.api.addr_validate(&admin))
.transpose()?;
ADMIN.set(deps.branch(), admin_addr)?;
let mut total = 0u64;
for member in members.into_iter() {
total += member.weight;
let member_addr = deps.api.addr_validate(&member.addr)?;
MEMBERS.save(deps.storage, &member_addr, &member.weight, height)?;
}
TOTAL.save(deps.storage, &total)?;
Ok(())
}
// And declare a custom Error variant for the ones where you will want to make use of it
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
let api = deps.api;
match msg {
ExecuteMsg::UpdateAdmin { admin } => Ok(ADMIN.execute_update_admin(
deps,
info,
admin.map(|admin| api.addr_validate(&admin)).transpose()?,
)?),
ExecuteMsg::UpdateMembers { add, remove } => {
execute_update_members(deps, env, info, add, remove)
}
ExecuteMsg::AddHook { addr } => {
Ok(HOOKS.execute_add_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?)
}
ExecuteMsg::RemoveHook { addr } => {
Ok(HOOKS.execute_remove_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?)
}
}
}
pub fn execute_update_members(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
add: Vec<Member>,
remove: Vec<String>,
) -> Result<Response, ContractError> {
let attributes = vec![
attr("action", "update_members"),
attr("added", add.len().to_string()),
attr("removed", remove.len().to_string()),
attr("sender", &info.sender),
];
// make the local update
let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?;
// call all registered hooks
let messages = HOOKS.prepare_hooks(deps.storage, |h| {
diff.clone().into_cosmos_msg(h).map(SubMsg::new)
})?;
Ok(Response::new()
.add_submessages(messages)
.add_attributes(attributes))
}
// the logic from execute_update_members extracted for easier import
pub fn update_members(
deps: DepsMut,
height: u64,
sender: Addr,
to_add: Vec<Member>,
to_remove: Vec<String>,
) -> Result<MemberChangedHookMsg, ContractError> {
ADMIN.assert_admin(deps.as_ref(), &sender)?;
let mut total = TOTAL.load(deps.storage)?;
let mut diffs: Vec<MemberDiff> = vec![];
// add all new members and update total
for add in to_add.into_iter() {
let add_addr = deps.api.addr_validate(&add.addr)?;
MEMBERS.update(deps.storage, &add_addr, height, |old| -> StdResult<_> {
total -= old.unwrap_or_default();
total += add.weight;
diffs.push(MemberDiff::new(add.addr, old, Some(add.weight)));
Ok(add.weight)
})?;
}
for remove in to_remove.into_iter() {
let remove_addr = deps.api.addr_validate(&remove)?;
let old = MEMBERS.may_load(deps.storage, &remove_addr)?;
// Only process this if they were actually in the list before
if let Some(weight) = old {
diffs.push(MemberDiff::new(remove, Some(weight), None));
total -= weight;
MEMBERS.remove(deps.storage, &remove_addr, height)?;
}
}
TOTAL.save(deps.storage, &total)?;
Ok(MemberChangedHookMsg { diffs })
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Member {
addr,
at_height: height,
} => to_binary(&query_member(deps, addr, height)?),
QueryMsg::ListMembers { start_after, limit } => {
to_binary(&list_members(deps, start_after, limit)?)
}
QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?),
QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?),
QueryMsg::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?),
}
}
fn query_total_weight(deps: Deps) -> StdResult<TotalWeightResponse> {
let weight = TOTAL.load(deps.storage)?;
Ok(TotalWeightResponse { weight })
}
fn query_member(deps: Deps, addr: String, height: Option<u64>) -> StdResult<MemberResponse> {
let addr = deps.api.addr_validate(&addr)?;
let weight = match height {
Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h),
None => MEMBERS.may_load(deps.storage, &addr),
}?;
Ok(MemberResponse { weight })
}
// settings for pagination
const MAX_LIMIT: u32 = 30;
const DEFAULT_LIMIT: u32 = 10;
fn list_members(
deps: Deps,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<MemberListResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let addr = maybe_addr(deps.api, start_after)?;
let start = addr.as_ref().map(Bound::exclusive);
let members = MEMBERS
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|item| {
item.map(|(addr, weight)| Member {
addr: addr.into(),
weight,
})
})
.collect::<StdResult<_>>()?;
Ok(MemberListResponse { members })
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{from_slice, Api, OwnedDeps, Querier, Storage};
use cw4::{member_key, TOTAL_KEY};
use cw_controllers::{AdminError, HookError};
const INIT_ADMIN: &str = "juan";
const USER1: &str = "somebody";
const USER2: &str = "else";
const USER3: &str = "funny";
fn do_instantiate(deps: DepsMut) {
let msg = InstantiateMsg {
admin: Some(INIT_ADMIN.into()),
members: vec![
Member {
addr: USER1.into(),
weight: 11,
},
Member {
addr: USER2.into(),
weight: 6,
},
],
};
let info = mock_info("creator", &[]);
instantiate(deps, mock_env(), info, msg).unwrap();
}
#[test]
fn proper_instantiation() {
let mut deps = mock_dependencies();
do_instantiate(deps.as_mut());
// it worked, let's query the state
let res = ADMIN.query_admin(deps.as_ref()).unwrap();
assert_eq!(Some(INIT_ADMIN.into()), res.admin);
let res = query_total_weight(deps.as_ref()).unwrap();
assert_eq!(17, res.weight);
}
#[test]
fn try_member_queries() {
let mut deps = mock_dependencies();
do_instantiate(deps.as_mut());
let member1 = query_member(deps.as_ref(), USER1.into(), None).unwrap();
assert_eq!(member1.weight, Some(11));
let member2 = query_member(deps.as_ref(), USER2.into(), None).unwrap();
assert_eq!(member2.weight, Some(6));
let member3 = query_member(deps.as_ref(), USER3.into(), None).unwrap();
assert_eq!(member3.weight, None);
let members = list_members(deps.as_ref(), None, None).unwrap();
assert_eq!(members.members.len(), 2);
// TODO: assert the set is proper
}
fn assert_users<S: Storage, A: Api, Q: Querier>(
deps: &OwnedDeps<S, A, Q>,
user1_weight: Option<u64>,
user2_weight: Option<u64>,
user3_weight: Option<u64>,
height: Option<u64>,
) {
let member1 = query_member(deps.as_ref(), USER1.into(), height).unwrap();
assert_eq!(member1.weight, user1_weight);
let member2 = query_member(deps.as_ref(), USER2.into(), height).unwrap();
assert_eq!(member2.weight, user2_weight);
let member3 = query_member(deps.as_ref(), USER3.into(), height).unwrap();
assert_eq!(member3.weight, user3_weight);
// this is only valid if we are not doing a historical query
if height.is_none() {
// compute expected metrics
let weights = vec![user1_weight, user2_weight, user3_weight];
let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum();
let count = weights.iter().filter(|x| x.is_some()).count();
// TODO: more detailed compare?
let members = list_members(deps.as_ref(), None, None).unwrap();
assert_eq!(count, members.members.len());
let total = query_total_weight(deps.as_ref()).unwrap();
assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21
}
}
#[test]
fn add_new_remove_old_member() {
let mut deps = mock_dependencies();
do_instantiate(deps.as_mut());
// add a new one and remove existing one
let add = vec![Member {
addr: USER3.into(),
weight: 15,
}];
let remove = vec![USER1.into()];
// non-admin cannot update
let height = mock_env().block.height;
let err = update_members(
deps.as_mut(),
height + 5,
Addr::unchecked(USER1),
add.clone(),
remove.clone(),
)
.unwrap_err();
assert_eq!(err, AdminError::NotAdmin {}.into());
// Test the values from instantiate
assert_users(&deps, Some(11), Some(6), None, None);
// Note all values were set at height, the beginning of that block was all None
assert_users(&deps, None, None, None, Some(height));
// This will get us the values at the start of the block after instantiate (expected initial values)
assert_users(&deps, Some(11), Some(6), None, Some(height + 1));
// admin updates properly
update_members(
deps.as_mut(),
height + 10,
Addr::unchecked(INIT_ADMIN),
add,
remove,
)
.unwrap();
// updated properly
assert_users(&deps, None, Some(6), Some(15), None);
// snapshot still shows old value
assert_users(&deps, Some(11), Some(6), None, Some(height + 1));
}
#[test]
fn add_old_remove_new_member() {
// add will over-write and remove have no effect
let mut deps = mock_dependencies();
do_instantiate(deps.as_mut());
// add a new one and remove existing one
let add = vec![Member {
addr: USER1.into(),
weight: 4,
}];
let remove = vec![USER3.into()];
// admin updates properly
let height = mock_env().block.height;
update_members(
deps.as_mut(),
height,
Addr::unchecked(INIT_ADMIN),
add,
remove,
)
.unwrap();
assert_users(&deps, Some(4), Some(6), None, None);
}
#[test]
fn add_and_remove_same_member() {
// add will over-write and remove have no effect
let mut deps = mock_dependencies();
do_instantiate(deps.as_mut());
// USER1 is updated and remove in the same call, we should remove this an add member3
let add = vec![
Member {
addr: USER1.into(),
weight: 20,
},
Member {
addr: USER3.into(),
weight: 5,
},
];
let remove = vec![USER1.into()];
// admin updates properly
let height = mock_env().block.height;
update_members(
deps.as_mut(),
height,
Addr::unchecked(INIT_ADMIN),
add,
remove,
)
.unwrap();
assert_users(&deps, None, Some(6), Some(5), None);
}
#[test]
fn add_remove_hooks() {
// add will over-write and remove have no effect
let mut deps = mock_dependencies();
do_instantiate(deps.as_mut());
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert!(hooks.hooks.is_empty());
let contract1 = String::from("hook1");
let contract2 = String::from("hook2");
let add_msg = ExecuteMsg::AddHook {
addr: contract1.clone(),
};
// non-admin cannot add hook
let user_info = mock_info(USER1, &[]);
let err = execute(
deps.as_mut(),
mock_env(),
user_info.clone(),
add_msg.clone(),
)
.unwrap_err();
assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
// admin can add it, and it appears in the query
let admin_info = mock_info(INIT_ADMIN, &[]);
let _ = execute(
deps.as_mut(),
mock_env(),
admin_info.clone(),
add_msg.clone(),
)
.unwrap();
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert_eq!(hooks.hooks, vec![contract1.clone()]);
// cannot remove a non-registered contract
let remove_msg = ExecuteMsg::RemoveHook {
addr: contract2.clone(),
};
let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err();
assert_eq!(err, HookError::HookNotRegistered {}.into());
// add second contract
let add_msg2 = ExecuteMsg::AddHook {
addr: contract2.clone(),
};
let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap();
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]);
// cannot re-add an existing contract
let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err();
assert_eq!(err, HookError::HookAlreadyRegistered {}.into());
// non-admin cannot remove
let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 };
let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err();
assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into());
// remove the original
let _ = execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap();
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert_eq!(hooks.hooks, vec![contract2]);
}
#[test]
fn hooks_fire() {
let mut deps = mock_dependencies();
do_instantiate(deps.as_mut());
let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap();
assert!(hooks.hooks.is_empty());
let contract1 = String::from("hook1");
let contract2 = String::from("hook2");
// register 2 hooks
let admin_info = mock_info(INIT_ADMIN, &[]);
let add_msg = ExecuteMsg::AddHook {
addr: contract1.clone(),
};
let add_msg2 = ExecuteMsg::AddHook {
addr: contract2.clone(),
};
for msg in vec![add_msg, add_msg2] {
let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap();
}
// make some changes - add 3, remove 2, and update 1
// USER1 is updated and remove in the same call, we should remove this an add member3
let add = vec![
Member {
addr: USER1.into(),
weight: 20,
},
Member {
addr: USER3.into(),
weight: 5,
},
];
let remove = vec![USER2.into()];
let msg = ExecuteMsg::UpdateMembers { remove, add };
// admin updates properly
assert_users(&deps, Some(11), Some(6), None, None);
let res = execute(deps.as_mut(), mock_env(), admin_info, msg).unwrap();
assert_users(&deps, Some(20), None, Some(5), None);
// ensure 2 messages for the 2 hooks
assert_eq!(res.messages.len(), 2);
// same order as in the message (adds first, then remove)
let diffs = vec![
MemberDiff::new(USER1, Some(11), Some(20)),
MemberDiff::new(USER3, None, Some(5)),
MemberDiff::new(USER2, Some(6), None),
];
let hook_msg = MemberChangedHookMsg { diffs };
let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1).unwrap());
let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2).unwrap());
assert_eq!(res.messages, vec![msg1, msg2]);
}
#[test]
fn raw_queries_work() {
// add will over-write and remove have no effect
let mut deps = mock_dependencies();
do_instantiate(deps.as_mut());
// get total from raw key
let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap();
let total: u64 = from_slice(&total_raw).unwrap();
assert_eq!(17, total);
// get member votes from raw key
let member2_raw = deps.storage.get(&member_key(USER2)).unwrap();
let member2: u64 = from_slice(&member2_raw).unwrap();
assert_eq!(6, member2);
// and execute misses
let member3_raw = deps.storage.get(&member_key(USER3));
assert_eq!(None, member3_raw);
}
}
+19
View File
@@ -0,0 +1,19 @@
use cosmwasm_std::StdError;
use thiserror::Error;
use cw_controllers::{AdminError, HookError};
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("{0}")]
Hook(#[from] HookError),
#[error("{0}")]
Admin(#[from] AdminError),
#[error("Unauthorized")]
Unauthorized {},
}
@@ -0,0 +1,43 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdResult, WasmMsg};
use cw4::{Cw4Contract, Member};
use crate::msg::ExecuteMsg;
/// Cw4GroupContract is a wrapper around Cw4Contract that provides a lot of helpers
/// for working with cw4-group contracts.
///
/// It extends Cw4Contract to add the extra calls from cw4-group.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Cw4GroupContract(pub Cw4Contract);
impl Deref for Cw4GroupContract {
type Target = Cw4Contract;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Cw4GroupContract {
pub fn new(addr: Addr) -> Self {
Cw4GroupContract(Cw4Contract(addr))
}
fn encode_msg(&self, msg: ExecuteMsg) -> StdResult<CosmosMsg> {
Ok(WasmMsg::Execute {
contract_addr: self.addr().into(),
msg: to_binary(&msg)?,
funds: vec![],
}
.into())
}
pub fn update_members(&self, remove: Vec<String>, add: Vec<Member>) -> StdResult<CosmosMsg> {
let msg = ExecuteMsg::UpdateMembers { remove, add };
self.encode_msg(msg)
}
}
+7
View File
@@ -0,0 +1,7 @@
pub mod contract;
pub mod error;
pub mod helpers;
pub mod msg;
pub mod state;
pub use crate::error::ContractError;
+51
View File
@@ -0,0 +1,51 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cw4::Member;
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub struct InstantiateMsg {
/// The admin is the only account that can update the group state.
/// Omit it to make the group immutable.
pub admin: Option<String>,
pub members: Vec<Member>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
/// Change the admin
UpdateAdmin { admin: Option<String> },
/// apply a diff to the existing members.
/// remove is applied after add, so if an address is in both, it is removed
UpdateMembers {
remove: Vec<String>,
add: Vec<Member>,
},
/// Add a new hook to be informed of all membership changes. Must be called by Admin
AddHook { addr: String },
/// Remove a hook. Must be called by Admin
RemoveHook { addr: String },
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
/// Return AdminResponse
Admin {},
/// Return TotalWeightResponse
TotalWeight {},
/// Returns MembersListResponse
ListMembers {
start_after: Option<String>,
limit: Option<u32>,
},
/// Returns MemberResponse
Member {
addr: String,
at_height: Option<u64>,
},
/// Shows all registered hooks. Returns HooksResponse.
Hooks {},
}
+16
View File
@@ -0,0 +1,16 @@
use cosmwasm_std::Addr;
use cw4::TOTAL_KEY;
use cw_controllers::{Admin, Hooks};
use cw_storage_plus::{Item, SnapshotMap, Strategy};
pub const ADMIN: Admin = Admin::new("admin");
pub const HOOKS: Hooks = Hooks::new("cw4-hooks");
pub const TOTAL: Item<u64> = Item::new(TOTAL_KEY);
pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new(
cw4::MEMBERS_KEY,
cw4::MEMBERS_CHECKPOINTS,
cw4::MEMBERS_CHANGELOG,
Strategy::EveryBlock,
);
+6 -8
View File
@@ -43,6 +43,10 @@ pub struct Init {
#[clap(long)]
validator_apis: Option<String>,
/// Cosmos wallet mnemonic needed for double spending protection
#[clap(long)]
mnemonic: String,
/// Set this gateway to work in a testnet mode that would allow clients to bypass bandwidth credential requirement
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
@@ -57,11 +61,6 @@ pub struct Init {
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
validators: Option<String>,
/// Cosmos wallet mnemonic
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
mnemonic: String,
}
impl From<Init> for OverrideConfig {
@@ -74,6 +73,7 @@ impl From<Init> for OverrideConfig {
datastore: init_config.datastore,
announce_host: init_config.announce_host,
validator_apis: init_config.validator_apis,
mnemonic: Some(init_config.mnemonic),
#[cfg(all(feature = "eth", not(feature = "coconut")))]
testnet_mode: init_config.testnet_mode,
@@ -83,9 +83,6 @@ impl From<Init> for OverrideConfig {
#[cfg(all(feature = "eth", not(feature = "coconut")))]
validators: init_config.validators,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
mnemonic: Some(init_config.mnemonic),
}
}
}
@@ -166,6 +163,7 @@ mod tests {
announce_host: Some("foo-announce-host".to_string()),
datastore: Some("foo-datastore".to_string()),
validator_apis: None,
mnemonic: "a b c".to_string(),
};
let config = Config::new(&args.id);
+5 -11
View File
@@ -19,9 +19,6 @@ pub(crate) mod upgrade;
const DEFAULT_ETH_ENDPOINT: &str = "https://rinkeby.infura.io/v3/00000000000000000000000000000000";
#[cfg(all(not(feature = "eth"), not(feature = "coconut")))]
const DEFAULT_VALIDATOR_ENDPOINT: &str = "http://localhost:26657";
// A dummy mnemonic
#[cfg(all(not(feature = "eth"), not(feature = "coconut")))]
const DEFAULT_MNEMONIC: &str = "typical regret aware used tennis noise resource crisp defy join donate orient army item immense clean emerge globe gift chronic loan flat enter egg";
#[derive(Subcommand)]
pub(crate) enum Commands {
@@ -50,6 +47,7 @@ pub(crate) struct OverrideConfig {
datastore: Option<String>,
announce_host: Option<String>,
validator_apis: Option<String>,
mnemonic: Option<String>,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
testnet_mode: bool,
@@ -59,9 +57,6 @@ pub(crate) struct OverrideConfig {
#[cfg(all(feature = "eth", not(feature = "coconut")))]
validators: Option<String>,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
mnemonic: Option<String>,
}
pub(crate) async fn execute(args: Cli) {
@@ -121,10 +116,13 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
config = config.with_custom_persistent_store(datastore_path);
}
if let Some(cosmos_mnemonic) = args.mnemonic {
config = config.with_cosmos_mnemonic(cosmos_mnemonic);
}
#[cfg(all(not(feature = "eth"), not(feature = "coconut")))]
{
config = config.with_custom_validator_nymd(parse_validators(DEFAULT_VALIDATOR_ENDPOINT));
config = config.with_cosmos_mnemonic(String::from(DEFAULT_MNEMONIC));
config = config.with_eth_endpoint(String::from(DEFAULT_ETH_ENDPOINT));
}
@@ -142,10 +140,6 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
config = config.with_custom_validator_nymd(parse_validators(&raw_validators));
}
if let Some(cosmos_mnemonic) = args.mnemonic {
config = config.with_cosmos_mnemonic(String::from(cosmos_mnemonic));
}
if let Some(eth_endpoint) = args.eth_endpoint {
config = config.with_eth_endpoint(eth_endpoint);
}
+5 -8
View File
@@ -43,6 +43,10 @@ pub struct Run {
#[clap(long)]
validator_apis: Option<String>,
/// Cosmos wallet mnemonic
#[clap(long)]
mnemonic: Option<String>,
/// Set this gateway to work in a testnet mode that would allow clients to bypass bandwidth credential requirement
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
@@ -57,11 +61,6 @@ pub struct Run {
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
validators: Option<String>,
/// Cosmos wallet mnemonic
#[cfg(all(feature = "eth", not(feature = "coconut")))]
#[clap(long)]
mnemonic: Option<String>,
}
impl From<Run> for OverrideConfig {
@@ -74,6 +73,7 @@ impl From<Run> for OverrideConfig {
datastore: run_config.datastore,
announce_host: run_config.announce_host,
validator_apis: run_config.validator_apis,
mnemonic: run_config.mnemonic,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
testnet_mode: run_config.testnet_mode,
@@ -83,9 +83,6 @@ impl From<Run> for OverrideConfig {
#[cfg(all(feature = "eth", not(feature = "coconut")))]
validators: run_config.validators,
#[cfg(all(feature = "eth", not(feature = "coconut")))]
mnemonic: run_config.mnemonic,
}
}
}
+2 -6
View File
@@ -138,7 +138,6 @@ impl Config {
self
}
#[cfg(not(feature = "coconut"))]
pub fn with_cosmos_mnemonic(mut self, cosmos_mnemonic: String) -> Self {
self.gateway.cosmos_mnemonic = cosmos_mnemonic;
self
@@ -237,8 +236,7 @@ impl Config {
self.gateway.validator_nymd_urls.clone()
}
#[cfg(not(feature = "coconut"))]
pub fn get_cosmos_mnemonic(&self) -> String {
pub fn _get_cosmos_mnemonic(&self) -> String {
self.gateway.cosmos_mnemonic.clone()
}
@@ -348,8 +346,7 @@ pub struct Gateway {
#[cfg(not(feature = "coconut"))]
validator_nymd_urls: Vec<Url>,
/// Mnemonic of a cosmos wallet used for checking for double spending.
#[cfg(not(feature = "coconut"))]
/// Mnemonic of a cosmos wallet used in checking for double spending.
cosmos_mnemonic: String,
/// nym_home_directory specifies absolute path to the home nym gateways directory.
@@ -405,7 +402,6 @@ impl Default for Gateway {
validator_api_urls: default_api_endpoints(),
#[cfg(not(feature = "coconut"))]
validator_nymd_urls: default_nymd_endpoints(),
#[cfg(not(feature = "coconut"))]
cosmos_mnemonic: "".to_string(),
nym_root_directory: Config::default_root_directory(),
persistent_storage: Default::default(),
+2 -13
View File
@@ -11,9 +11,6 @@ use credentials::error::Error;
#[cfg(not(feature = "coconut"))]
use credentials::token::bandwidth::TokenCredential;
#[cfg(feature = "coconut")]
const BANDWIDTH_INDEX: usize = 0;
pub struct Bandwidth {
value: u64,
}
@@ -29,16 +26,8 @@ impl TryFrom<Credential> for Bandwidth {
type Error = Error;
fn try_from(credential: Credential) -> Result<Self, Self::Error> {
match credential.public_attributes().get(BANDWIDTH_INDEX) {
None => Err(Error::NotEnoughPublicAttributes),
Some(attr) => match <[u8; 8]>::try_from(attr.as_slice()) {
Ok(bandwidth_bytes) => {
let value = u64::from_be_bytes(bandwidth_bytes);
Ok(Self { value })
}
Err(_) => Err(Error::InvalidBandwidthSize),
},
}
let value = credential.voucher_value()?;
Ok(Self { value })
}
}
+1 -1
View File
@@ -276,7 +276,7 @@ where
let erc20_bridge = ERC20Bridge::new(
self.config.get_eth_endpoint(),
self.config.get_validator_nymd_endpoints(),
self.config.get_cosmos_mnemonic(),
self.config._get_cosmos_mnemonic(),
);
let mix_forwarding_channel = self.start_packet_forwarder();
+1
View File
@@ -57,6 +57,7 @@ validator-client = { path="../common/client-libs/validator-client", features = [
version-checker = { path="../common/version-checker" }
coconut-interface = { path = "../common/coconut-interface", optional = true }
credentials = { path = "../common/credentials", optional = true }
credential-storage = { path = "../common/credential-storage" }
# validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
console-subscriber = { version = "0.1.1", optional = true}
cfg-if = "1.0"
+2 -27
View File
@@ -1,35 +1,10 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::error::{CoconutError, Result};
use crate::config::DEFAULT_LOCAL_VALIDATOR;
use validator_client::nymd::{tx::Hash, NymdClient, QueryNymdClient, TxResponse};
use async_trait::async_trait;
use crate::coconut::error::Result;
use validator_client::nymd::TxResponse;
#[async_trait]
pub trait Client {
async fn get_tx(&self, tx_hash: &str) -> Result<TxResponse>;
}
pub struct QueryClient {
inner: NymdClient<QueryNymdClient>,
}
impl QueryClient {
pub fn new() -> Result<Self> {
let inner = NymdClient::connect(DEFAULT_LOCAL_VALIDATOR, None, None, None)?;
Ok(Self { inner })
}
}
#[async_trait]
impl Client for QueryClient {
async fn get_tx(&self, tx_hash: &str) -> Result<TxResponse> {
let tx_hash = tx_hash
.parse::<Hash>()
.map_err(|_| CoconutError::TxHashParseError)?;
Ok(self.inner.get_tx(tx_hash).await?)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
pub(crate) mod client;
mod deposit;
mod error;
pub(crate) mod error;
#[cfg(test)]
mod tests;
+48 -28
View File
@@ -27,6 +27,12 @@ const DEFAULT_GATEWAY_PING_INTERVAL: Duration = Duration::from_secs(60);
const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
const DEFAULT_GATEWAY_CONNECTION_TIMEOUT: Duration = Duration::from_millis(2_500);
#[cfg(not(feature = "coconut"))]
const DEFAULT_ETH_ENDPOINT: &str = "https://rinkeby.infura.io/v3/00000000000000000000000000000000";
#[cfg(not(feature = "coconut"))]
const DEFAULT_ETH_PRIVATE_KEY: &str =
"0000000000000000000000000000000000000000000000000000000000000001";
const DEFAULT_TEST_ROUTES: usize = 3;
const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1;
const DEFAULT_ROUTE_TEST_PACKETS: usize = 1000;
@@ -53,6 +59,10 @@ pub struct Config {
#[serde(default)]
rewarding: Rewarding,
#[serde(default)]
#[cfg(feature = "coconut")]
coconut_signer: CoconutSigner,
}
impl NymConfig for Config {
@@ -88,9 +98,8 @@ pub struct Base {
/// Address of the validator contract managing the network
mixnet_contract_address: String,
// Avoid breaking derives for now
#[cfg(feature = "coconut")]
keypair_bs58: String,
/// Mnemonic used for rewarding and/or multisig operations
mnemonic: String,
}
impl Default for Base {
@@ -100,8 +109,7 @@ impl Default for Base {
.parse()
.expect("default local validator is malformed!"),
mixnet_contract_address: DEFAULT_NETWORK.mixnet_contract_address().to_string(),
#[cfg(feature = "coconut")]
keypair_bs58: String::default(),
mnemonic: String::default(),
}
}
}
@@ -154,11 +162,8 @@ pub struct NetworkMonitor {
#[serde(with = "humantime_serde")]
packet_delivery_timeout: Duration,
/// Path to directory containing public/private keys used for bandwidth token purchase.
/// Those are saved in case of emergency, to be able to reclaim bandwidth tokens.
/// The public key is the name of the file, while the private key is the content.
#[cfg(not(feature = "coconut"))]
backup_bandwidth_token_keys_dir: PathBuf,
/// Path to the database containing bandwidth credentials of this client.
credentials_database_path: PathBuf,
/// Ethereum private key.
#[cfg(not(feature = "coconut"))]
@@ -184,9 +189,8 @@ pub struct NetworkMonitor {
}
impl NetworkMonitor {
#[cfg(not(feature = "coconut"))]
fn default_backup_bandwidth_token_keys_dir() -> PathBuf {
Config::default_data_directory(None).join("backup_bandwidth_token_keys_dir")
fn default_credentials_database_path() -> PathBuf {
Config::default_data_directory(None).join("credentials_database.db")
}
}
@@ -205,12 +209,11 @@ impl Default for NetworkMonitor {
gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT,
gateway_connection_timeout: DEFAULT_GATEWAY_CONNECTION_TIMEOUT,
packet_delivery_timeout: DEFAULT_PACKET_DELIVERY_TIMEOUT,
credentials_database_path: Self::default_credentials_database_path(),
#[cfg(not(feature = "coconut"))]
backup_bandwidth_token_keys_dir: Self::default_backup_bandwidth_token_keys_dir(),
eth_private_key: DEFAULT_ETH_PRIVATE_KEY.to_string(),
#[cfg(not(feature = "coconut"))]
eth_private_key: "".to_string(),
#[cfg(not(feature = "coconut"))]
eth_endpoint: "".to_string(),
eth_endpoint: DEFAULT_ETH_ENDPOINT.to_string(),
test_routes: DEFAULT_TEST_ROUTES,
minimum_test_routes: DEFAULT_MINIMUM_TEST_ROUTES,
route_test_packets: DEFAULT_ROUTE_TEST_PACKETS,
@@ -261,9 +264,6 @@ pub struct Rewarding {
/// Specifies whether rewarding service is enabled in this process.
enabled: bool,
/// Mnemonic (currently of the network monitor) used for rewarding
mnemonic: String,
/// 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
@@ -274,12 +274,22 @@ impl Default for Rewarding {
fn default() -> Self {
Rewarding {
enabled: false,
mnemonic: String::default(),
minimum_interval_monitor_threshold: DEFAULT_MONITOR_THRESHOLD,
}
}
}
#[derive(Debug, Default, 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,
}
impl Config {
pub fn new() -> Self {
Config::default()
@@ -287,7 +297,7 @@ impl Config {
#[cfg(feature = "coconut")]
pub fn keypair(&self) -> KeyPair {
KeyPair::try_from_bs58(self.base.keypair_bs58.clone()).unwrap()
KeyPair::try_from_bs58(self.coconut_signer.keypair_bs58.clone()).unwrap()
}
pub fn with_network_monitor_enabled(mut self, enabled: bool) -> Self {
@@ -305,6 +315,12 @@ impl Config {
self
}
#[cfg(feature = "coconut")]
pub fn with_coconut_signer_enabled(mut self, enabled: bool) -> Self {
self.coconut_signer.enabled = enabled;
self
}
pub fn with_custom_nymd_validator(mut self, validator: Url) -> Self {
self.base.local_validator = validator;
self
@@ -316,13 +332,13 @@ impl Config {
}
pub fn with_mnemonic<S: Into<String>>(mut self, mnemonic: S) -> Self {
self.rewarding.mnemonic = mnemonic.into();
self.base.mnemonic = mnemonic.into();
self
}
#[cfg(feature = "coconut")]
pub fn with_keypair<S: Into<String>>(mut self, keypair_bs58: S) -> Self {
self.base.keypair_bs58 = keypair_bs58.into();
self.coconut_signer.keypair_bs58 = keypair_bs58.into();
self
}
@@ -362,13 +378,17 @@ impl Config {
self.network_monitor.enabled
}
#[cfg(feature = "coconut")]
pub fn get_coconut_signer_enabled(&self) -> bool {
self.coconut_signer.enabled
}
pub fn get_testnet_mode(&self) -> bool {
self.network_monitor.testnet_mode
}
#[cfg(not(feature = "coconut"))]
pub fn get_backup_bandwidth_token_keys_dir(&self) -> PathBuf {
self.network_monitor.backup_bandwidth_token_keys_dir.clone()
pub fn get_credentials_database_path(&self) -> PathBuf {
self.network_monitor.credentials_database_path.clone()
}
#[cfg(not(feature = "coconut"))]
@@ -396,7 +416,7 @@ impl Config {
}
pub fn get_mnemonic(&self) -> String {
self.rewarding.mnemonic.clone()
self.base.mnemonic.clone()
}
pub fn get_network_monitor_run_interval(&self) -> Duration {
+1 -4
View File
@@ -58,10 +58,7 @@ gateway_connection_timeout = '{{ network_monitor.gateway_connection_timeout }}'
# packets before declaring nodes unreachable.
packet_delivery_timeout = '{{ network_monitor.packet_delivery_timeout }}'
# Path to directory containing public/private keys used for bandwidth token purchase.
# Those are saved in case of emergency, to be able to reclaim bandwidth tokens.
# The public key is the name of the file, while the private key is the content.
backup_bandwidth_token_keys_dir = '{{ network_monitor.backup_bandwidth_token_keys_dir }}'
credentials_database_path = '{{ network_monitor.credentials_database_path }}'
# Ethereum private key.
eth_private_key = '{{ network_monitor.eth_private_key }}'
+7 -3
View File
@@ -128,9 +128,13 @@ impl<C> ValidatorCacheRefresher<C> {
self.nymd_client.get_gateways(),
)?;
let rewarded_set_identities = self.nymd_client.get_rewarded_set_identities().await?;
let (rewarded_set, active_set) =
self.collect_rewarded_and_active_set_details(&mixnodes, rewarded_set_identities);
let (rewarded_set, active_set) = if let Ok(rewarded_set_identities) =
self.nymd_client.get_rewarded_set_identities().await
{
self.collect_rewarded_and_active_set_details(&mixnodes, rewarded_set_identities)
} else {
(Vec::new(), Vec::new())
};
let epoch_rewarding_params = self.nymd_client.get_current_epoch_reward_params().await?;
let current_epoch = self.nymd_client.get_current_epoch().await?;
+48 -50
View File
@@ -29,7 +29,8 @@ use url::Url;
use crate::rewarded_set_updater::RewardedSetUpdater;
#[cfg(feature = "coconut")]
use coconut::{client::QueryClient, InternalSignRequest};
use coconut::InternalSignRequest;
use validator_client::nymd::SigningNymdClient;
pub(crate) mod config;
pub(crate) mod contract_cache;
@@ -55,10 +56,7 @@ const TESTNET_MODE_ARG_NAME: &str = "testnet-mode";
const KEYPAIR_ARG: &str = "keypair";
#[cfg(feature = "coconut")]
const SIGNED_DEPOSITS_ARG: &str = "signed-deposits";
#[cfg(feature = "coconut")]
const COCONUT_ONLY_FLAG: &str = "coconut-only";
const COCONUT_ENABLED: &str = "enable-coconut";
#[cfg(not(feature = "coconut"))]
const ETH_ENDPOINT: &str = "eth_endpoint";
@@ -113,10 +111,6 @@ fn long_version() -> String {
}
fn parse_args<'a>() -> ArgMatches<'a> {
#[cfg(feature = "coconut")]
let monitor_reqs = &[];
#[cfg(not(feature = "coconut"))]
let monitor_reqs = &[ETH_ENDPOINT, ETH_PRIVATE_KEY];
let build_details = long_version();
let base_app = App::new("Nym Validator API")
.version(crate_version!())
@@ -127,14 +121,13 @@ fn parse_args<'a>() -> ArgMatches<'a> {
.help("specifies whether a network monitoring is enabled on this API")
.long(MONITORING_ENABLED)
.short("m")
.requires_all(monitor_reqs)
)
.arg(
Arg::with_name(REWARDING_ENABLED)
.help("specifies whether a network rewarding is enabled on this API")
.long(REWARDING_ENABLED)
.short("r")
.requires(MONITORING_ENABLED)
.requires_all(&[MONITORING_ENABLED, MNEMONIC_ARG])
)
.arg(
Arg::with_name(NYMD_VALIDATOR_ARG)
@@ -151,7 +144,6 @@ fn parse_args<'a>() -> ArgMatches<'a> {
.long(MNEMONIC_ARG)
.help("Mnemonic of the network monitor used for rewarding operators")
.takes_value(true)
.requires(REWARDING_ENABLED),
)
.arg(
Arg::with_name(WRITE_CONFIG_ARG)
@@ -170,7 +162,6 @@ fn parse_args<'a>() -> ArgMatches<'a> {
.help("Specifies the minimum percentage of monitor test run data present in order to distribute rewards for given interval.")
.takes_value(true)
.long(REWARDING_MONITOR_THRESHOLD_ARG)
.requires(REWARDING_ENABLED)
)
.arg(
Arg::with_name(TESTNET_MODE_ARG_NAME)
@@ -179,21 +170,19 @@ fn parse_args<'a>() -> ArgMatches<'a> {
);
#[cfg(feature = "coconut")]
let base_app = base_app.arg(
Arg::with_name(KEYPAIR_ARG)
.help("Path to the secret key file")
.takes_value(true)
.long(KEYPAIR_ARG),
).arg(
Arg::with_name(SIGNED_DEPOSITS_ARG)
.help("Path to the directory used to store the already signed deposit transactions. This prevents the validator for double signing for the same deposit")
.takes_value(true)
.long(SIGNED_DEPOSITS_ARG),
).arg(
Arg::with_name(COCONUT_ONLY_FLAG)
.help("Flag to indicate whether validator api should only be used for credential issuance with no blockchain connection")
.long(COCONUT_ONLY_FLAG),
);
let base_app = base_app
.arg(
Arg::with_name(KEYPAIR_ARG)
.help("Path to the secret key file")
.takes_value(true)
.long(KEYPAIR_ARG),
)
.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])
.long(COCONUT_ENABLED),
);
#[cfg(not(feature = "coconut"))]
let base_app = base_app.arg(
@@ -253,6 +242,11 @@ fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config {
config = config.with_rewarding_enabled(true)
}
#[cfg(feature = "coconut")]
if matches.is_present(COCONUT_ENABLED) {
config = config.with_coconut_signer_enabled(true)
}
if let Some(raw_validators) = matches.value_of(API_VALIDATORS_ARG) {
config = config.with_custom_validator_apis(parse_validators(raw_validators));
}
@@ -397,7 +391,11 @@ fn expected_monitor_test_runs(config: &Config, interval_length: Duration) -> usi
(interval_length.as_secs() / test_delay.as_secs()) as usize
}
async fn setup_rocket(config: &Config, liftoff_notify: Arc<Notify>) -> Result<Rocket<Ignite>> {
async fn setup_rocket(
config: &Config,
liftoff_notify: Arc<Notify>,
_nymd_client: Option<Client<SigningNymdClient>>,
) -> Result<Rocket<Ignite>> {
// let's build our rocket!
let rocket = rocket::build()
.attach(setup_cors()?)
@@ -413,11 +411,15 @@ async fn setup_rocket(config: &Config, liftoff_notify: Arc<Notify>) -> Result<Ro
};
#[cfg(feature = "coconut")]
let rocket = rocket.attach(InternalSignRequest::stage(
QueryClient::new()?,
config.keypair(),
storage.clone().unwrap(),
));
let rocket = if config.get_coconut_signer_enabled() {
rocket.attach(InternalSignRequest::stage(
_nymd_client.expect("Should have a signing client here"),
config.keypair(),
storage.clone().unwrap(),
))
} else {
rocket
};
// see if we should start up network monitor and if so, attach the node status api
if config.get_network_monitor_enabled() {
@@ -459,25 +461,21 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
return Ok(());
}
#[cfg(feature = "coconut")]
if matches.is_present(COCONUT_ONLY_FLAG) {
// this simplifies everything - we just want to run coconut things
return rocket::build()
.attach(setup_cors()?)
.attach(InternalSignRequest::stage(
QueryClient::new()?,
config.keypair(),
ValidatorApiStorage::init(config.get_node_status_api_database_path()).await?,
))
.launch()
.await
.map_err(|err| err.into());
}
let signing_nymd_client = if matches.is_present(MNEMONIC_ARG) {
Some(Client::new_signing(&config))
} else {
None
};
let liftoff_notify = Arc::new(Notify::new());
// let's build our rocket!
let rocket = setup_rocket(&config, Arc::clone(&liftoff_notify)).await?;
let rocket = setup_rocket(
&config,
Arc::clone(&liftoff_notify),
signing_nymd_client.clone(),
)
.await?;
let monitor_builder = setup_network_monitor(&config, system_version, &rocket);
let validator_cache = rocket.state::<ValidatorCache>().unwrap().clone();
@@ -485,7 +483,7 @@ 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 = Client::new_signing(&config);
let nymd_client = signing_nymd_client.expect("We should have a signing client here");
let validator_cache_refresher = ValidatorCacheRefresher::new(
nymd_client.clone(),
config.get_caching_interval(),
+6 -3
View File
@@ -1,6 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use credential_storage::PersistentStorage;
use crypto::asymmetric::{encryption, identity};
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
@@ -73,14 +74,16 @@ impl<'a> NetworkMonitorBuilder<'a> {
#[cfg(feature = "coconut")]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_credentials_database_path())
.await,
self.config.get_all_validator_api_endpoints(),
*identity_keypair.public_key(),
);
#[cfg(not(feature = "coconut"))]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_credentials_database_path())
.await,
self.config.get_network_monitor_eth_endpoint(),
self.config.get_network_monitor_eth_private_key(),
self.config.get_backup_bandwidth_token_keys_dir(),
)
.expect("Could not create bandwidth controller");
@@ -157,7 +160,7 @@ fn new_packet_sender(
gateways_status_updater: GatewayClientUpdateSender,
local_identity: Arc<identity::KeyPair>,
max_sending_rate: usize,
bandwidth_controller: BandwidthController,
bandwidth_controller: BandwidthController<PersistentStorage>,
testnet_mode: bool,
) -> PacketSender {
PacketSender::new(
@@ -7,6 +7,7 @@ use crate::network_monitor::monitor::gateway_clients_cache::{
use crate::network_monitor::monitor::gateways_pinger::GatewayPinger;
use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender};
use config::defaults::REMAINING_BANDWIDTH_THRESHOLD;
use credential_storage::PersistentStorage;
use crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH};
use futures::channel::mpsc;
use futures::stream::{self, FuturesUnordered, StreamExt};
@@ -97,7 +98,7 @@ struct FreshGatewayClientData {
// for coconut bandwidth credentials we currently have no double spending protection, just to
// get things running we're re-using the same credential for all gateways all the time.
// THIS IS VERY BAD!!
bandwidth_controller: BandwidthController,
bandwidth_controller: BandwidthController<PersistentStorage>,
testnet_mode: bool,
}
@@ -157,7 +158,7 @@ impl PacketSender {
gateway_connection_timeout: Duration,
max_concurrent_clients: usize,
max_sending_rate: usize,
bandwidth_controller: BandwidthController,
bandwidth_controller: BandwidthController<PersistentStorage>,
testnet_mode: bool,
) -> Self {
PacketSender {
+20 -1
View File
@@ -3,6 +3,8 @@
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::{
@@ -21,7 +23,7 @@ use validator_client::nymd::{
};
use validator_client::ValidatorClientError;
pub(crate) struct Client<C>(Arc<RwLock<validator_client::Client<C>>>);
pub(crate) struct Client<C>(pub(crate) Arc<RwLock<validator_client::Client<C>>>);
impl<C> Clone for Client<C> {
fn clone(&self) -> Self {
@@ -401,3 +403,20 @@ impl<C> Client<C> {
}
}
}
#[async_trait]
#[cfg(feature = "coconut")]
impl<C> crate::coconut::client::Client for Client<C>
where
C: CosmWasmClient + Sync + Send,
{
async fn get_tx(
&self,
tx_hash: &str,
) -> 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)?;
Ok(self.0.read().await.nymd.get_tx(tx_hash).await?)
}
}