Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bde44164e7 | |||
| 18add0a0f1 | |||
| 1846ed3476 | |||
| 39c7b131b6 | |||
| 4071a20bb5 | |||
| b4519cd77e | |||
| 35748e07c4 | |||
| c8f2548090 | |||
| 62f20a905e | |||
| a3dd94507a | |||
| dd36b329d5 | |||
| 581af4a295 | |||
| 085761a9fb | |||
| 58537224df | |||
| 54e217ccea | |||
| 78a5dbbf05 | |||
| c8f81d118b | |||
| a193e948e6 | |||
| ab7f24dc1f | |||
| cc77f6e392 | |||
| 58f02109be | |||
| 724e0f78c6 | |||
| d764b11122 | |||
| 4953b40a42 | |||
| 66e5afe485 | |||
| 9b17a1ca77 | |||
| 5ce6147839 | |||
| dd3643a1bb | |||
| 52fedd9866 | |||
| 289605f36b | |||
| 4f6bf3423b | |||
| 04a32156d4 | |||
| 427880d23f | |||
| a5e47dead8 | |||
| 297b7b2355 | |||
| d7f3abfe7d | |||
| 234952a6bb | |||
| 930561faf5 | |||
| 17507bc8c5 | |||
| 12f8e453ea | |||
| 97ef024b8a | |||
| e5051f98c5 | |||
| 2f69958e21 | |||
| d427591a66 | |||
| 0257c42ee9 | |||
| 8ebfc72da8 | |||
| b1178b7ad5 |
+2
-2
@@ -24,6 +24,7 @@ v6-topology.json
|
||||
/explorer/downloads/topology.json
|
||||
/explorer/public/downloads/mixmining.json
|
||||
/explorer/public/downloads/topology.json
|
||||
/nym-wallet/dist/*
|
||||
/clients/validator/examples/nym-driver-example/current-contract.txt
|
||||
validator-api/v4.json
|
||||
validator-api/v6.json
|
||||
@@ -33,5 +34,4 @@ contracts/mixnet/code_id
|
||||
contracts/mixnet/Justfile
|
||||
contracts/mixnet/Makefile
|
||||
validator-config
|
||||
*.patch
|
||||
validator-api-config.toml
|
||||
*.patch
|
||||
Generated
+164
-20
@@ -420,7 +420,10 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -504,6 +507,15 @@ dependencies = [
|
||||
"system-deps 3.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a"
|
||||
dependencies = [
|
||||
"rustc_version 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.70"
|
||||
@@ -667,30 +679,11 @@ dependencies = [
|
||||
name = "coconut-interface"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"coconut-rs",
|
||||
"getset",
|
||||
"nymcoconut",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coconut-rs"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/nymtech/coconut.git?branch=0.5.0#a1b72d51aa2a67b73b9f58d707030ae6dc70af7f"
|
||||
dependencies = [
|
||||
"bls12_381",
|
||||
"bs58",
|
||||
"digest 0.9.0",
|
||||
"ff",
|
||||
"getrandom 0.2.3",
|
||||
"group",
|
||||
"itertools",
|
||||
"rand 0.8.4",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "2.0.0"
|
||||
@@ -986,6 +979,42 @@ dependencies = [
|
||||
"validator-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"cast",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"csv",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_cbor",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d00996de9f2f7559f7f4dc286073197f83e92256a59ed395f9aac01fe717da57"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.1"
|
||||
@@ -1125,6 +1154,28 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ct-logs"
|
||||
version = "0.8.0"
|
||||
@@ -1740,6 +1791,7 @@ dependencies = [
|
||||
"az",
|
||||
"bytemuck",
|
||||
"half",
|
||||
"serde",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
@@ -3464,6 +3516,7 @@ dependencies = [
|
||||
"gateway-client",
|
||||
"gateway-requests",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"pemstore",
|
||||
"pretty_env_logger",
|
||||
@@ -3613,6 +3666,7 @@ dependencies = [
|
||||
"gateway-client",
|
||||
"gateway-requests",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"ordered-buffer",
|
||||
"pemstore",
|
||||
@@ -3670,6 +3724,27 @@ dependencies = [
|
||||
"version-checker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nymcoconut"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bls12_381",
|
||||
"bs58",
|
||||
"criterion",
|
||||
"digest 0.9.0",
|
||||
"doc-comment",
|
||||
"ff",
|
||||
"getrandom 0.2.3",
|
||||
"group",
|
||||
"itertools",
|
||||
"rand 0.8.4",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nymsphinx"
|
||||
version = "0.1.0"
|
||||
@@ -3834,6 +3909,12 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.3"
|
||||
@@ -4264,6 +4345,34 @@ version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pmutil"
|
||||
version = "0.5.3"
|
||||
@@ -4810,6 +4919,12 @@ dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.25"
|
||||
@@ -5091,6 +5206,15 @@ dependencies = [
|
||||
"semver 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.19.1"
|
||||
@@ -5330,6 +5454,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_cbor"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
||||
dependencies = [
|
||||
"half",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.130"
|
||||
@@ -6608,6 +6742,16 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.12.0"
|
||||
|
||||
@@ -30,6 +30,7 @@ members = [
|
||||
"common/mixnode-common",
|
||||
"common/network-defaults",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
"common/nymcoconut",
|
||||
"common/nymsphinx",
|
||||
"common/nymsphinx/acknowledgements",
|
||||
"common/nymsphinx/addressing",
|
||||
|
||||
@@ -24,7 +24,7 @@ dirs = "3.0" # for determining default store directories in config
|
||||
dotenv = "0.15.0" # for obtaining environmental variables (only used for RUST_LOG for time being)
|
||||
log = "0.4" # self explanatory
|
||||
pretty_env_logger = "0.4" # for formatting log messages
|
||||
rand = {version = "0.7.3", features = ["wasm-bindgen"]} # rng-related traits + some rng implementation to use
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits + some rng implementation to use
|
||||
serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization
|
||||
sled = "0.34" # for storage of replySURB decryption keys
|
||||
tokio = { version = "1.4", features = ["rt-multi-thread", "net", "signal"] } # async runtime
|
||||
@@ -44,6 +44,7 @@ topology = { path = "../../common/topology" }
|
||||
websocket-requests = { path = "websocket-requests" }
|
||||
validator-client = { path = "../../common/client-libs/validator-client" }
|
||||
version-checker = { path = "../../common/version-checker" }
|
||||
network-defaults = { path = "../../common/network-defaults" }
|
||||
|
||||
[features]
|
||||
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut"]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::config::{Config, SocketType};
|
||||
use crate::websocket;
|
||||
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
|
||||
use client_core::client::inbound_messages::{
|
||||
InputMessage, InputMessageReceiver, InputMessageSender,
|
||||
@@ -24,6 +22,7 @@ use client_core::client::topology_control::{
|
||||
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
|
||||
use crypto::asymmetric::identity;
|
||||
use futures::channel::mpsc;
|
||||
use gateway_client::bandwidth::BandwidthController;
|
||||
use gateway_client::{
|
||||
AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver,
|
||||
MixnetMessageSender,
|
||||
@@ -35,7 +34,8 @@ use nymsphinx::anonymous_replies::ReplySurb;
|
||||
use nymsphinx::receiver::ReconstructedMessage;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use gateway_client::bandwidth::BandwidthController;
|
||||
use crate::client::config::{Config, SocketType};
|
||||
use crate::websocket;
|
||||
|
||||
pub(crate) mod config;
|
||||
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::commands::override_config;
|
||||
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::{hash_to_scalar, Credential, Parameters};
|
||||
use config::NymConfig;
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::coconut::bandwidth::{
|
||||
obtain_signature, prepare_for_spending, BandwidthVoucherAttributes, TOTAL_ATTRIBUTES,
|
||||
};
|
||||
#[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::rngs::OsRng;
|
||||
@@ -21,6 +29,9 @@ use std::time::Duration;
|
||||
use topology::{filter::VersionFilterable, gateway};
|
||||
use url::Url;
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::commands::override_config;
|
||||
|
||||
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
let app = App::new("init")
|
||||
.about("Initialise a Nym client. Do this first!")
|
||||
@@ -36,9 +47,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
.takes_value(true)
|
||||
)
|
||||
.arg(Arg::with_name("validators")
|
||||
.long("validators")
|
||||
.help("Comma separated list of rest endpoints of the validators")
|
||||
.takes_value(true),
|
||||
.long("validators")
|
||||
.help("Comma separated list of rest endpoints of the validators")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(Arg::with_name("disable-socket")
|
||||
.long("disable-socket")
|
||||
@@ -71,6 +82,36 @@ 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 bandwidth_credential_attributes = BandwidthVoucherAttributes {
|
||||
serial_number: params.random_scalar(),
|
||||
binding_number: params.random_scalar(),
|
||||
voucher_value: hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
|
||||
voucher_info: hash_to_scalar(String::from("BandwidthVoucher").as_bytes()),
|
||||
};
|
||||
|
||||
let bandwidth_credential =
|
||||
obtain_signature(¶ms, &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>,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) mod handler;
|
||||
pub(crate) mod listener;
|
||||
|
||||
pub(crate) use handler::Handler;
|
||||
pub(crate) use listener::Listener;
|
||||
|
||||
pub(crate) mod handler;
|
||||
pub(crate) mod listener;
|
||||
|
||||
@@ -32,13 +32,14 @@ crypto = { path = "../../common/crypto" }
|
||||
gateway-client = { path = "../../common/client-libs/gateway-client" }
|
||||
gateway-requests = { path = "../../gateway/gateway-requests" }
|
||||
nymsphinx = { path = "../../common/nymsphinx" }
|
||||
ordered-buffer = {path = "../../common/socks5/ordered-buffer"}
|
||||
ordered-buffer = { path = "../../common/socks5/ordered-buffer" }
|
||||
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" }
|
||||
version-checker = { path = "../../common/version-checker" }
|
||||
network-defaults = { path = "../../common/network-defaults" }
|
||||
|
||||
[features]
|
||||
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut"]
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::socks::{
|
||||
authentication::{AuthenticationMethods, Authenticator, User},
|
||||
server::SphinxSocksServer,
|
||||
};
|
||||
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
|
||||
use client_core::client::inbound_messages::{
|
||||
InputMessage, InputMessageReceiver, InputMessageSender,
|
||||
@@ -25,6 +20,7 @@ use client_core::client::topology_control::{
|
||||
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
|
||||
use crypto::asymmetric::identity;
|
||||
use futures::channel::mpsc;
|
||||
use gateway_client::bandwidth::BandwidthController;
|
||||
use gateway_client::{
|
||||
AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver,
|
||||
MixnetMessageSender,
|
||||
@@ -34,7 +30,11 @@ use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use gateway_client::bandwidth::BandwidthController;
|
||||
use crate::client::config::Config;
|
||||
use crate::socks::{
|
||||
authentication::{AuthenticationMethods, Authenticator, User},
|
||||
server::SphinxSocksServer,
|
||||
};
|
||||
|
||||
pub(crate) mod config;
|
||||
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::commands::override_config;
|
||||
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::{hash_to_scalar, Credential, Parameters};
|
||||
use config::NymConfig;
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::coconut::bandwidth::{
|
||||
obtain_signature, prepare_for_spending, BandwidthVoucherAttributes, TOTAL_ATTRIBUTES,
|
||||
};
|
||||
#[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};
|
||||
@@ -19,6 +27,9 @@ use std::time::Duration;
|
||||
use topology::{filter::VersionFilterable, gateway};
|
||||
use url::Url;
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::commands::override_config;
|
||||
|
||||
pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
let app = App::new("init")
|
||||
.about("Initialise a Nym client. Do this first!")
|
||||
@@ -40,9 +51,9 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
|
||||
.takes_value(true)
|
||||
)
|
||||
.arg(Arg::with_name("validators")
|
||||
.long("validators")
|
||||
.help("Comma separated list of rest endpoints of the validators")
|
||||
.takes_value(true),
|
||||
.long("validators")
|
||||
.help("Comma separated list of rest endpoints of the validators")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(Arg::with_name("port")
|
||||
.short("p")
|
||||
@@ -71,6 +82,36 @@ 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 bandwidth_credential_attributes = BandwidthVoucherAttributes {
|
||||
serial_number: params.random_scalar(),
|
||||
binding_number: params.random_scalar(),
|
||||
voucher_value: hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
|
||||
voucher_info: hash_to_scalar("BandwidthVoucher"),
|
||||
};
|
||||
|
||||
let bandwidth_credential =
|
||||
obtain_signature(¶ms, &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,21 +3,24 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
use coconut_interface::{
|
||||
self, hash_to_scalar, Attribute, Credential, Parameters, Signature, Theta, VerificationKey,
|
||||
};
|
||||
use credentials::{obtain_aggregate_signature, obtain_aggregate_verification_key};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
struct State {
|
||||
signatures: Vec<Signature>,
|
||||
n_attributes: u32,
|
||||
params: Parameters,
|
||||
public_attributes_bytes: Vec<Vec<u8>>,
|
||||
public_attributes: Vec<Attribute>,
|
||||
private_attributes: Vec<Attribute>,
|
||||
serial_number: Attribute,
|
||||
binding_number: Attribute,
|
||||
voucher_value: Attribute,
|
||||
voucher_info: Attribute,
|
||||
aggregated_verification_key: Option<VerificationKey>,
|
||||
}
|
||||
|
||||
@@ -37,9 +40,10 @@ impl State {
|
||||
signatures: Vec::new(),
|
||||
n_attributes,
|
||||
params,
|
||||
public_attributes_bytes,
|
||||
public_attributes,
|
||||
private_attributes,
|
||||
serial_number: private_attributes[0],
|
||||
binding_number: private_attributes[1],
|
||||
voucher_value: public_attributes[0],
|
||||
voucher_info: public_attributes[1],
|
||||
aggregated_verification_key: None,
|
||||
}
|
||||
}
|
||||
@@ -63,8 +67,8 @@ async fn randomise_credential(
|
||||
) -> Result<Vec<Signature>, String> {
|
||||
let mut state = state.write().await;
|
||||
let signature = state.signatures.remove(idx);
|
||||
let new = signature.randomise(&state.params);
|
||||
state.signatures.insert(idx, new);
|
||||
let (new_signature, _) = signature.randomise(&state.params);
|
||||
state.signatures.insert(idx, new_signature);
|
||||
Ok(state.signatures.clone())
|
||||
}
|
||||
|
||||
@@ -117,14 +121,15 @@ async fn prove_credential(
|
||||
let state = state.read().await;
|
||||
|
||||
if let Some(signature) = state.signatures.get(idx) {
|
||||
match coconut_interface::prove_credential(
|
||||
match coconut_interface::prove_bandwidth_credential(
|
||||
&state.params,
|
||||
&verification_key,
|
||||
signature,
|
||||
&state.private_attributes,
|
||||
state.serial_number,
|
||||
state.binding_number,
|
||||
) {
|
||||
Ok(theta) => Ok(theta),
|
||||
Err(e) => Err(format!("{}", e)),
|
||||
Err(e) => Err(format!("{:?}", e)),
|
||||
}
|
||||
} else {
|
||||
Err("Got invalid Signature idx".to_string())
|
||||
@@ -144,10 +149,15 @@ async fn verify_credential(
|
||||
|
||||
let state = state.read().await;
|
||||
|
||||
let public_attributes_bytes = vec![
|
||||
state.voucher_value.to_bytes().to_vec(),
|
||||
state.voucher_info.to_bytes().to_vec(),
|
||||
];
|
||||
|
||||
let credential = Credential::new(
|
||||
state.n_attributes,
|
||||
theta,
|
||||
state.public_attributes_bytes.clone(),
|
||||
public_attributes_bytes,
|
||||
state
|
||||
.signatures
|
||||
.get(idx)
|
||||
@@ -164,11 +174,13 @@ async fn get_credential(
|
||||
) -> Result<Vec<Signature>, String> {
|
||||
let guard = state.read().await;
|
||||
let parsed_urls = parse_url_validators(&validator_urls)?;
|
||||
let public_attributes = vec![guard.voucher_value, guard.voucher_info];
|
||||
let private_attributes = vec![guard.serial_number, guard.binding_number];
|
||||
|
||||
let signature = obtain_aggregate_signature(
|
||||
&guard.params,
|
||||
&guard.public_attributes,
|
||||
&guard.private_attributes,
|
||||
&public_attributes,
|
||||
&private_attributes,
|
||||
&parsed_urls,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import NetClient, {INetClient} from "./net-client";
|
||||
import NetClient, { INetClient } from "./net-client";
|
||||
import {
|
||||
StateParams,
|
||||
ContractSettingsParams,
|
||||
Delegation,
|
||||
PagedMixDelegationsResponse,
|
||||
PagedGatewayDelegationsResponse,
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
Gateway,
|
||||
SendRequest
|
||||
} from "./types";
|
||||
import {Bip39, Random} from "@cosmjs/crypto";
|
||||
import {DirectSecp256k1HdWallet, EncodeObject} from "@cosmjs/proto-signing";
|
||||
import { Bip39, Random } from "@cosmjs/crypto";
|
||||
import { DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing";
|
||||
import MixnodesCache from "./caches/mixnodes";
|
||||
import {buildFeeTable, coin, Coin, coins, StdFee} from "@cosmjs/stargate";
|
||||
import { buildFeeTable, coin, Coin, coins, StdFee } from "@cosmjs/stargate";
|
||||
import {
|
||||
ExecuteResult,
|
||||
InstantiateOptions,
|
||||
@@ -32,17 +32,17 @@ import {
|
||||
nativeToPrintable
|
||||
} from "./currency";
|
||||
import GatewaysCache from "./caches/gateways";
|
||||
import QueryClient, {IQueryClient} from "./query-client";
|
||||
import {nymGasLimits, nymGasPrice} from "./stargate-helper";
|
||||
import {BroadcastTxSuccess, isBroadcastTxFailure} from "@cosmjs/stargate";
|
||||
import {makeBankMsgSend} from "./utils";
|
||||
import QueryClient, { IQueryClient } from "./query-client";
|
||||
import { nymGasLimits, nymGasPrice } from "./stargate-helper";
|
||||
import { BroadcastTxSuccess, isBroadcastTxFailure } from "@cosmjs/stargate";
|
||||
import { makeBankMsgSend } from "./utils";
|
||||
|
||||
export const VALIDATOR_API_PORT = "8080";
|
||||
export const VALIDATOR_API_GATEWAYS = "v1/gateways";
|
||||
export const VALIDATOR_API_MIXNODES = "v1/mixnodes";
|
||||
|
||||
export {coins, coin};
|
||||
export {Coin};
|
||||
export { coins, coin };
|
||||
export { Coin };
|
||||
export {
|
||||
displayAmountToNative,
|
||||
nativeCoinToDisplay,
|
||||
@@ -52,7 +52,7 @@ export {
|
||||
MappedCoin,
|
||||
CoinMap
|
||||
}
|
||||
export {nymGasLimits, nymGasPrice}
|
||||
export { nymGasLimits, nymGasPrice }
|
||||
|
||||
export default class ValidatorClient {
|
||||
private readonly client: INetClient | IQueryClient
|
||||
@@ -226,12 +226,12 @@ export default class ValidatorClient {
|
||||
*/
|
||||
static async mnemonicToAddress(mnemonic: string, prefix: string): Promise<string> {
|
||||
const wallet = await ValidatorClient.buildWallet(mnemonic, prefix);
|
||||
const [{address}] = await wallet.getAccounts()
|
||||
const [{ address }] = await wallet.getAccounts()
|
||||
return address
|
||||
}
|
||||
|
||||
static async buildWallet(mnemonic: string, prefix: string): Promise<DirectSecp256k1HdWallet> {
|
||||
const signerOptions = {prefix: prefix};
|
||||
const signerOptions = { prefix: prefix };
|
||||
return DirectSecp256k1HdWallet.fromMnemonic(mnemonic, signerOptions);
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ export default class ValidatorClient {
|
||||
return this.client.getBalance(address, this.denom).catch((err) => this.handleRequestFailure(err));
|
||||
}
|
||||
|
||||
async getStateParams(): Promise<StateParams> {
|
||||
async getStateParams(): Promise<ContractSettingsParams> {
|
||||
return this.client.getStateParams(this.contractAddress).catch((err) => this.handleRequestFailure(err))
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ export default class ValidatorClient {
|
||||
*/
|
||||
async bondMixnode(mixNode: MixNode, bond: Coin): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {bond_mixnode: {mix_node: mixNode}}, "adding mixnode", [bond]).catch((err) => this.handleRequestFailure(err));
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { bond_mixnode: { mix_node: mixNode } }, "adding mixnode", [bond]).catch((err) => this.handleRequestFailure(err));
|
||||
console.log(`account ${this.client.clientAddress} added mixnode with ${mixNode.host}`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -307,7 +307,7 @@ export default class ValidatorClient {
|
||||
*/
|
||||
async unbondMixnode(): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {unbond_mixnode: {}}).catch((err) => this.handleRequestFailure(err))
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { unbond_mixnode: {} }).catch((err) => this.handleRequestFailure(err))
|
||||
console.log(`account ${this.client.clientAddress} unbonded mixnode`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -324,7 +324,7 @@ export default class ValidatorClient {
|
||||
// requires coin type to ensure correct denomination (
|
||||
async delegateToMixnode(mixIdentity: string, amount: Coin): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {delegate_to_mixnode: {mix_identity: mixIdentity}}, `delegating to ${mixIdentity}`, [amount]).catch((err) => this.handleRequestFailure(err))
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { delegate_to_mixnode: { mix_identity: mixIdentity } }, `delegating to ${mixIdentity}`, [amount]).catch((err) => this.handleRequestFailure(err))
|
||||
console.log(`account ${this.client.clientAddress} delegated ${amount} to mixnode ${mixIdentity}`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -339,7 +339,7 @@ export default class ValidatorClient {
|
||||
*/
|
||||
async removeMixnodeDelegation(mixIdentity: string): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {undelegate_from_mixnode: {mix_identity: mixIdentity}}).catch((err) => this.handleRequestFailure(err))
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { undelegate_from_mixnode: { mix_identity: mixIdentity } }).catch((err) => this.handleRequestFailure(err))
|
||||
console.log(`account ${this.client.clientAddress} removed delegation from mixnode ${mixIdentity}`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -356,7 +356,7 @@ export default class ValidatorClient {
|
||||
// requires coin type to ensure correct denomination (
|
||||
async delegateToGateway(gatewayIdentity: string, amount: Coin): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {delegate_to_gateway: {gateway_identity: gatewayIdentity}}, `delegating to ${gatewayIdentity}`, [amount]).catch((err) => this.handleRequestFailure(err))
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { delegate_to_gateway: { gateway_identity: gatewayIdentity } }, `delegating to ${gatewayIdentity}`, [amount]).catch((err) => this.handleRequestFailure(err))
|
||||
console.log(`account ${this.client.clientAddress} delegated ${amount} to gateway ${gatewayIdentity}`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -371,7 +371,7 @@ export default class ValidatorClient {
|
||||
*/
|
||||
async removeGatewayDelegation(gatewayIdentity: string): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {undelegate_from_gateway: {gateway_identity: gatewayIdentity}}).catch((err) => this.handleRequestFailure(err))
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { undelegate_from_gateway: { gateway_identity: gatewayIdentity } }).catch((err) => this.handleRequestFailure(err))
|
||||
console.log(`account ${this.client.clientAddress} removed delegation from gateway ${gatewayIdentity}`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -450,7 +450,7 @@ export default class ValidatorClient {
|
||||
*/
|
||||
async bondGateway(gateway: Gateway, bond: Coin): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {bond_gateway: {gateway: gateway}}, "adding gateway", [bond]).catch((err) => this.handleRequestFailure(err));
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { bond_gateway: { gateway: gateway } }, "adding gateway", [bond]).catch((err) => this.handleRequestFailure(err));
|
||||
console.log(`account ${this.client.clientAddress} added gateway with ${gateway.host}`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -463,7 +463,7 @@ export default class ValidatorClient {
|
||||
*/
|
||||
async unbondGateway(): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {unbond_gateway: {}}).catch((err) => this.handleRequestFailure(err))
|
||||
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { unbond_gateway: {} }).catch((err) => this.handleRequestFailure(err))
|
||||
console.log(`account ${this.client.clientAddress} unbonded gateway`);
|
||||
return result;
|
||||
} else {
|
||||
@@ -471,9 +471,9 @@ export default class ValidatorClient {
|
||||
}
|
||||
}
|
||||
|
||||
async updateStateParams(newParams: StateParams): Promise<ExecuteResult> {
|
||||
async updateStateParams(newParams: ContractSettingsParams): Promise<ExecuteResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
return await this.client.executeContract(this.client.clientAddress, this.contractAddress, {update_state_params: newParams}, "updating contract state").catch((err) => this.handleRequestFailure(err));
|
||||
return await this.client.executeContract(this.client.clientAddress, this.contractAddress, { update_contract_settings: newParams }, "updating contract settings").catch((err) => this.handleRequestFailure(err));
|
||||
} else {
|
||||
throw new Error("Tried to update state params with a query client")
|
||||
}
|
||||
@@ -580,7 +580,7 @@ export default class ValidatorClient {
|
||||
|
||||
// the function to calculate fee for a single entry is not exposed...
|
||||
console.log(`this.denom is ${this.denom}`);
|
||||
const table = buildFeeTable(nymGasPrice(this.prefix), {sendMultiple: nymGasLimits.send * data.length}, {sendMultiple: nymGasLimits.send * data.length})
|
||||
const table = buildFeeTable(nymGasPrice(this.prefix), { sendMultiple: nymGasLimits.send * data.length }, { sendMultiple: nymGasLimits.send * data.length })
|
||||
const fee = table.sendMultiple
|
||||
const result = await this.client.signAndBroadcast(senderAddress, encoded, fee, memo)
|
||||
if (isBroadcastTxFailure(result)) {
|
||||
|
||||
@@ -80,7 +80,7 @@ export default class NetClient implements INetClient {
|
||||
}
|
||||
|
||||
public static async connect(wallet: DirectSecp256k1HdWallet, url: string, prefix: string): Promise<INetClient> {
|
||||
const [{address}] = await wallet.getAccounts();
|
||||
const [{ address }] = await wallet.getAccounts();
|
||||
const signerOptions: SigningCosmWasmClientOptions = {
|
||||
gasPrice: nymGasPrice(prefix),
|
||||
gasLimits: nymGasLimits,
|
||||
@@ -95,17 +95,17 @@ export default class NetClient implements INetClient {
|
||||
|
||||
public getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise<PagedMixnodeResponse> {
|
||||
if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here.
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {get_mix_nodes: {limit}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit } });
|
||||
} else {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {get_mix_nodes: {limit, start_after}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit, start_after } });
|
||||
}
|
||||
}
|
||||
|
||||
public getGateways(contractAddress: string, limit: number, start_after?: string): Promise<PagedGatewayResponse> {
|
||||
if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here.
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {get_gateways: {limit}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_gateways: { limit } });
|
||||
} else {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {get_gateways: {limit, start_after}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_gateways: { limit, start_after } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,11 +166,11 @@ export default class NetClient implements INetClient {
|
||||
}
|
||||
|
||||
public ownsMixNode(contractAddress: string, address: string): Promise<MixOwnershipResponse> {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {owns_mixnode: {address}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { owns_mixnode: { address } });
|
||||
}
|
||||
|
||||
public ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse> {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {owns_gateway: {address}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { owns_gateway: { address } });
|
||||
}
|
||||
|
||||
public getBalance(address: string, denom: string): Promise<Coin | null> {
|
||||
@@ -178,7 +178,7 @@ export default class NetClient implements INetClient {
|
||||
}
|
||||
|
||||
public getStateParams(contractAddress: string): Promise<StateParams> {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {state_params: {}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { contract_settings_params: {} });
|
||||
}
|
||||
|
||||
public executeContract(senderAddress: string, contractAddress: string, handleMsg: Record<string, unknown>, memo?: string, transferAmount?: readonly Coin[]): Promise<ExecuteResult> {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
MixOwnershipResponse, PagedGatewayDelegationsResponse,
|
||||
PagedGatewayResponse, PagedMixDelegationsResponse,
|
||||
PagedMixnodeResponse,
|
||||
StateParams
|
||||
ContractSettingsParams
|
||||
} from "./types";
|
||||
|
||||
export interface IQueryClient {
|
||||
@@ -28,7 +28,7 @@ export interface IQueryClient {
|
||||
|
||||
ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse>;
|
||||
|
||||
getStateParams(contractAddress: string): Promise<StateParams>;
|
||||
getStateParams(contractAddress: string): Promise<ContractSettingsParams>;
|
||||
|
||||
changeValidator(newUrl: string): Promise<void>
|
||||
}
|
||||
@@ -59,17 +59,17 @@ export default class QueryClient implements IQueryClient {
|
||||
|
||||
public getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise<PagedMixnodeResponse> {
|
||||
if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here.
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {get_mix_nodes: {limit}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit } });
|
||||
} else {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {get_mix_nodes: {limit, start_after}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit, start_after } });
|
||||
}
|
||||
}
|
||||
|
||||
public getGateways(contractAddress: string, limit: number, start_after?: string): Promise<PagedGatewayResponse> {
|
||||
if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here.
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {get_gateways: {limit}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_gateways: { limit } });
|
||||
} else {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {get_gateways: {limit, start_after}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_gateways: { limit, start_after } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,18 +130,18 @@ export default class QueryClient implements IQueryClient {
|
||||
}
|
||||
|
||||
public ownsMixNode(contractAddress: string, address: string): Promise<MixOwnershipResponse> {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {owns_mixnode: {address}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { owns_mixnode: { address } });
|
||||
}
|
||||
|
||||
public ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse> {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {owns_gateway: {address}});
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { owns_gateway: { address } });
|
||||
}
|
||||
|
||||
public getBalance(address: string, stakeDenom: string): Promise<Coin | null> {
|
||||
return this.cosmClient.getBalance(address, stakeDenom);
|
||||
}
|
||||
|
||||
public getStateParams(contractAddress: string): Promise<StateParams> {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, {state_params: {}});
|
||||
public getStateParams(contractAddress: string): Promise<ContractSettingsParams> {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { contract_settings_params: {} });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export type GatewayOwnershipResponse = {
|
||||
has_gateway: boolean,
|
||||
}
|
||||
|
||||
export type StateParams = {
|
||||
export type ContractSettingsParams = {
|
||||
epoch_length: number,
|
||||
// ideally I'd want to define those as `number` rather than `string`, but
|
||||
// rust-side they are defined as Uint128 and Decimal that don't have
|
||||
@@ -103,7 +103,7 @@ export type MixNode = {
|
||||
export type GatewayBond = {
|
||||
owner: string
|
||||
gateway: Gateway,
|
||||
|
||||
|
||||
bond_amount: Coin,
|
||||
total_delegation: Coin,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use crypto::asymmetric::{encryption, identity};
|
||||
use futures::channel::mpsc;
|
||||
use gateway_client::bandwidth::BandwidthController;
|
||||
use gateway_client::GatewayClient;
|
||||
use nymsphinx::acknowledgements::AckKey;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
@@ -17,8 +18,6 @@ use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use wasm_utils::{console_log, console_warn};
|
||||
|
||||
use gateway_client::bandwidth::BandwidthController;
|
||||
|
||||
pub(crate) mod received_processor;
|
||||
|
||||
const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200);
|
||||
@@ -250,7 +249,7 @@ impl NymClient {
|
||||
let validator_client = validator_client::ApiClient::new(self.validator_server.clone());
|
||||
|
||||
let mixnodes = match validator_client.get_cached_active_mixnodes().await {
|
||||
Err(err) => panic!("{}", err),
|
||||
Err(err) => panic!("{:?}", err),
|
||||
Ok(mixes) => mixes,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::GatewayClientError;
|
||||
#[cfg(feature = "coconut")]
|
||||
use credentials::coconut::{
|
||||
bandwidth::{obtain_signature, prepare_for_spending},
|
||||
bandwidth::{
|
||||
obtain_signature, prepare_for_spending, BandwidthVoucherAttributes, TOTAL_ATTRIBUTES,
|
||||
},
|
||||
utils::obtain_aggregate_verification_key,
|
||||
};
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
@@ -12,10 +13,11 @@ use credentials::token::bandwidth::TokenCredential;
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
use crypto::asymmetric::identity;
|
||||
use crypto::asymmetric::identity::PublicKey;
|
||||
use network_defaults::BANDWIDTH_VALUE;
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
use network_defaults::{
|
||||
eth_contract::ETH_JSON_ABI, BANDWIDTH_VALUE, ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS,
|
||||
ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN,
|
||||
eth_contract::ETH_JSON_ABI, ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS, ETH_MIN_BLOCK_DEPTH,
|
||||
TOKENS_TO_BURN,
|
||||
};
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
use rand::rngs::OsRng;
|
||||
@@ -33,6 +35,8 @@ use web3::{
|
||||
Web3,
|
||||
};
|
||||
|
||||
use crate::error::GatewayClientError;
|
||||
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
pub fn eth_contract(web3: Web3<Http>) -> Contract<Http> {
|
||||
Contract::from_json(
|
||||
@@ -108,15 +112,31 @@ 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 bandwidth_credential =
|
||||
obtain_signature(&self.identity.to_bytes(), &self.validator_endpoints).await?;
|
||||
// TODO: Decide what is the value and additional info associated with the bandwidth voucher
|
||||
let bandwidth_credential_attributes = BandwidthVoucherAttributes {
|
||||
serial_number: params.random_scalar(),
|
||||
binding_number: params.random_scalar(),
|
||||
voucher_value: coconut_interface::hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes()),
|
||||
voucher_info: coconut_interface::hash_to_scalar(
|
||||
String::from("BandwidthVoucher").as_bytes(),
|
||||
),
|
||||
};
|
||||
|
||||
let bandwidth_credential = obtain_signature(
|
||||
¶ms,
|
||||
&bandwidth_credential_attributes,
|
||||
&self.validator_endpoints,
|
||||
)
|
||||
.await?;
|
||||
// the above would presumably be loaded from a file
|
||||
|
||||
// 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,
|
||||
&verification_key,
|
||||
)?)
|
||||
}
|
||||
@@ -203,9 +223,10 @@ impl BandwidthController {
|
||||
#[cfg(not(feature = "coconut"))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use network_defaults::ETH_EVENT_NAME;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_contract() {
|
||||
let transport =
|
||||
|
||||
@@ -6,11 +6,11 @@ use crate::nymd::{
|
||||
error::NymdError, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient,
|
||||
};
|
||||
#[cfg(feature = "nymd-client")]
|
||||
use mixnet_contract::StateParams;
|
||||
use mixnet_contract::ContractSettingsParams;
|
||||
|
||||
use crate::{validator_api, ValidatorClientError};
|
||||
use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse};
|
||||
use mixnet_contract::{GatewayBond, MixNodeBond};
|
||||
use mixnet_contract::{GatewayBond, MixNodeBond, MixnodeRewardingStatusResponse};
|
||||
#[cfg(feature = "nymd-client")]
|
||||
use mixnet_contract::{RawDelegationData, RewardingIntervalResponse};
|
||||
use url::Url;
|
||||
@@ -165,11 +165,13 @@ impl<C> Client<C> {
|
||||
Ok(self.validator_api.get_gateways().await?)
|
||||
}
|
||||
|
||||
pub async fn get_state_params(&self) -> Result<StateParams, ValidatorClientError>
|
||||
pub async fn get_contract_settings(
|
||||
&self,
|
||||
) -> Result<ContractSettingsParams, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
Ok(self.nymd.get_state_params().await?)
|
||||
Ok(self.nymd.get_contract_settings().await?)
|
||||
}
|
||||
|
||||
pub async fn get_current_rewarding_interval(
|
||||
@@ -181,6 +183,20 @@ impl<C> Client<C> {
|
||||
Ok(self.nymd.get_current_rewarding_interval().await?)
|
||||
}
|
||||
|
||||
pub async fn get_rewarding_status(
|
||||
&self,
|
||||
mix_identity: mixnet_contract::IdentityKey,
|
||||
rewarding_interval_nonce: u32,
|
||||
) -> Result<MixnodeRewardingStatusResponse, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
Ok(self
|
||||
.nymd
|
||||
.get_rewarding_status(mix_identity, rewarding_interval_nonce)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_reward_pool(&self) -> Result<u128, ValidatorClientError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
|
||||
@@ -13,11 +13,11 @@ use cosmrs::rpc::endpoint::broadcast;
|
||||
use cosmrs::rpc::{Error as TendermintRpcError, HttpClientUrl};
|
||||
use cosmwasm_std::{Coin, Uint128};
|
||||
use mixnet_contract::{
|
||||
Addr, Delegation, ExecuteMsg, Gateway, GatewayOwnershipResponse, IdentityKey,
|
||||
LayerDistribution, MixNode, MixOwnershipResponse, PagedAllDelegationsResponse,
|
||||
PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse,
|
||||
PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData, RewardingIntervalResponse,
|
||||
StateParams,
|
||||
Addr, ContractSettingsParams, Delegation, ExecuteMsg, Gateway, GatewayOwnershipResponse,
|
||||
IdentityKey, LayerDistribution, MixNode, MixOwnershipResponse, MixnodeRewardingStatusResponse,
|
||||
PagedAllDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse,
|
||||
PagedMixnodeResponse, PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData,
|
||||
RewardingIntervalResponse,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -208,7 +208,7 @@ impl<C> NymdClient<C> {
|
||||
self.client.get_balance(address, self.denom()?).await
|
||||
}
|
||||
|
||||
pub async fn get_state_params(&self) -> Result<StateParams, NymdError>
|
||||
pub async fn get_contract_settings(&self) -> Result<ContractSettingsParams, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
@@ -230,6 +230,23 @@ impl<C> NymdClient<C> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_rewarding_status(
|
||||
&self,
|
||||
mix_identity: mixnet_contract::IdentityKey,
|
||||
rewarding_interval_nonce: u32,
|
||||
) -> Result<MixnodeRewardingStatusResponse, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let request = QueryMsg::GetRewardingStatus {
|
||||
mix_identity,
|
||||
rewarding_interval_nonce,
|
||||
};
|
||||
self.client
|
||||
.query_contract_smart(self.contract_address()?, &request)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_layer_distribution(&self) -> Result<LayerDistribution, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
@@ -690,16 +707,16 @@ impl<C> NymdClient<C> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_state_params(
|
||||
pub async fn update_contract_settings(
|
||||
&self,
|
||||
new_params: StateParams,
|
||||
new_params: ContractSettingsParams,
|
||||
) -> Result<ExecuteResult, NymdError>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
{
|
||||
let fee = self.get_fee(Operation::UpdateStateParams);
|
||||
|
||||
let req = ExecuteMsg::UpdateStateParams(new_params);
|
||||
let req = ExecuteMsg::UpdateContractSettings(new_params);
|
||||
self.client
|
||||
.execute(
|
||||
self.address(),
|
||||
|
||||
@@ -8,4 +8,4 @@ description = "Crutch library until there is proper SerDe support for coconut st
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
getset = "0.1.1"
|
||||
|
||||
coconut-rs = { git = "https://github.com/nymtech/coconut.git", branch = "0.5.0" }
|
||||
nymcoconut = {path = "../nymcoconut" }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use getset::{CopyGetters, Getters};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use coconut_rs::*;
|
||||
pub use nymcoconut::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Getters, CopyGetters, Clone)]
|
||||
pub struct Credential {
|
||||
@@ -42,7 +42,7 @@ impl Credential {
|
||||
.iter()
|
||||
.map(hash_to_scalar)
|
||||
.collect::<Vec<Attribute>>();
|
||||
coconut_rs::verify_credential(¶ms, verification_key, &self.theta, &public_attributes)
|
||||
nymcoconut::verify_credential(¶ms, verification_key, &self.theta, &public_attributes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ pub struct BlindSignRequestBody {
|
||||
#[getset(get = "pub")]
|
||||
blind_sign_request: BlindSignRequest,
|
||||
#[getset(get = "pub")]
|
||||
public_key: coconut_rs::PublicKey,
|
||||
public_key: nymcoconut::PublicKey,
|
||||
public_attributes: Vec<String>,
|
||||
#[getset(get = "pub")]
|
||||
total_params: u32,
|
||||
@@ -92,13 +92,13 @@ pub struct BlindSignRequestBody {
|
||||
|
||||
impl BlindSignRequestBody {
|
||||
pub fn new(
|
||||
blind_sign_request: BlindSignRequest,
|
||||
public_key: &coconut_rs::PublicKey,
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
public_key: &nymcoconut::PublicKey,
|
||||
public_attributes: &[Attribute],
|
||||
total_params: u32,
|
||||
) -> BlindSignRequestBody {
|
||||
BlindSignRequestBody {
|
||||
blind_sign_request,
|
||||
blind_sign_request: blind_sign_request.clone(),
|
||||
public_key: public_key.clone(),
|
||||
public_attributes: public_attributes
|
||||
.iter()
|
||||
|
||||
@@ -6,41 +6,71 @@
|
||||
// right now this has no double-spending protection, spender binding, etc
|
||||
// it's the simplest possible case
|
||||
|
||||
use coconut_interface::{
|
||||
Credential, Parameters, PrivateAttribute, PublicAttribute, Signature, VerificationKey,
|
||||
};
|
||||
use network_defaults::BANDWIDTH_VALUE;
|
||||
use url::Url;
|
||||
|
||||
use super::utils::{obtain_aggregate_signature, prepare_credential_for_spending};
|
||||
use crate::error::Error;
|
||||
use coconut_interface::{hash_to_scalar, Credential, Parameters, Signature, VerificationKey};
|
||||
use network_defaults::BANDWIDTH_VALUE;
|
||||
|
||||
pub const PUBLIC_ATTRIBUTES: u32 = 1;
|
||||
pub const PRIVATE_ATTRIBUTES: u32 = 1;
|
||||
use super::utils::{obtain_aggregate_signature, prepare_credential_for_spending};
|
||||
|
||||
pub const PUBLIC_ATTRIBUTES: u32 = 2;
|
||||
pub const PRIVATE_ATTRIBUTES: u32 = 2;
|
||||
pub const TOTAL_ATTRIBUTES: u32 = PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES;
|
||||
|
||||
pub struct BandwidthVoucherAttributes {
|
||||
// a random secret value generated by the client used for double-spending detection
|
||||
pub serial_number: PrivateAttribute,
|
||||
// a random secret value generated by the client used to bind multiple credentials together
|
||||
pub binding_number: PrivateAttribute,
|
||||
// the value (e.g., bandwidth) encoded in this voucher
|
||||
pub voucher_value: PublicAttribute,
|
||||
// a field with public information, e.g., type of voucher, epoch etc.
|
||||
pub voucher_info: PublicAttribute,
|
||||
}
|
||||
|
||||
impl BandwidthVoucherAttributes {
|
||||
pub fn get_public_attributes(&self) -> Vec<PublicAttribute> {
|
||||
vec![self.voucher_value, self.voucher_info]
|
||||
}
|
||||
|
||||
pub fn get_private_attributes(&self) -> Vec<PrivateAttribute> {
|
||||
vec![self.serial_number, self.binding_number]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this definitely has to be moved somewhere else. It's just a temporary solution
|
||||
pub async fn obtain_signature(raw_identity: &[u8], validators: &[Url]) -> Result<Signature, Error> {
|
||||
let public_attributes = vec![hash_to_scalar(BANDWIDTH_VALUE.to_be_bytes())];
|
||||
let private_attributes = vec![hash_to_scalar(raw_identity)];
|
||||
pub async fn obtain_signature(
|
||||
params: &Parameters,
|
||||
attributes: &BandwidthVoucherAttributes,
|
||||
validators: &[Url],
|
||||
) -> Result<Signature, Error> {
|
||||
let public_attributes = attributes.get_public_attributes();
|
||||
let private_attributes = attributes.get_private_attributes();
|
||||
|
||||
let params = Parameters::new(TOTAL_ATTRIBUTES)?;
|
||||
|
||||
obtain_aggregate_signature(¶ms, &public_attributes, &private_attributes, validators).await
|
||||
obtain_aggregate_signature(params, &public_attributes, &private_attributes, validators).await
|
||||
}
|
||||
|
||||
pub fn prepare_for_spending(
|
||||
raw_identity: &[u8],
|
||||
signature: &Signature,
|
||||
attributes: &BandwidthVoucherAttributes,
|
||||
verification_key: &VerificationKey,
|
||||
) -> Result<Credential, Error> {
|
||||
let public_attributes = vec![BANDWIDTH_VALUE.to_be_bytes().to_vec()];
|
||||
let private_attributes = vec![raw_identity.to_vec()];
|
||||
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(
|
||||
¶ms,
|
||||
public_attributes,
|
||||
private_attributes,
|
||||
attributes.serial_number,
|
||||
attributes.binding_number,
|
||||
signature,
|
||||
verification_key,
|
||||
)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::Error;
|
||||
use coconut_interface::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, hash_to_scalar, prepare_blind_sign,
|
||||
prove_credential, Attribute, BlindSignRequestBody, Credential, Parameters, Signature,
|
||||
aggregate_signature_shares, aggregate_verification_keys, prepare_blind_sign,
|
||||
prove_bandwidth_credential, Attribute, BlindSignRequestBody, Credential, Parameters, Signature,
|
||||
SignatureShare, VerificationKey,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::coconut::bandwidth::PRIVATE_ATTRIBUTES;
|
||||
use crate::error::Error;
|
||||
|
||||
/// Contacts all provided validators and then aggregate their verification keys.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -63,17 +65,18 @@ async fn obtain_partial_credential(
|
||||
public_attributes: &[Attribute],
|
||||
private_attributes: &[Attribute],
|
||||
client: &validator_client::ApiClient,
|
||||
validator_vk: &VerificationKey,
|
||||
) -> Result<Signature, Error> {
|
||||
let elgamal_keypair = coconut_interface::elgamal_keygen(params);
|
||||
let blind_sign_request = prepare_blind_sign(
|
||||
params,
|
||||
elgamal_keypair.public_key(),
|
||||
&elgamal_keypair,
|
||||
private_attributes,
|
||||
public_attributes,
|
||||
)?;
|
||||
|
||||
let blind_sign_request_body = BlindSignRequestBody::new(
|
||||
blind_sign_request,
|
||||
&blind_sign_request,
|
||||
elgamal_keypair.public_key(),
|
||||
public_attributes,
|
||||
(public_attributes.len() + private_attributes.len()) as u32,
|
||||
@@ -83,7 +86,17 @@ async fn obtain_partial_credential(
|
||||
.blind_sign(&blind_sign_request_body)
|
||||
.await?
|
||||
.blinded_signature;
|
||||
Ok(blinded_signature.unblind(elgamal_keypair.private_key()))
|
||||
|
||||
let unblinded_signature = blinded_signature.unblind(
|
||||
params,
|
||||
elgamal_keypair.private_key(),
|
||||
validator_vk,
|
||||
private_attributes,
|
||||
public_attributes,
|
||||
&blind_sign_request.get_commitment_hash(),
|
||||
)?;
|
||||
|
||||
Ok(unblinded_signature)
|
||||
}
|
||||
|
||||
pub async fn obtain_aggregate_signature(
|
||||
@@ -97,40 +110,76 @@ pub async fn obtain_aggregate_signature(
|
||||
}
|
||||
|
||||
let mut shares = Vec::with_capacity(validators.len());
|
||||
let mut validators_partial_vks: Vec<VerificationKey> = Vec::with_capacity(validators.len());
|
||||
|
||||
let mut client = validator_client::ApiClient::new(validators[0].clone());
|
||||
let first =
|
||||
obtain_partial_credential(params, public_attributes, private_attributes, &client).await?;
|
||||
let validator_partial_vk = client.get_coconut_verification_key().await?;
|
||||
validators_partial_vks.push(validator_partial_vk.key.clone());
|
||||
|
||||
let first = obtain_partial_credential(
|
||||
params,
|
||||
public_attributes,
|
||||
private_attributes,
|
||||
&client,
|
||||
&validator_partial_vk.key,
|
||||
)
|
||||
.await?;
|
||||
shares.push(SignatureShare::new(first, 0));
|
||||
|
||||
for (id, validator_url) in validators.iter().enumerate().skip(1) {
|
||||
client.change_validator_api(validator_url.clone());
|
||||
let signature =
|
||||
obtain_partial_credential(params, public_attributes, private_attributes, &client)
|
||||
.await?;
|
||||
let validator_partial_vk = client.get_coconut_verification_key().await?;
|
||||
validators_partial_vks.push(validator_partial_vk.key.clone());
|
||||
let signature = obtain_partial_credential(
|
||||
params,
|
||||
public_attributes,
|
||||
private_attributes,
|
||||
&client,
|
||||
&validator_partial_vk.key,
|
||||
)
|
||||
.await?;
|
||||
let share = SignatureShare::new(signature, id as u64);
|
||||
shares.push(share)
|
||||
}
|
||||
|
||||
Ok(aggregate_signature_shares(&shares)?)
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
attributes.extend_from_slice(private_attributes);
|
||||
attributes.extend_from_slice(public_attributes);
|
||||
|
||||
let mut indices: Vec<u64> = Vec::with_capacity(validators_partial_vks.len());
|
||||
for i in 1..validators_partial_vks.len() {
|
||||
indices.push(i as u64);
|
||||
}
|
||||
let verification_key =
|
||||
aggregate_verification_keys(&validators_partial_vks, Some(indices.as_ref()))?;
|
||||
|
||||
Ok(aggregate_signature_shares(
|
||||
params,
|
||||
&verification_key,
|
||||
&attributes,
|
||||
&shares,
|
||||
)?)
|
||||
}
|
||||
|
||||
// TODO: better type flow
|
||||
pub fn prepare_credential_for_spending(
|
||||
params: &Parameters,
|
||||
public_attributes: Vec<Vec<u8>>,
|
||||
private_attributes: Vec<Vec<u8>>,
|
||||
serial_number: Attribute,
|
||||
binding_number: Attribute,
|
||||
signature: &Signature,
|
||||
verification_key: &VerificationKey,
|
||||
) -> Result<Credential, Error> {
|
||||
let private_attributes = private_attributes
|
||||
.iter()
|
||||
.map(hash_to_scalar)
|
||||
.collect::<Vec<Attribute>>();
|
||||
let theta = prove_credential(params, verification_key, signature, &private_attributes)?;
|
||||
let theta = prove_bandwidth_credential(
|
||||
params,
|
||||
verification_key,
|
||||
signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)?;
|
||||
|
||||
Ok(Credential::new(
|
||||
(public_attributes.len() + private_attributes.len()) as u32,
|
||||
(public_attributes.len() + PRIVATE_ATTRIBUTES as usize) as u32,
|
||||
theta,
|
||||
public_attributes,
|
||||
signature,
|
||||
|
||||
@@ -17,7 +17,7 @@ schemars = "0.8"
|
||||
ts-rs = { version = "5.1", optional = true }
|
||||
thiserror = "1.0"
|
||||
network-defaults = { path = "../network-defaults" }
|
||||
fixed = "1.1"
|
||||
fixed = { version = "1.1", features = ["serde"] }
|
||||
az = "1.1"
|
||||
log = "0.4.14"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::fmt::Display;
|
||||
pub struct UnpackedDelegation<T> {
|
||||
pub owner: Addr,
|
||||
pub node_identity: IdentityKey,
|
||||
pub delegation_data: T, // proxy address used to delegate the funds on behalf of anouther address
|
||||
pub delegation_data: T,
|
||||
}
|
||||
|
||||
impl<T> UnpackedDelegation<T> {
|
||||
@@ -28,15 +28,13 @@ impl<T> UnpackedDelegation<T> {
|
||||
pub struct RawDelegationData {
|
||||
pub amount: Uint128,
|
||||
pub block_height: u64,
|
||||
pub proxy: Option<Addr> // proxy address used to delegate the funds on behalf of anouther address
|
||||
}
|
||||
|
||||
impl RawDelegationData {
|
||||
pub fn new(amount: Uint128, block_height: u64, proxy: Option<Addr>) -> Self {
|
||||
pub fn new(amount: Uint128, block_height: u64) -> Self {
|
||||
RawDelegationData {
|
||||
amount,
|
||||
block_height,
|
||||
proxy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ pub mod mixnode;
|
||||
mod msg;
|
||||
mod types;
|
||||
|
||||
pub const MIXNODE_DELEGATORS_PAGE_LIMIT: usize = 250;
|
||||
|
||||
pub use cosmwasm_std::{Addr, Coin};
|
||||
pub use delegation::{
|
||||
Delegation, PagedAllDelegationsResponse, PagedMixDelegationsResponse,
|
||||
@@ -15,8 +17,5 @@ pub use delegation::{
|
||||
};
|
||||
pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse};
|
||||
pub use mixnode::{Layer, MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse};
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::{
|
||||
IdentityKey, IdentityKeyRef, LayerDistribution, RewardingIntervalResponse, SphinxKey,
|
||||
StateParams,
|
||||
};
|
||||
pub use msg::*;
|
||||
pub use types::*;
|
||||
|
||||
@@ -14,6 +14,10 @@ use std::fmt::Display;
|
||||
|
||||
type U128 = fixed::types::U75F53; // u128 with 18 significant digits
|
||||
|
||||
fixed::const_fixed_from_int! {
|
||||
const ONE: U128 = 1;
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)]
|
||||
pub struct MixNode {
|
||||
@@ -110,7 +114,7 @@ impl NodeRewardParams {
|
||||
}
|
||||
|
||||
pub fn one_over_k(&self) -> U128 {
|
||||
U128::from_num(1) / U128::from_num(self.k.u128())
|
||||
ONE / U128::from_num(self.k.u128())
|
||||
}
|
||||
|
||||
pub fn alpha(&self) -> U128 {
|
||||
@@ -118,6 +122,90 @@ impl NodeRewardParams {
|
||||
}
|
||||
}
|
||||
|
||||
// cosmwasm's limited serde doesn't work with U128 directly
|
||||
#[allow(non_snake_case)]
|
||||
pub mod fixed_U128_as_string {
|
||||
use super::U128;
|
||||
use serde::de::Error;
|
||||
use serde::Deserialize;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn serialize<S>(val: &U128, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let s = (*val).to_string();
|
||||
serializer.serialize_str(&s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<U128, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
U128::from_str(&s).map_err(|err| {
|
||||
D::Error::custom(format!(
|
||||
"failed to deserialize U128 with its string representation - {}",
|
||||
err
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// everything required to reward delegator of given mixnode
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DelegatorRewardParams {
|
||||
node_reward_params: NodeRewardParams,
|
||||
|
||||
// to be completely honest I don't understand all consequences of using `#[schemars(with = "String")]`
|
||||
// for U128 here, but it seems that CosmWasm is using the same attribute for their Uint128
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "fixed_U128_as_string")]
|
||||
sigma: U128,
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "fixed_U128_as_string")]
|
||||
profit_margin: U128,
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "fixed_U128_as_string")]
|
||||
node_profit: U128,
|
||||
}
|
||||
|
||||
impl DelegatorRewardParams {
|
||||
pub fn new(mixnode_bond: &MixNodeBond, node_reward_params: NodeRewardParams) -> Self {
|
||||
DelegatorRewardParams {
|
||||
sigma: mixnode_bond.sigma(&node_reward_params),
|
||||
profit_margin: mixnode_bond.profit_margin(),
|
||||
node_profit: mixnode_bond.node_profit(&node_reward_params),
|
||||
node_reward_params,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn determine_delegation_reward(&self, delegation_amount: Uint128) -> u128 {
|
||||
// change all values into their fixed representations
|
||||
let delegation_amount = U128::from_num(delegation_amount.u128());
|
||||
let circulating_supply = U128::from_num(self.node_reward_params.circulating_supply());
|
||||
|
||||
let scaled_delegation_amount = delegation_amount / circulating_supply;
|
||||
let delegator_reward =
|
||||
(ONE - self.profit_margin) * scaled_delegation_amount / self.sigma * self.node_profit;
|
||||
|
||||
let reward = delegator_reward.max(U128::ZERO);
|
||||
if let Some(int_reward) = reward.checked_cast() {
|
||||
int_reward
|
||||
} else {
|
||||
error!(
|
||||
"Could not cast delegator reward ({}) to u128, returning 0",
|
||||
reward,
|
||||
);
|
||||
0u128
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node_reward_params(&self) -> &NodeRewardParams {
|
||||
&self.node_reward_params
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeRewardResult {
|
||||
reward: U128,
|
||||
@@ -148,7 +236,6 @@ pub struct MixNodeBond {
|
||||
pub block_height: u64,
|
||||
pub mix_node: MixNode,
|
||||
pub profit_margin_percent: Option<u8>,
|
||||
pub proxy: Option<Addr>
|
||||
}
|
||||
|
||||
impl MixNodeBond {
|
||||
@@ -159,7 +246,6 @@ impl MixNodeBond {
|
||||
block_height: u64,
|
||||
mix_node: MixNode,
|
||||
profit_margin_percent: Option<u8>,
|
||||
proxy: Option<Addr>,
|
||||
) -> Self {
|
||||
MixNodeBond {
|
||||
total_delegation: coin(0, &bond_amount.denom),
|
||||
@@ -169,7 +255,6 @@ impl MixNodeBond {
|
||||
block_height,
|
||||
mix_node,
|
||||
profit_margin_percent,
|
||||
proxy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,14 +316,14 @@ impl MixNodeBond {
|
||||
|
||||
pub fn reward(&self, params: &NodeRewardParams) -> NodeRewardResult {
|
||||
// Assuming uniform work distribution across the network this is one_over_k * k
|
||||
let omega_k = U128::from_num(1u128);
|
||||
let omega_k = ONE;
|
||||
let lambda = self.lambda(params);
|
||||
let sigma = self.sigma(params);
|
||||
|
||||
let reward = params.performance()
|
||||
* params.period_reward_pool()
|
||||
* (sigma * omega_k + params.alpha() * lambda * sigma * params.k())
|
||||
/ (U128::from_num(1) + params.alpha());
|
||||
/ (ONE + params.alpha());
|
||||
|
||||
NodeRewardResult {
|
||||
reward,
|
||||
@@ -264,7 +349,7 @@ impl MixNodeBond {
|
||||
};
|
||||
let operator_base_reward = reward.reward.min(params.operator_cost());
|
||||
let operator_reward = (self.profit_margin()
|
||||
+ (U128::from_num(1) - self.profit_margin()) * reward.lambda / reward.sigma)
|
||||
+ (ONE - self.profit_margin()) * reward.lambda / reward.sigma)
|
||||
* profit;
|
||||
|
||||
let reward = (operator_reward + operator_base_reward).max(U128::from_num(0));
|
||||
@@ -291,25 +376,8 @@ impl MixNodeBond {
|
||||
}
|
||||
|
||||
pub fn reward_delegation(&self, delegation_amount: Uint128, params: &NodeRewardParams) -> u128 {
|
||||
let scaled_delegation_amount =
|
||||
U128::from_num(delegation_amount.u128()) / U128::from_num(params.circulating_supply());
|
||||
|
||||
let delegator_reward = (U128::from_num(1) - self.profit_margin())
|
||||
* scaled_delegation_amount
|
||||
/ self.sigma(params)
|
||||
* self.node_profit(params);
|
||||
|
||||
let reward = delegator_reward.max(U128::from_num(0));
|
||||
if let Some(int_reward) = reward.checked_cast() {
|
||||
int_reward
|
||||
} else {
|
||||
error!(
|
||||
"Could not cast delegator reward ({}) to u128, returning 0 - mixnode {}",
|
||||
reward,
|
||||
self.identity()
|
||||
);
|
||||
0u128
|
||||
}
|
||||
let reward_params = DelegatorRewardParams::new(self, *params);
|
||||
reward_params.determine_delegation_reward(delegation_amount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,7 +511,6 @@ mod tests {
|
||||
block_height: 100,
|
||||
mix_node: mixnode_fixture(),
|
||||
profit_margin_percent: Some(10),
|
||||
proxy: None
|
||||
};
|
||||
|
||||
let mix2 = MixNodeBond {
|
||||
@@ -454,7 +521,6 @@ mod tests {
|
||||
block_height: 120,
|
||||
mix_node: mixnode_fixture(),
|
||||
profit_margin_percent: Some(10),
|
||||
proxy: None
|
||||
};
|
||||
|
||||
let mix3 = MixNodeBond {
|
||||
@@ -465,7 +531,6 @@ mod tests {
|
||||
block_height: 120,
|
||||
mix_node: mixnode_fixture(),
|
||||
profit_margin_percent: Some(10),
|
||||
proxy: None
|
||||
};
|
||||
|
||||
let mix4 = MixNodeBond {
|
||||
@@ -476,7 +541,6 @@ mod tests {
|
||||
block_height: 120,
|
||||
mix_node: mixnode_fixture(),
|
||||
profit_margin_percent: Some(10),
|
||||
proxy: None
|
||||
};
|
||||
|
||||
let mix5 = MixNodeBond {
|
||||
@@ -487,7 +551,6 @@ mod tests {
|
||||
block_height: 120,
|
||||
mix_node: mixnode_fixture(),
|
||||
profit_margin_percent: Some(10),
|
||||
proxy: None
|
||||
};
|
||||
|
||||
// summary:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::mixnode::NodeRewardParams;
|
||||
use crate::StateParams;
|
||||
use crate::ContractSettingsParams;
|
||||
use crate::{Gateway, IdentityKey, MixNode};
|
||||
use cosmwasm_std::Addr;
|
||||
use schemars::JsonSchema;
|
||||
@@ -21,7 +22,7 @@ pub enum ExecuteMsg {
|
||||
gateway: Gateway,
|
||||
},
|
||||
UnbondGateway {},
|
||||
UpdateStateParams(StateParams),
|
||||
UpdateContractSettings(ContractSettingsParams),
|
||||
|
||||
DelegateToMixnode {
|
||||
mix_identity: IdentityKey,
|
||||
@@ -36,15 +37,6 @@ pub enum ExecuteMsg {
|
||||
rewarding_interval_nonce: u32,
|
||||
},
|
||||
|
||||
RewardMixnode {
|
||||
identity: IdentityKey,
|
||||
// percentage value in range 0-100
|
||||
uptime: u32,
|
||||
|
||||
// nonce of the current rewarding interval
|
||||
rewarding_interval_nonce: u32,
|
||||
},
|
||||
|
||||
FinishMixnodeRewarding {
|
||||
// nonce of the current rewarding interval
|
||||
rewarding_interval_nonce: u32,
|
||||
@@ -58,20 +50,11 @@ pub enum ExecuteMsg {
|
||||
// nonce of the current rewarding interval
|
||||
rewarding_interval_nonce: u32,
|
||||
},
|
||||
DelegateToMixnodeOnBehalf {
|
||||
|
||||
RewardNextMixDelegators {
|
||||
mix_identity: IdentityKey,
|
||||
delegate: Addr,
|
||||
},
|
||||
UndelegateFromMixnodeOnBehalf {
|
||||
mix_identity: IdentityKey,
|
||||
delegate: Addr,
|
||||
},
|
||||
BondMixnodeOnBehalf {
|
||||
mix_node: MixNode,
|
||||
owner: Addr,
|
||||
},
|
||||
UnbondMixnodeOnBehalf {
|
||||
owner: Addr,
|
||||
// nonce of the current rewarding interval
|
||||
rewarding_interval_nonce: u32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -117,6 +100,10 @@ pub enum QueryMsg {
|
||||
GetCirculatingSupply {},
|
||||
GetEpochRewardPercent {},
|
||||
GetSybilResistancePercent {},
|
||||
GetRewardingStatus {
|
||||
mix_identity: IdentityKey,
|
||||
rewarding_interval_nonce: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::mixnode::DelegatorRewardParams;
|
||||
use crate::Layer;
|
||||
use cosmwasm_std::{Decimal, Uint128};
|
||||
use cosmwasm_std::Uint128;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
@@ -34,15 +35,14 @@ pub struct RewardingIntervalResponse {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct StateParams {
|
||||
pub epoch_length: u32, // length of a rewarding epoch/interval, expressed in hours
|
||||
|
||||
pub struct ContractSettingsParams {
|
||||
// so currently epoch_length is being unused and validator API performs rewarding
|
||||
// based on its own epoch length config value. I guess that's fine for time being
|
||||
// however, in the future, the contract constant should be controlling it instead.
|
||||
// pub epoch_length: u32, // length of a rewarding epoch/interval, expressed in hours
|
||||
pub minimum_mixnode_bond: Uint128, // minimum amount a mixnode must bond to get into the system
|
||||
pub minimum_gateway_bond: Uint128, // minimum amount a gateway must bond to get into the system
|
||||
|
||||
pub mixnode_bond_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
|
||||
pub mixnode_delegation_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
|
||||
|
||||
// number of mixnode that are going to get rewarded during current rewarding interval (k_m)
|
||||
// based on overall demand for private bandwidth-
|
||||
pub mixnode_rewarded_set_size: u32,
|
||||
@@ -52,22 +52,11 @@ pub struct StateParams {
|
||||
pub mixnode_active_set_size: u32,
|
||||
}
|
||||
|
||||
impl Display for StateParams {
|
||||
impl Display for ContractSettingsParams {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Contract state parameters: [ ")?;
|
||||
write!(f, "epoch length: {}; ", self.epoch_length)?;
|
||||
write!(f, "minimum mixnode bond: {}; ", self.minimum_mixnode_bond)?;
|
||||
write!(f, "minimum gateway bond: {}; ", self.minimum_gateway_bond)?;
|
||||
write!(
|
||||
f,
|
||||
"mixnode bond reward rate: {}; ",
|
||||
self.mixnode_bond_reward_rate
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
"mixnode delegation reward rate: {}; ",
|
||||
self.mixnode_delegation_reward_rate
|
||||
)?;
|
||||
write!(
|
||||
f,
|
||||
"mixnode rewarded set size: {}",
|
||||
@@ -81,6 +70,33 @@ impl Display for StateParams {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct RewardingResult {
|
||||
pub operator_reward: Uint128,
|
||||
pub total_delegator_reward: Uint128,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PendingDelegatorRewarding {
|
||||
// keep track of the running rewarding results so we'd known how much was the operator and its delegators rewarded
|
||||
pub running_results: RewardingResult,
|
||||
|
||||
pub next_start: String,
|
||||
|
||||
pub rewarding_params: DelegatorRewardParams,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum RewardingStatus {
|
||||
Complete(RewardingResult),
|
||||
PendingNextDelegatorPage(PendingDelegatorRewarding),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MixnodeRewardingStatusResponse {
|
||||
pub status: Option<RewardingStatus>,
|
||||
}
|
||||
|
||||
// type aliases for better reasoning about available data
|
||||
pub type IdentityKey = String;
|
||||
pub type IdentityKeyRef<'a> = &'a str;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
[package]
|
||||
name = "nymcoconut"
|
||||
version = "0.5.0"
|
||||
authors = ["Jedrzej Stuczynski <andrew@nymtech.net>", "Ania Piotrowska <ania@nymtech.net>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] }
|
||||
itertools = "0.10"
|
||||
digest = "0.9"
|
||||
rand = "0.8"
|
||||
thiserror = "1.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
bs58 = "0.4.0"
|
||||
sha2 = "0.9"
|
||||
|
||||
[dependencies.ff]
|
||||
version = "0.10"
|
||||
default-features = false
|
||||
|
||||
[dependencies.group]
|
||||
version = "0.10"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version="0.3", features=["html_reports"] }
|
||||
doc-comment = "0.3"
|
||||
|
||||
[dev-dependencies.bincode]
|
||||
version = "1"
|
||||
|
||||
#[[bench]]
|
||||
#name = "benchmarks"
|
||||
#harness = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies]
|
||||
getrandom = { version="0.2", features=["js"] }
|
||||
@@ -0,0 +1,322 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use core::ops::{Deref, Mul};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G1Projective, Scalar};
|
||||
use group::Curve;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::{try_deserialize_g1_projective, try_deserialize_scalar};
|
||||
|
||||
/// Type alias for the ephemeral key generated during ElGamal encryption
|
||||
pub type EphemeralKey = Scalar;
|
||||
|
||||
/// Two G1 points representing ElGamal ciphertext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct Ciphertext(pub(crate) G1Projective, pub(crate) G1Projective);
|
||||
|
||||
impl TryFrom<&[u8]> for Ciphertext {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Ciphertext> {
|
||||
if bytes.len() != 96 {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"Ciphertext must be exactly 96 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let c1_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap();
|
||||
let c2_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap();
|
||||
|
||||
let c1 = try_deserialize_g1_projective(
|
||||
c1_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed c1".to_string()),
|
||||
)?;
|
||||
let c2 = try_deserialize_g1_projective(
|
||||
c2_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed c2".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(Ciphertext(c1, c2))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ciphertext {
|
||||
pub fn c1(&self) -> &G1Projective {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn c2(&self) -> &G1Projective {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 96] {
|
||||
let mut bytes = [0u8; 96];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Ciphertext> {
|
||||
Ciphertext::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// PrivateKey used in the ElGamal encryption scheme to recover the plaintext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct PrivateKey(pub(crate) Scalar);
|
||||
|
||||
impl PrivateKey {
|
||||
/// Decrypt takes the ElGamal encryption of a message and returns a point on the G1 curve
|
||||
/// that represents original h^m.
|
||||
pub fn decrypt(&self, ciphertext: &Ciphertext) -> G1Projective {
|
||||
let (c1, c2) = &(ciphertext.0, ciphertext.1);
|
||||
|
||||
// (gamma^k * h^m) / (g1^{d * k}) | note: gamma = g1^d
|
||||
c2 - c1 * self.0
|
||||
}
|
||||
|
||||
pub fn public_key(&self, params: &Parameters) -> PublicKey {
|
||||
PublicKey(params.gen1() * self.0)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.0.to_bytes()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8; 32]) -> Result<PrivateKey> {
|
||||
try_deserialize_scalar(
|
||||
bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize ElGamal private key - it was not in the canonical form"
|
||||
.to_string(),
|
||||
),
|
||||
)
|
||||
.map(PrivateKey)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for PrivateKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
PrivateKey::from_bytes(slice.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for PrivateKey {}
|
||||
|
||||
// TODO: perhaps be more explicit and apart from gamma also store generator and group order?
|
||||
/// PublicKey used in the ElGamal encryption scheme to produce the ciphertext
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct PublicKey(G1Projective);
|
||||
|
||||
impl PublicKey {
|
||||
/// Encrypt encrypts the given message in the form of h^m,
|
||||
/// where h is a point on the G1 curve using the given public key.
|
||||
/// The random k is returned alongside the encryption
|
||||
/// as it is required by the Coconut Scheme to create proofs of knowledge.
|
||||
pub fn encrypt(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
h: &G1Projective,
|
||||
msg: &Scalar,
|
||||
) -> (Ciphertext, EphemeralKey) {
|
||||
let k = params.random_scalar();
|
||||
// c1 = g1^k
|
||||
let c1 = params.gen1() * k;
|
||||
// c2 = gamma^k * h^m
|
||||
let c2 = self.0 * k + h * msg;
|
||||
|
||||
(Ciphertext(c1, c2), k)
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 48] {
|
||||
self.to_byte_vec().try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8; 48]) -> Result<PublicKey> {
|
||||
Ok(PublicKey::try_from(bytes.to_vec().as_slice()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for PublicKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.0.to_affine().to_compressed().into()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Ok(PublicKey::from_bytes(slice.try_into().unwrap()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for PublicKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(slice: &[u8]) -> Result<PublicKey> {
|
||||
try_deserialize_g1_projective(
|
||||
slice.try_into().unwrap(),
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed ElGamal public key".to_string(),
|
||||
),
|
||||
)
|
||||
.map(PublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for PublicKey {}
|
||||
|
||||
impl Deref for PublicKey {
|
||||
type Target = G1Projective;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Mul<&'b Scalar> for &'a PublicKey {
|
||||
type Output = G1Projective;
|
||||
|
||||
fn mul(self, rhs: &'b Scalar) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
/// A convenient wrapper for both keys of the ElGamal keypair
|
||||
pub struct ElGamalKeyPair {
|
||||
private_key: PrivateKey,
|
||||
public_key: PublicKey,
|
||||
}
|
||||
|
||||
impl ElGamalKeyPair {
|
||||
pub fn public_key(&self) -> &PublicKey {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
pub fn private_key(&self) -> &PrivateKey {
|
||||
&self.private_key
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a fresh ElGamal keypair using the group generator specified by the provided [Parameters]
|
||||
pub fn elgamal_keygen(params: &Parameters) -> ElGamalKeyPair {
|
||||
let private_key = params.random_scalar();
|
||||
let gamma = params.gen1() * private_key;
|
||||
|
||||
ElGamalKeyPair {
|
||||
private_key: PrivateKey(private_key),
|
||||
public_key: PublicKey(gamma),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn keygen() {
|
||||
let params = Parameters::default();
|
||||
let keypair = super::elgamal_keygen(¶ms);
|
||||
|
||||
let expected = params.gen1() * keypair.private_key.0;
|
||||
let gamma = keypair.public_key.0;
|
||||
assert_eq!(
|
||||
expected, gamma,
|
||||
"Public key, gamma, should be equal to g1^d, where d is the private key"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encryption() {
|
||||
let params = Parameters::default();
|
||||
let keypair = super::elgamal_keygen(¶ms);
|
||||
|
||||
let r = params.random_scalar();
|
||||
let h = params.gen1() * r;
|
||||
let m = params.random_scalar();
|
||||
|
||||
let (ciphertext, ephemeral_key) = keypair.public_key.encrypt(¶ms, &h, &m);
|
||||
|
||||
let expected_c1 = params.gen1() * ephemeral_key;
|
||||
assert_eq!(expected_c1, ciphertext.0, "c1 should be equal to g1^k");
|
||||
|
||||
let expected_c2 = keypair.public_key.0 * ephemeral_key + h * m;
|
||||
assert_eq!(
|
||||
expected_c2, ciphertext.1,
|
||||
"c2 should be equal to gamma^k * h^m"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decryption() {
|
||||
let params = Parameters::default();
|
||||
let keypair = super::elgamal_keygen(¶ms);
|
||||
|
||||
let r = params.random_scalar();
|
||||
let h = params.gen1() * r;
|
||||
let m = params.random_scalar();
|
||||
|
||||
let (ciphertext, _) = keypair.public_key.encrypt(¶ms, &h, &m);
|
||||
let dec = keypair.private_key.decrypt(&ciphertext);
|
||||
|
||||
let expected = h * m;
|
||||
assert_eq!(
|
||||
expected, dec,
|
||||
"after ElGamal decryption, original h^m should be obtained"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_key_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let private_key = PrivateKey(params.random_scalar());
|
||||
let bytes = private_key.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal scalar's bytes
|
||||
assert_eq!(private_key.0.to_bytes(), bytes);
|
||||
assert_eq!(private_key, PrivateKey::from_bytes(&bytes).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_key_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let public_key = PublicKey(params.gen1() * r);
|
||||
let bytes = public_key.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes
|
||||
assert_eq!(public_key.0.to_affine().to_compressed(), bytes);
|
||||
assert_eq!(public_key, PublicKey::from_bytes(&bytes).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ciphertext_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
let ciphertext = Ciphertext(params.gen1() * r, params.gen1() * s);
|
||||
let bytes = ciphertext.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
|
||||
let expected_bytes = [
|
||||
ciphertext.0.to_affine().to_compressed(),
|
||||
ciphertext.1.to_affine().to_compressed(),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(expected_bytes, bytes);
|
||||
assert_eq!(ciphertext, Ciphertext::try_from(&bytes[..]).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// A `Result` alias where the `Err` case is `coconut_rs::Error`.
|
||||
pub type Result<T> = std::result::Result<T, CoconutError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CoconutError {
|
||||
#[error("Setup error: {0}")]
|
||||
Setup(String),
|
||||
|
||||
#[error("encountered error during keygen")]
|
||||
Keygen,
|
||||
|
||||
#[error("Issuance related error: {0}")]
|
||||
Issuance(String),
|
||||
|
||||
#[error("Tried to prepare blind sign request for higher than specified number of attributes (max: {}, requested: {})", max, requested)]
|
||||
IssuanceMaxAttributes { max: usize, requested: usize },
|
||||
|
||||
#[error("Interpolation error: {0}")]
|
||||
Interpolation(String),
|
||||
|
||||
#[error("Aggregation error: {0}")]
|
||||
Aggregation(String),
|
||||
|
||||
#[error("Unblind error: {0}")]
|
||||
Unblind(String),
|
||||
|
||||
#[error("Verification error: {0}")]
|
||||
Verification(String),
|
||||
|
||||
#[error("Deserialization error: {0}")]
|
||||
Deserialization(String),
|
||||
|
||||
#[error(
|
||||
"Deserailization error, expected at least {} bytes, got {}",
|
||||
min,
|
||||
actual
|
||||
)]
|
||||
DeserializationMinLength { min: usize, actual: usize },
|
||||
|
||||
#[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {} or {modulus_target} % {modulus} == 0")]
|
||||
DeserializationInvalidLength {
|
||||
actual: usize,
|
||||
target: usize,
|
||||
modulus_target: usize,
|
||||
modulus: usize,
|
||||
object: String,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use crate::{BlindSignRequest, BlindedSignature, Bytable, Theta};
|
||||
|
||||
macro_rules! impl_clone {
|
||||
($struct:ident) => {
|
||||
impl Clone for $struct {
|
||||
fn clone(&self) -> Self {
|
||||
Self::try_from_byte_slice(&self.to_byte_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_clone!(BlindSignRequest);
|
||||
impl_clone!(BlindedSignature);
|
||||
impl_clone!(Theta);
|
||||
@@ -0,0 +1,2 @@
|
||||
mod clone;
|
||||
mod serde;
|
||||
@@ -0,0 +1,56 @@
|
||||
use crate::elgamal::PrivateKey;
|
||||
use crate::scheme::SecretKey;
|
||||
use crate::{
|
||||
Base58, BlindSignRequest, BlindedSignature, PublicKey, Signature, Theta, VerificationKey,
|
||||
};
|
||||
use serde::de::Unexpected;
|
||||
use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
|
||||
macro_rules! impl_serde {
|
||||
($struct:ident, $visitor:ident) => {
|
||||
pub struct $visitor {}
|
||||
|
||||
impl Serialize for $struct {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_bs58())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for $visitor {
|
||||
type Value = $struct;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(formatter, "A base58 encoded struct")
|
||||
}
|
||||
|
||||
fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
|
||||
match $struct::try_from_bs58(s) {
|
||||
Ok(x) => Ok(x),
|
||||
Err(_) => Err(Error::invalid_value(Unexpected::Str(s), &self)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for $struct {
|
||||
fn deserialize<D>(deserializer: D) -> Result<$struct, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str($visitor {})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_serde!(SecretKey, V1);
|
||||
impl_serde!(VerificationKey, V2);
|
||||
impl_serde!(PublicKey, V3);
|
||||
impl_serde!(PrivateKey, V4);
|
||||
impl_serde!(BlindSignRequest, V5);
|
||||
impl_serde!(BlindedSignature, V6);
|
||||
impl_serde!(Signature, V7);
|
||||
impl_serde!(Theta, V8);
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::Scalar;
|
||||
|
||||
pub use elgamal::elgamal_keygen;
|
||||
pub use elgamal::ElGamalKeyPair;
|
||||
pub use elgamal::PublicKey;
|
||||
pub use error::CoconutError;
|
||||
pub use scheme::aggregation::aggregate_signature_shares;
|
||||
pub use scheme::aggregation::aggregate_verification_keys;
|
||||
pub use scheme::issuance::blind_sign;
|
||||
pub use scheme::issuance::prepare_blind_sign;
|
||||
pub use scheme::issuance::BlindSignRequest;
|
||||
pub use scheme::keygen::ttp_keygen;
|
||||
pub use scheme::keygen::KeyPair;
|
||||
pub use scheme::keygen::VerificationKey;
|
||||
pub use scheme::setup::setup;
|
||||
pub use scheme::setup::Parameters;
|
||||
pub use scheme::verification::prove_bandwidth_credential;
|
||||
pub use scheme::verification::verify_credential;
|
||||
pub use scheme::verification::Theta;
|
||||
pub use scheme::BlindedSignature;
|
||||
pub use scheme::Signature;
|
||||
pub use scheme::SignatureShare;
|
||||
pub use traits::Base58;
|
||||
pub use utils::hash_to_scalar;
|
||||
|
||||
use crate::traits::Bytable;
|
||||
|
||||
pub mod elgamal;
|
||||
mod error;
|
||||
mod impls;
|
||||
mod proofs;
|
||||
mod scheme;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod traits;
|
||||
mod utils;
|
||||
|
||||
pub type Attribute = Scalar;
|
||||
pub type PrivateAttribute = Attribute;
|
||||
pub type PublicAttribute = Attribute;
|
||||
|
||||
impl Bytable for Attribute {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError> {
|
||||
Ok(Attribute::from_bytes(slice.try_into().unwrap()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Attribute {}
|
||||
@@ -0,0 +1,663 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// TODO: look at https://crates.io/crates/merlin to perhaps use it instead?
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G1Projective, G2Projective, Scalar};
|
||||
use digest::generic_array::typenum::Unsigned;
|
||||
use digest::Digest;
|
||||
use group::GroupEncoding;
|
||||
use itertools::izip;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::elgamal::Ciphertext;
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::VerificationKey;
|
||||
use crate::utils::{hash_g1, try_deserialize_scalar, try_deserialize_scalar_vec};
|
||||
use crate::{elgamal, Attribute, ElGamalKeyPair};
|
||||
|
||||
// as per the reference python implementation
|
||||
type ChallengeDigest = Sha256;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ProofCmCs {
|
||||
challenge: Scalar,
|
||||
response_opening: Scalar,
|
||||
response_private_elgamal_key: Scalar,
|
||||
response_keys: Vec<Scalar>,
|
||||
response_attributes: Vec<Scalar>,
|
||||
}
|
||||
|
||||
// note: this is slightly different from the reference python implementation
|
||||
// as we omit the unnecessary string conversion. Instead we concatenate byte
|
||||
// representations together and hash that.
|
||||
// note2: G1 and G2 elements are using their compressed representations
|
||||
// and as per the bls12-381 library all elements are using big-endian form
|
||||
/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points.
|
||||
fn compute_challenge<D, I, B>(iter: I) -> Scalar
|
||||
where
|
||||
D: Digest,
|
||||
I: Iterator<Item = B>,
|
||||
B: AsRef<[u8]>,
|
||||
{
|
||||
let mut h = D::new();
|
||||
for point_representation in iter {
|
||||
h.update(point_representation);
|
||||
}
|
||||
let digest = h.finalize();
|
||||
|
||||
// TODO: I don't like the 0 padding here (though it's what we've been using before,
|
||||
// but we never had a security audit anyway...)
|
||||
// instead we could maybe use the `from_bytes` variant and adding some suffix
|
||||
// when computing the digest until we produce a valid scalar.
|
||||
let mut bytes = [0u8; 64];
|
||||
let pad_size = 64usize
|
||||
.checked_sub(D::OutputSize::to_usize())
|
||||
.unwrap_or_default();
|
||||
|
||||
bytes[pad_size..].copy_from_slice(&digest);
|
||||
|
||||
Scalar::from_bytes_wide(&bytes)
|
||||
}
|
||||
|
||||
fn produce_response(witness: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar {
|
||||
witness - challenge * secret
|
||||
}
|
||||
|
||||
// note: it's caller's responsibility to ensure witnesses.len() = secrets.len()
|
||||
fn produce_responses<S>(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec<Scalar>
|
||||
where
|
||||
S: Borrow<Scalar>,
|
||||
{
|
||||
debug_assert_eq!(witnesses.len(), secrets.len());
|
||||
|
||||
witnesses
|
||||
.iter()
|
||||
.zip(secrets.iter())
|
||||
.map(|(w, x)| produce_response(w, challenge, x.borrow()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl ProofCmCs {
|
||||
/// Construct non-interactive zero-knowledge proof of correctness of the ciphertexts and the commitment
|
||||
/// using the Fiat-Shamir heuristic.
|
||||
pub(crate) fn construct(
|
||||
params: &Parameters,
|
||||
elgamal_keypair: &ElGamalKeyPair,
|
||||
ephemeral_keys: &[elgamal::EphemeralKey],
|
||||
commitment: &G1Projective,
|
||||
commitment_opening: &Scalar,
|
||||
private_attributes: &[Attribute],
|
||||
priv_attributes_ciphertexts: &[Ciphertext],
|
||||
) -> Self {
|
||||
// note: this is only called from `prepare_blind_sign` that already checks
|
||||
// whether private attributes are non-empty and whether we don't have too many
|
||||
// attributes in total to sign.
|
||||
// we also know, due to the single call place, that ephemeral_keys.len() == private_attributes.len()
|
||||
|
||||
// witness creation
|
||||
|
||||
let witness_commitment_opening = params.random_scalar();
|
||||
let witness_private_elgamal_key = params.random_scalar();
|
||||
let witness_keys = params.n_random_scalars(ephemeral_keys.len());
|
||||
let witness_attributes = params.n_random_scalars(private_attributes.len());
|
||||
|
||||
// recompute h
|
||||
let h = hash_g1(commitment.to_bytes());
|
||||
let hs_bytes = params
|
||||
.gen_hs()
|
||||
.iter()
|
||||
.map(|h| h.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let g1 = params.gen1();
|
||||
|
||||
// compute commitments
|
||||
let commitment_private_key_elgamal = g1 * witness_private_elgamal_key;
|
||||
|
||||
// Aw[i] = (wk[i] * g1)
|
||||
let commitment_keys1_bytes = witness_keys
|
||||
.iter()
|
||||
.map(|wk_i| g1 * wk_i)
|
||||
.map(|witness| witness.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Bw[i] = (wm[i] * h) + (wk[i] * gamma)
|
||||
let commitment_keys2_bytes = witness_keys
|
||||
.iter()
|
||||
.zip(witness_attributes.iter())
|
||||
.map(|(wk_i, wm_i)| elgamal_keypair.public_key() * wk_i + h * wm_i)
|
||||
.map(|witness| witness.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// zkp commitment for the attributes commitment cm
|
||||
// Ccm = (wr * g1) + (wm[0] * hs[0]) + ... + (wm[i] * hs[i])
|
||||
let commitment_attributes = g1 * witness_commitment_opening
|
||||
+ witness_attributes
|
||||
.iter()
|
||||
.zip(params.gen_hs().iter())
|
||||
.map(|(wm_i, hs_i)| hs_i * wm_i)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
let ciphertexts_bytes = priv_attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|c| c.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// compute challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen1().to_bytes().as_ref())
|
||||
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
|
||||
.chain(std::iter::once(h.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(
|
||||
elgamal_keypair.public_key().to_bytes().as_ref(),
|
||||
))
|
||||
.chain(std::iter::once(commitment.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(
|
||||
commitment_private_key_elgamal.to_bytes().as_ref(),
|
||||
))
|
||||
.chain(commitment_keys1_bytes.iter().map(|aw| aw.as_ref()))
|
||||
.chain(commitment_keys2_bytes.iter().map(|bw| bw.as_ref()))
|
||||
.chain(ciphertexts_bytes.iter().map(|c| c.as_ref())),
|
||||
);
|
||||
|
||||
// Responses
|
||||
let response_opening =
|
||||
produce_response(&witness_commitment_opening, &challenge, commitment_opening);
|
||||
let response_private_elgamal_key = produce_response(
|
||||
&witness_private_elgamal_key,
|
||||
&challenge,
|
||||
&elgamal_keypair.private_key().0,
|
||||
);
|
||||
let response_keys = produce_responses(&witness_keys, &challenge, ephemeral_keys);
|
||||
let response_attributes = produce_responses(
|
||||
&witness_attributes,
|
||||
&challenge,
|
||||
&private_attributes.iter().collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
ProofCmCs {
|
||||
challenge,
|
||||
response_opening,
|
||||
response_private_elgamal_key,
|
||||
response_keys,
|
||||
response_attributes,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
pub_key: &elgamal::PublicKey,
|
||||
commitment: &G1Projective,
|
||||
attributes_ciphertexts: &[elgamal::Ciphertext],
|
||||
) -> bool {
|
||||
if self.response_keys.len() != attributes_ciphertexts.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// recompute h
|
||||
let h = hash_g1(commitment.to_bytes());
|
||||
let g1 = params.gen1();
|
||||
|
||||
let hs_bytes = params
|
||||
.gen_hs()
|
||||
.iter()
|
||||
.map(|h| h.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// recompute witnesses commitments
|
||||
let commitment_private_key_elgamal =
|
||||
pub_key * &self.challenge + g1 * self.response_private_elgamal_key;
|
||||
|
||||
// Aw[i] = (c * c1[i]) + (rk[i] * g1)
|
||||
let commitment_keys1_bytes = attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|ciphertext| ciphertext.c1())
|
||||
.zip(self.response_keys.iter())
|
||||
.map(|(c1, res_k)| c1 * self.challenge + g1 * res_k)
|
||||
.map(|witness| witness.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Bw[i] = (c * c2[i]) + (rk[i] * gamma) + (rm[i] * h)
|
||||
let commitment_keys2_bytes = izip!(
|
||||
attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|ciphertext| ciphertext.c2()),
|
||||
self.response_keys.iter(),
|
||||
self.response_attributes.iter()
|
||||
)
|
||||
.map(|(c2, res_key, res_attr)| c2 * self.challenge + pub_key * res_key + h * res_attr)
|
||||
.map(|witness| witness.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Cw = (cm * c) + (rr * g1) + (rm[0] * hs[0]) + ... + (rm[n] * hs[n])
|
||||
let commitment_attributes = commitment * self.challenge
|
||||
+ g1 * self.response_opening
|
||||
+ self
|
||||
.response_attributes
|
||||
.iter()
|
||||
.zip(params.gen_hs().iter())
|
||||
.map(|(res_attr, hs)| hs * res_attr)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
let ciphertexts_bytes = attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|c| c.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// re-compute the challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen1().to_bytes().as_ref())
|
||||
.chain(hs_bytes.iter().map(|hs| hs.as_ref()))
|
||||
.chain(std::iter::once(h.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(pub_key.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_attributes.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(
|
||||
commitment_private_key_elgamal.to_bytes().as_ref(),
|
||||
))
|
||||
.chain(commitment_keys1_bytes.iter().map(|aw| aw.as_ref()))
|
||||
.chain(commitment_keys2_bytes.iter().map(|bw| bw.as_ref()))
|
||||
.chain(ciphertexts_bytes.iter().map(|c| c.as_ref())),
|
||||
);
|
||||
|
||||
challenge == self.challenge
|
||||
}
|
||||
|
||||
// challenge || response opening || response private elgamal key || keys len || response keys || attributes len || response attributes
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let keys_len = self.response_keys.len() as u64;
|
||||
let attributes_len = self.response_attributes.len() as u64;
|
||||
|
||||
let mut bytes = Vec::with_capacity(16 + (keys_len + attributes_len + 3) as usize * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.challenge.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_opening.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_private_elgamal_key.to_bytes());
|
||||
bytes.extend_from_slice(&keys_len.to_le_bytes());
|
||||
|
||||
for rk in &self.response_keys {
|
||||
bytes.extend_from_slice(&rk.to_bytes());
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&attributes_len.to_le_bytes());
|
||||
|
||||
for rm in &self.response_attributes {
|
||||
bytes.extend_from_slice(&rm.to_bytes());
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
// at the very minimum there must be a single attribute being proven
|
||||
if bytes.len() < 32 * 5 + 16 || (bytes.len() - 16) % 32 != 0 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
"tried to deserialize proof of ciphertexts and commitment with bytes of invalid length".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
let mut idx = 0;
|
||||
let challenge_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
let response_opening_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
let response_private_elgamal_key_bytes = bytes[idx..idx + 32].try_into().unwrap();
|
||||
idx += 32;
|
||||
|
||||
let challenge = try_deserialize_scalar(
|
||||
&challenge_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
|
||||
)?;
|
||||
let response_opening = try_deserialize_scalar(
|
||||
&response_opening_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize the response to the random".to_string(),
|
||||
),
|
||||
)?;
|
||||
let response_private_elgamal_key = try_deserialize_scalar(
|
||||
&response_private_elgamal_key_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize the response to the private ElGamal key".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
let rk_len = u64::from_le_bytes(bytes[idx..idx + 8].try_into().unwrap());
|
||||
idx += 8;
|
||||
if bytes[idx..].len() < rk_len as usize * 32 + 8 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
"tried to deserialize proof of ciphertexts and commitment with insufficient number of bytes provided".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
let rk_end = idx + rk_len as usize * 32;
|
||||
let response_keys = try_deserialize_scalar_vec(
|
||||
rk_len,
|
||||
&bytes[idx..rk_end],
|
||||
CoconutError::Deserialization("Failed to deserialize keys response".to_string()),
|
||||
)?;
|
||||
|
||||
let rm_len = u64::from_le_bytes(bytes[rk_end..rk_end + 8].try_into().unwrap());
|
||||
let response_attributes = try_deserialize_scalar_vec(
|
||||
rm_len,
|
||||
&bytes[rk_end + 8..],
|
||||
CoconutError::Deserialization("Failed to deserialize attributes response".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(ProofCmCs {
|
||||
challenge,
|
||||
response_opening,
|
||||
response_private_elgamal_key,
|
||||
response_keys,
|
||||
response_attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ProofKappaZeta {
|
||||
// c
|
||||
challenge: Scalar,
|
||||
|
||||
// responses
|
||||
response_serial_number: Scalar,
|
||||
response_binding_number: Scalar,
|
||||
response_blinder: Scalar,
|
||||
}
|
||||
|
||||
impl ProofKappaZeta {
|
||||
pub(crate) fn construct(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
serial_number: &Attribute,
|
||||
binding_number: &Attribute,
|
||||
blinding_factor: &Scalar,
|
||||
blinded_message: &G2Projective,
|
||||
blinded_serial_number: &G2Projective,
|
||||
) -> Self {
|
||||
// create the witnesses
|
||||
let witness_blinder = params.random_scalar();
|
||||
let witness_serial_number = params.random_scalar();
|
||||
let witness_binding_number = params.random_scalar();
|
||||
let witness_attributes = vec![witness_serial_number, witness_binding_number];
|
||||
|
||||
let beta_bytes = verification_key
|
||||
.beta
|
||||
.iter()
|
||||
.map(|beta_i| beta_i.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// witnesses commitments
|
||||
// Aw = g2 * wt + alpha + beta[0] * wm[0] + ... + beta[i] * wm[i]
|
||||
let commitment_kappa = params.gen2() * witness_blinder
|
||||
+ verification_key.alpha
|
||||
+ witness_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(wm_i, beta_i)| beta_i * wm_i)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// zeta is the public value associated with the serial number
|
||||
let commitment_zeta = params.gen2() * witness_serial_number;
|
||||
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen2().to_bytes().as_ref())
|
||||
.chain(std::iter::once(blinded_message.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(blinded_serial_number.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
|
||||
.chain(beta_bytes.iter().map(|b| b.as_ref()))
|
||||
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
|
||||
);
|
||||
|
||||
// responses
|
||||
let response_blinder = produce_response(&witness_blinder, &challenge, blinding_factor);
|
||||
let response_serial_number =
|
||||
produce_response(&witness_serial_number, &challenge, serial_number);
|
||||
let response_binding_number =
|
||||
produce_response(&witness_binding_number, &challenge, binding_number);
|
||||
|
||||
ProofKappaZeta {
|
||||
challenge,
|
||||
response_serial_number,
|
||||
response_binding_number,
|
||||
response_blinder,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn private_attributes_len(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
pub(crate) fn verify(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
kappa: &G2Projective,
|
||||
zeta: &G2Projective,
|
||||
) -> bool {
|
||||
let beta_bytes = verification_key
|
||||
.beta
|
||||
.iter()
|
||||
.map(|beta_i| beta_i.to_bytes())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let response_attributes = vec![self.response_serial_number, self.response_binding_number];
|
||||
// re-compute witnesses commitments
|
||||
// Aw = (c * kappa) + (rt * g2) + ((1 - c) * alpha) + (rm[0] * beta[0]) + ... + (rm[i] * beta[i])
|
||||
let commitment_kappa = kappa * self.challenge
|
||||
+ params.gen2() * self.response_blinder
|
||||
+ verification_key.alpha * (Scalar::one() - self.challenge)
|
||||
+ response_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// zeta is the public value associated with the serial number
|
||||
let commitment_zeta = zeta * self.challenge + params.gen2() * self.response_serial_number;
|
||||
|
||||
// compute the challenge
|
||||
let challenge = compute_challenge::<ChallengeDigest, _, _>(
|
||||
std::iter::once(params.gen2().to_bytes().as_ref())
|
||||
.chain(std::iter::once(kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(zeta.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(verification_key.alpha.to_bytes().as_ref()))
|
||||
.chain(beta_bytes.iter().map(|b| b.as_ref()))
|
||||
.chain(std::iter::once(commitment_kappa.to_bytes().as_ref()))
|
||||
.chain(std::iter::once(commitment_zeta.to_bytes().as_ref())),
|
||||
);
|
||||
|
||||
challenge == self.challenge
|
||||
}
|
||||
|
||||
// challenge || response serial number || response binding number || repose blinder
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let attributes_len = 2; // because we have serial number and the binding number
|
||||
let mut bytes = Vec::with_capacity((1 + attributes_len + 1) as usize * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.challenge.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_serial_number.to_bytes());
|
||||
bytes.extend_from_slice(&self.response_binding_number.to_bytes());
|
||||
|
||||
bytes.extend_from_slice(&self.response_blinder.to_bytes());
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
// at the very minimum there must be a single attribute being proven
|
||||
if bytes.len() < 32 * 4 || (bytes.len()) % 32 != 0 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len(),
|
||||
modulus: 32,
|
||||
object: "kappa and zeta".to_string(),
|
||||
target: 32 * 4,
|
||||
});
|
||||
}
|
||||
|
||||
let challenge_bytes = bytes[..32].try_into().unwrap();
|
||||
let challenge = try_deserialize_scalar(
|
||||
&challenge_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize challenge".to_string()),
|
||||
)?;
|
||||
|
||||
let serial_number_bytes = &bytes[32..64].try_into().unwrap();
|
||||
let response_serial_number = try_deserialize_scalar(
|
||||
serial_number_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the serial number".to_string()),
|
||||
)?;
|
||||
|
||||
let binding_number_bytes = &bytes[64..96].try_into().unwrap();
|
||||
let response_binding_number = try_deserialize_scalar(
|
||||
binding_number_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the binding number".to_string()),
|
||||
)?;
|
||||
|
||||
let blinder_bytes = bytes[96..].try_into().unwrap();
|
||||
let response_blinder = try_deserialize_scalar(
|
||||
&blinder_bytes,
|
||||
CoconutError::Deserialization("failed to deserialize the blinder".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(ProofKappaZeta {
|
||||
challenge,
|
||||
response_serial_number,
|
||||
response_binding_number,
|
||||
response_blinder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// proof builder:
|
||||
// - commitment
|
||||
// - challenge
|
||||
// - responses
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use group::Group;
|
||||
use rand::thread_rng;
|
||||
|
||||
use crate::scheme::issuance::{compute_attribute_encryption, compute_commitment_hash};
|
||||
use crate::scheme::keygen::keygen;
|
||||
use crate::scheme::setup::setup;
|
||||
use crate::scheme::verification::{compute_kappa, compute_zeta};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn proof_cm_cs_bytes_roundtrip() {
|
||||
let mut rng = thread_rng();
|
||||
let mut params = setup(1).unwrap();
|
||||
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(¶ms);
|
||||
let private_attributes = params.n_random_scalars(1);
|
||||
|
||||
// we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes
|
||||
let cm = G1Projective::random(&mut rng);
|
||||
let r = params.random_scalar();
|
||||
|
||||
let commitment_hash = compute_commitment_hash(cm);
|
||||
let (attributes_ciphertexts, _): (Vec<_>, Vec<_>) = compute_attribute_encryption(
|
||||
¶ms,
|
||||
private_attributes.as_ref(),
|
||||
elgamal_keypair.public_key(),
|
||||
commitment_hash,
|
||||
);
|
||||
let ephemeral_keys = params.n_random_scalars(1);
|
||||
|
||||
// 0 public 1 private
|
||||
let pi_s = ProofCmCs::construct(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&ephemeral_keys,
|
||||
&cm,
|
||||
&r,
|
||||
&private_attributes,
|
||||
&*attributes_ciphertexts,
|
||||
);
|
||||
|
||||
let bytes = pi_s.to_bytes();
|
||||
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
|
||||
|
||||
// 2 private
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
let ephemeral_keys = params.n_random_scalars(2);
|
||||
|
||||
let pi_s = ProofCmCs::construct(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&ephemeral_keys,
|
||||
&cm,
|
||||
&r,
|
||||
&private_attributes,
|
||||
&*attributes_ciphertexts,
|
||||
);
|
||||
|
||||
let bytes = pi_s.to_bytes();
|
||||
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_kappa_zeta_bytes_roundtrip() {
|
||||
let mut params = setup(4).unwrap();
|
||||
|
||||
let keypair = keygen(&mut params);
|
||||
|
||||
// we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
|
||||
let r = params.random_scalar();
|
||||
let kappa = compute_kappa(¶ms, &keypair.verification_key(), &private_attributes, r);
|
||||
let zeta = compute_zeta(¶ms, serial_number);
|
||||
|
||||
// 0 public 2 private
|
||||
let pi_v = ProofKappaZeta::construct(
|
||||
&mut params,
|
||||
&keypair.verification_key(),
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
&r,
|
||||
&kappa,
|
||||
&zeta,
|
||||
);
|
||||
|
||||
let proof_bytes = pi_v.to_bytes();
|
||||
|
||||
let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap();
|
||||
assert_eq!(proof_from_bytes, pi_v);
|
||||
|
||||
// 2 public 2 private
|
||||
let mut params = setup(4).unwrap();
|
||||
let keypair = keygen(&mut params);
|
||||
|
||||
let pi_v = ProofKappaZeta::construct(
|
||||
&mut params,
|
||||
&keypair.verification_key(),
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
&r,
|
||||
&kappa,
|
||||
&zeta,
|
||||
);
|
||||
|
||||
let proof_bytes = pi_v.to_bytes();
|
||||
|
||||
let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap();
|
||||
assert_eq!(proof_from_bytes, pi_v);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use core::iter::Sum;
|
||||
use core::ops::Mul;
|
||||
|
||||
use bls12_381::{G2Prepared, G2Projective, Scalar};
|
||||
use group::Curve;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::verification::check_bilinear_pairing;
|
||||
use crate::scheme::{PartialSignature, Signature, SignatureShare, SignerIndex, VerificationKey};
|
||||
use crate::utils::perform_lagrangian_interpolation_at_origin;
|
||||
use crate::{Attribute, Parameters};
|
||||
|
||||
pub(crate) trait Aggregatable: Sized {
|
||||
fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self>;
|
||||
|
||||
fn check_unique_indices(indices: &[SignerIndex]) -> bool {
|
||||
// if aggregation is a threshold one, all indices should be unique
|
||||
indices.iter().unique_by(|&index| index).count() == indices.len()
|
||||
}
|
||||
}
|
||||
|
||||
// includes `VerificationKey`
|
||||
impl<T> Aggregatable for T
|
||||
where
|
||||
T: Sum,
|
||||
for<'a> T: Sum<&'a T>,
|
||||
for<'a> &'a T: Mul<Scalar, Output = T>,
|
||||
{
|
||||
fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result<T> {
|
||||
if aggregatable.is_empty() {
|
||||
return Err(CoconutError::Aggregation("Empty set of values".to_string()));
|
||||
}
|
||||
|
||||
if let Some(indices) = indices {
|
||||
if !Self::check_unique_indices(indices) {
|
||||
return Err(CoconutError::Aggregation("Non-unique indices".to_string()));
|
||||
}
|
||||
perform_lagrangian_interpolation_at_origin(indices, aggregatable)
|
||||
} else {
|
||||
// non-threshold
|
||||
Ok(aggregatable.iter().sum())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Aggregatable for PartialSignature {
|
||||
fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result<Signature> {
|
||||
let h = sigs
|
||||
.get(0)
|
||||
.ok_or_else(|| CoconutError::Aggregation("Empty set of signatures".to_string()))?
|
||||
.sig1();
|
||||
|
||||
// TODO: is it possible to avoid this allocation?
|
||||
let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::<Vec<_>>();
|
||||
let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?;
|
||||
|
||||
Ok(Signature(*h, aggr_sigma))
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures all provided verification keys were generated to verify the same number of attributes.
|
||||
fn check_same_key_size(keys: &[VerificationKey]) -> bool {
|
||||
keys.iter().map(|vk| vk.beta.len()).all_equal()
|
||||
}
|
||||
|
||||
pub fn aggregate_verification_keys(
|
||||
keys: &[VerificationKey],
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<VerificationKey> {
|
||||
if !check_same_key_size(keys) {
|
||||
return Err(CoconutError::Aggregation(
|
||||
"Verification keys are of different sizes".to_string(),
|
||||
));
|
||||
}
|
||||
Aggregatable::aggregate(keys, indices)
|
||||
}
|
||||
|
||||
pub fn aggregate_signatures(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[Attribute],
|
||||
signatures: &[PartialSignature],
|
||||
indices: Option<&[SignerIndex]>,
|
||||
) -> Result<Signature> {
|
||||
// aggregate the signature
|
||||
|
||||
let signature = match Aggregatable::aggregate(signatures, indices) {
|
||||
Ok(res) => res,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
// Verify the signature
|
||||
let alpha = verification_key.alpha;
|
||||
|
||||
let tmp = attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
if !check_bilinear_pairing(
|
||||
&signature.0.to_affine(),
|
||||
&G2Prepared::from((alpha + tmp).to_affine()),
|
||||
&signature.1.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) {
|
||||
return Err(CoconutError::Aggregation(
|
||||
"Verification of the aggregated signature failed".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub fn aggregate_signature_shares(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
attributes: &[Attribute],
|
||||
shares: &[SignatureShare],
|
||||
) -> Result<Signature> {
|
||||
let (signatures, indices): (Vec<_>, Vec<_>) = shares
|
||||
.iter()
|
||||
.map(|share| (*share.signature(), share.index()))
|
||||
.unzip();
|
||||
|
||||
aggregate_signatures(
|
||||
params,
|
||||
verification_key,
|
||||
attributes,
|
||||
&signatures,
|
||||
Some(&indices),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bls12_381::G1Projective;
|
||||
use group::Group;
|
||||
|
||||
use crate::scheme::issuance::sign;
|
||||
use crate::scheme::keygen::ttp_keygen;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::verification::verify;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_works_for_any_subset_of_keys() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
|
||||
let vks = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let aggr_vk1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
|
||||
let aggr_vk2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
|
||||
|
||||
assert_eq!(aggr_vk1, aggr_vk2);
|
||||
|
||||
// TODO: should those two actually work or not?
|
||||
// aggregating threshold+1
|
||||
let aggr_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
|
||||
assert_eq!(aggr_vk1, aggr_more);
|
||||
|
||||
// aggregating all
|
||||
let aggr_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
|
||||
assert_eq!(aggr_all, aggr_vk1);
|
||||
|
||||
// not taking enough points (threshold was 3)
|
||||
let aggr_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
assert_ne!(aggr_not_enough, aggr_vk1);
|
||||
|
||||
// taking wrong index
|
||||
let aggr_bad = aggregate_verification_keys(&vks[2..], Some(&[42, 123, 100])).unwrap();
|
||||
assert_ne!(aggr_vk1, aggr_bad);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_for_empty_set_of_keys() {
|
||||
let keys: Vec<VerificationKey> = vec![];
|
||||
assert!(aggregate_verification_keys(&keys, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_if_indices_have_invalid_length() {
|
||||
let keys = vec![VerificationKey::identity(3)];
|
||||
|
||||
assert!(aggregate_verification_keys(&keys, Some(&[])).is_err());
|
||||
assert!(aggregate_verification_keys(&keys, Some(&[1, 2])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_for_non_unique_indices() {
|
||||
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(3)];
|
||||
|
||||
assert!(aggregate_verification_keys(&keys, Some(&[1, 1])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_aggregation_doesnt_work_for_keys_of_different_size() {
|
||||
let keys = vec![VerificationKey::identity(3), VerificationKey::identity(1)];
|
||||
|
||||
assert!(aggregate_verification_keys(&keys, None).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_works_for_any_subset_of_signatures() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
|
||||
let (sks, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.unzip();
|
||||
|
||||
let sigs = sks
|
||||
.iter()
|
||||
.map(|sk| sign(&mut params, sk, &attributes).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// aggregating (any) threshold works
|
||||
let aggr_vk_1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap();
|
||||
let aggr_sig1 = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_1,
|
||||
&attributes,
|
||||
&sigs[..3],
|
||||
Some(&[1, 2, 3]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let aggr_vk_2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap();
|
||||
let aggr_sig2 = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_1,
|
||||
&attributes,
|
||||
&sigs[2..],
|
||||
Some(&[3, 4, 5]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(aggr_sig1, aggr_sig2);
|
||||
|
||||
// verify credential for good measure
|
||||
assert!(verify(¶ms, &aggr_vk_1, &attributes, &aggr_sig1));
|
||||
assert!(verify(¶ms, &aggr_vk_2, &attributes, &aggr_sig2));
|
||||
|
||||
// aggregating threshold+1 works
|
||||
let aggr_vk_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap();
|
||||
let aggr_more = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_more,
|
||||
&attributes,
|
||||
&sigs[1..],
|
||||
Some(&[2, 3, 4, 5]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(aggr_sig1, aggr_more);
|
||||
|
||||
// aggregating all
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap();
|
||||
let aggr_all = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&sigs,
|
||||
Some(&[1, 2, 3, 4, 5]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(aggr_all, aggr_sig1);
|
||||
|
||||
// not taking enough points (threshold was 3) should fail
|
||||
let aggr_vk_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
let aggr_not_enough = aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_not_enough,
|
||||
&attributes,
|
||||
&sigs[..2],
|
||||
Some(&[1, 2]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(aggr_not_enough, aggr_sig1);
|
||||
|
||||
// taking wrong index should fail
|
||||
let aggr_vk_bad = aggregate_verification_keys(&vks[2..], Some(&[1, 2, 3])).unwrap();
|
||||
assert!(aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_bad,
|
||||
&attributes,
|
||||
&sigs[2..],
|
||||
Some(&[42, 123, 100]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
fn random_signature() -> Signature {
|
||||
let mut rng = rand::thread_rng();
|
||||
Signature(
|
||||
G1Projective::random(&mut rng),
|
||||
G1Projective::random(&mut rng),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_doesnt_work_for_empty_set_of_signatures() {
|
||||
let signatures: Vec<Signature> = vec![];
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.unzip();
|
||||
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
assert!(
|
||||
aggregate_signatures(¶ms, &aggr_vk_all, &attributes, &signatures, None).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_doesnt_work_if_indices_have_invalid_length() {
|
||||
let signatures = vec![random_signature()];
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
assert!(
|
||||
aggregate_signatures(¶ms, &aggr_vk_all, &attributes, &signatures, Some(&[]))
|
||||
.is_err()
|
||||
);
|
||||
assert!(aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
Some(&[1, 2]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_aggregation_doesnt_work_for_non_unique_indices() {
|
||||
let signatures = vec![random_signature(), random_signature()];
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
|
||||
let (_, vks): (Vec<_>, Vec<_>) = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
|
||||
.unzip();
|
||||
let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap();
|
||||
|
||||
assert!(aggregate_signatures(
|
||||
¶ms,
|
||||
&aggr_vk_all,
|
||||
&attributes,
|
||||
&signatures,
|
||||
Some(&[1, 1]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
// TODO: test for aggregating non-threshold keys
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G1Affine, G1Projective, Scalar};
|
||||
use group::{Curve, GroupEncoding};
|
||||
|
||||
use crate::elgamal::{Ciphertext, EphemeralKey};
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::proofs::ProofCmCs;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::BlindedSignature;
|
||||
use crate::scheme::SecretKey;
|
||||
/// Creates a Coconut Signature under a given secret key on a set of public attributes only.
|
||||
#[cfg(test)]
|
||||
use crate::Signature;
|
||||
use crate::{elgamal, Attribute, ElGamalKeyPair};
|
||||
// TODO: possibly completely remove those two functions.
|
||||
// They only exist to have a simpler and smaller code snippets to test
|
||||
// basic functionalities.
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::{hash_g1, try_deserialize_g1_projective};
|
||||
|
||||
// TODO NAMING: double check this one
|
||||
// Lambda
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct BlindSignRequest {
|
||||
// cm
|
||||
commitment: G1Projective,
|
||||
// h
|
||||
commitment_hash: G1Projective,
|
||||
// c
|
||||
private_attributes_ciphertexts: Vec<elgamal::Ciphertext>,
|
||||
// pi_s
|
||||
pi_s: ProofCmCs,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for BlindSignRequest {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<BlindSignRequest> {
|
||||
if bytes.len() < 48 + 48 + 8 + 96 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: 48 + 48 + 8 + 96,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut j = 0;
|
||||
let commitment_bytes_len = 48;
|
||||
let commitment_hash_bytes_len = 48;
|
||||
|
||||
let cm_bytes = bytes[..j + commitment_bytes_len].try_into().unwrap();
|
||||
let commitment = try_deserialize_g1_projective(
|
||||
&cm_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed commitment".to_string(),
|
||||
),
|
||||
)?;
|
||||
j += commitment_bytes_len;
|
||||
|
||||
let cm_hash_bytes = bytes[j..j + commitment_hash_bytes_len].try_into().unwrap();
|
||||
let commitment_hash = try_deserialize_g1_projective(
|
||||
&cm_hash_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize compressed commitment hash".to_string(),
|
||||
),
|
||||
)?;
|
||||
j += commitment_hash_bytes_len;
|
||||
|
||||
let c_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap());
|
||||
j += 8;
|
||||
if bytes[j..].len() < c_len as usize * 96 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: c_len as usize * 96,
|
||||
actual: bytes[56..].len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut private_attributes_ciphertexts = Vec::with_capacity(c_len as usize);
|
||||
for i in 0..c_len as usize {
|
||||
let start = j + i * 96;
|
||||
let end = start + 96;
|
||||
private_attributes_ciphertexts.push(Ciphertext::try_from(&bytes[start..end])?)
|
||||
}
|
||||
|
||||
let pi_s = ProofCmCs::from_bytes(&bytes[j + c_len as usize * 96..])?;
|
||||
|
||||
Ok(BlindSignRequest {
|
||||
commitment,
|
||||
commitment_hash,
|
||||
private_attributes_ciphertexts,
|
||||
pi_s,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for BlindSignRequest {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
let cm_bytes = self.commitment.to_affine().to_compressed();
|
||||
let cm_hash_bytes = self.commitment_hash.to_affine().to_compressed();
|
||||
let c_len = self.private_attributes_ciphertexts.len() as u64;
|
||||
let proof_bytes = self.pi_s.to_bytes();
|
||||
|
||||
let mut bytes = Vec::with_capacity(48 + 48 + 8 + c_len as usize * 96 + proof_bytes.len());
|
||||
|
||||
bytes.extend_from_slice(&cm_bytes);
|
||||
bytes.extend_from_slice(&cm_hash_bytes);
|
||||
bytes.extend_from_slice(&c_len.to_le_bytes());
|
||||
for c in &self.private_attributes_ciphertexts {
|
||||
bytes.extend_from_slice(&c.to_bytes());
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&proof_bytes);
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
BlindSignRequest::from_bytes(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for BlindSignRequest {}
|
||||
|
||||
impl BlindSignRequest {
|
||||
fn verify_proof(&self, params: &Parameters, pub_key: &elgamal::PublicKey) -> bool {
|
||||
self.pi_s.verify(
|
||||
params,
|
||||
pub_key,
|
||||
&self.commitment,
|
||||
&self.private_attributes_ciphertexts,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_commitment_hash(&self) -> G1Projective {
|
||||
self.commitment_hash
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.to_byte_vec()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<BlindSignRequest> {
|
||||
BlindSignRequest::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_private_attributes_commitment(
|
||||
params: &Parameters,
|
||||
private_attributes: &[Attribute],
|
||||
hs: &[G1Affine],
|
||||
) -> (Scalar, G1Projective) {
|
||||
let commitment_opening = params.random_scalar();
|
||||
|
||||
// Produces h0 ^ m0 * h1^m1 * .... * hn^mn
|
||||
// where m0, m1, ...., mn are private attributes
|
||||
let attr_cm = private_attributes
|
||||
.iter()
|
||||
.zip(hs)
|
||||
.map(|(&m, h)| h * m)
|
||||
.sum::<G1Projective>();
|
||||
|
||||
// Produces g1^r * h0 ^ m0 * h1^m1 * .... * hn^mn
|
||||
let commitment = params.gen1() * commitment_opening + attr_cm;
|
||||
(commitment_opening, commitment)
|
||||
}
|
||||
|
||||
pub fn compute_commitment_hash(commitment: G1Projective) -> G1Projective {
|
||||
hash_g1(commitment.to_bytes())
|
||||
}
|
||||
|
||||
pub fn compute_attribute_encryption(
|
||||
params: &Parameters,
|
||||
private_attributes: &[Attribute],
|
||||
pub_key: &elgamal::PublicKey,
|
||||
commitment_hash: G1Projective,
|
||||
) -> (Vec<Ciphertext>, Vec<EphemeralKey>) {
|
||||
private_attributes
|
||||
.iter()
|
||||
.map(|m| pub_key.encrypt(params, &commitment_hash, m))
|
||||
.unzip()
|
||||
}
|
||||
|
||||
/// Builds cryptographic material required for blind sign.
|
||||
pub fn prepare_blind_sign(
|
||||
params: &Parameters,
|
||||
elgamal_keypair: &ElGamalKeyPair,
|
||||
private_attributes: &[Attribute],
|
||||
public_attributes: &[Attribute],
|
||||
) -> Result<BlindSignRequest> {
|
||||
if private_attributes.is_empty() {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Tried to prepare blind sign request for an empty set of private attributes"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let hs = params.gen_hs();
|
||||
if private_attributes.len() + public_attributes.len() > hs.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
max: hs.len(),
|
||||
requested: private_attributes.len() + public_attributes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let (commitment_opening, commitment) =
|
||||
compute_private_attributes_commitment(params, private_attributes, hs);
|
||||
|
||||
// Compute the challenge as the commitment hash
|
||||
let commitment_hash = compute_commitment_hash(commitment);
|
||||
// build ElGamal encryption
|
||||
let (private_attributes_ciphertexts, ephemeral_keys): (Vec<_>, Vec<_>) =
|
||||
compute_attribute_encryption(
|
||||
params,
|
||||
private_attributes,
|
||||
elgamal_keypair.public_key(),
|
||||
commitment_hash,
|
||||
);
|
||||
|
||||
let pi_s = ProofCmCs::construct(
|
||||
params,
|
||||
elgamal_keypair,
|
||||
&ephemeral_keys,
|
||||
&commitment,
|
||||
&commitment_opening,
|
||||
private_attributes,
|
||||
&*private_attributes_ciphertexts,
|
||||
);
|
||||
|
||||
Ok(BlindSignRequest {
|
||||
commitment,
|
||||
commitment_hash,
|
||||
private_attributes_ciphertexts,
|
||||
pi_s,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn blind_sign(
|
||||
params: &Parameters,
|
||||
signing_secret_key: &SecretKey,
|
||||
prover_pub_key: &elgamal::PublicKey,
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
public_attributes: &[Attribute],
|
||||
) -> Result<BlindedSignature> {
|
||||
let num_private = blind_sign_request.private_attributes_ciphertexts.len();
|
||||
let hs = params.gen_hs();
|
||||
|
||||
if num_private + public_attributes.len() > hs.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
max: hs.len(),
|
||||
requested: num_private + public_attributes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the commitment hash
|
||||
let h = hash_g1(blind_sign_request.commitment.to_bytes());
|
||||
if !(h == blind_sign_request.commitment_hash) {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Failed to verify the commitment hash".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Verify the ZK proof
|
||||
if !blind_sign_request.verify_proof(params, prover_pub_key) {
|
||||
return Err(CoconutError::Issuance(
|
||||
"Failed to verify the proof of knowledge".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// in python implementation there are n^2 G1 multiplications, let's do it with a single one instead.
|
||||
// i.e. compute h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n]) directly (where m is number of PRIVATE attributes)
|
||||
// rather than ((h ^ pub_m[0]) ^ y[m + 1] , (h ^ pub_m[1]) ^ y[m + 2] , ...).sum() separately
|
||||
let signed_public = h * public_attributes
|
||||
.iter()
|
||||
.zip(signing_secret_key.ys.iter().skip(num_private))
|
||||
.map(|(attr, yi)| attr * yi)
|
||||
.sum::<Scalar>();
|
||||
|
||||
// c1[0] ^ y[0] * ... * c1[m] ^ y[m]
|
||||
let sig_1 = blind_sign_request
|
||||
.private_attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|ciphertext| ciphertext.c1())
|
||||
.zip(signing_secret_key.ys.iter())
|
||||
.map(|(c1, yi)| c1 * yi)
|
||||
.sum();
|
||||
|
||||
// h ^ x + c2[0] ^ y[0] + ... c2[m] ^ y[m] + h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n])
|
||||
let sig_2 = blind_sign_request
|
||||
.private_attributes_ciphertexts
|
||||
.iter()
|
||||
.map(|ciphertext| ciphertext.c2())
|
||||
.zip(signing_secret_key.ys.iter())
|
||||
.map(|(c2, yi)| c2 * yi)
|
||||
.chain(std::iter::once(h * signing_secret_key.x))
|
||||
.chain(std::iter::once(signed_public))
|
||||
.sum();
|
||||
|
||||
Ok(BlindedSignature(h, elgamal::Ciphertext(sig_1, sig_2)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn sign(
|
||||
params: &mut Parameters,
|
||||
secret_key: &SecretKey,
|
||||
public_attributes: &[Attribute],
|
||||
) -> Result<Signature> {
|
||||
if public_attributes.len() > secret_key.ys.len() {
|
||||
return Err(CoconutError::IssuanceMaxAttributes {
|
||||
max: secret_key.ys.len(),
|
||||
requested: public_attributes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: why in the python implementation this hash onto the curve is present
|
||||
// while it's not used in the paper? the paper uses random exponent instead.
|
||||
// (the python implementation hashes string representation of all attributes onto the curve,
|
||||
// but I think the same can be achieved by just summing the attributes thus avoiding the unnecessary
|
||||
// transformation. If I'm wrong, please correct me.)
|
||||
let attributes_sum = public_attributes.iter().sum::<Scalar>();
|
||||
let h = hash_g1((params.gen1() * attributes_sum).to_bytes());
|
||||
|
||||
// x + m0 * y0 + m1 * y1 + ... mn * yn
|
||||
let exponent = secret_key.x
|
||||
+ public_attributes
|
||||
.iter()
|
||||
.zip(secret_key.ys.iter())
|
||||
.map(|(m_i, y_i)| m_i * y_i)
|
||||
.sum::<Scalar>();
|
||||
|
||||
let sig2 = h * exponent;
|
||||
Ok(Signature(h, sig2))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn blind_sign_request_bytes_roundtrip() {
|
||||
let mut params = Parameters::new(1).unwrap();
|
||||
let public_attributes = params.n_random_scalars(0);
|
||||
let private_attributes = params.n_random_scalars(1);
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(¶ms);
|
||||
|
||||
let lambda = prepare_blind_sign(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bytes = lambda.to_bytes();
|
||||
println!("{:?}", bytes.len());
|
||||
assert_eq!(
|
||||
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
|
||||
lambda
|
||||
);
|
||||
|
||||
let mut params = Parameters::new(4).unwrap();
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
let lambda = prepare_blind_sign(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bytes = lambda.to_bytes();
|
||||
assert_eq!(
|
||||
BlindSignRequest::try_from(bytes.as_slice()).unwrap(),
|
||||
lambda
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use core::borrow::Borrow;
|
||||
use core::iter::Sum;
|
||||
use core::ops::{Add, Mul};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G2Projective, Scalar};
|
||||
use group::Curve;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::aggregation::aggregate_verification_keys;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::SignerIndex;
|
||||
use crate::traits::Bytable;
|
||||
use crate::utils::{
|
||||
try_deserialize_g2_projective, try_deserialize_scalar, try_deserialize_scalar_vec, Polynomial,
|
||||
};
|
||||
use crate::Base58;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct SecretKey {
|
||||
pub(crate) x: Scalar,
|
||||
pub(crate) ys: Vec<Scalar>,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for SecretKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<SecretKey> {
|
||||
if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len() - 8,
|
||||
target: 32 * 2 + 8,
|
||||
modulus: 32,
|
||||
object: "secret key".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// this conversion will not fail as we are taking the same length of data
|
||||
let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap();
|
||||
let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap());
|
||||
let actual_ys_len = (bytes.len() - 40) / 32;
|
||||
|
||||
if ys_len as usize != actual_ys_len {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"Tried to deserialize secret key with inconsistent ys len (expected {}, got {})",
|
||||
ys_len, actual_ys_len
|
||||
)));
|
||||
}
|
||||
|
||||
let x = try_deserialize_scalar(
|
||||
&x_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize secret key scalar".to_string()),
|
||||
)?;
|
||||
let ys = try_deserialize_scalar_vec(
|
||||
ys_len,
|
||||
&bytes[40..],
|
||||
CoconutError::Deserialization("Failed to deserialize secret key scalars".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(SecretKey { x, ys })
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretKey {
|
||||
/// Derive verification key using this secret key.
|
||||
pub fn verification_key(&self, params: &Parameters) -> VerificationKey {
|
||||
let g2 = params.gen2();
|
||||
VerificationKey {
|
||||
alpha: g2 * self.x,
|
||||
beta: self.ys.iter().map(|y| g2 * y).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// x || ys.len() || ys
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let ys_len = self.ys.len() as u64;
|
||||
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) as usize * 32);
|
||||
|
||||
bytes.extend_from_slice(&self.x.to_bytes());
|
||||
bytes.extend_from_slice(&ys_len.to_le_bytes());
|
||||
for y in self.ys.iter() {
|
||||
bytes.extend_from_slice(&y.to_bytes())
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<SecretKey> {
|
||||
SecretKey::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for SecretKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
SecretKey::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for SecretKey {}
|
||||
|
||||
// TODO: perhaps change points to affine representation
|
||||
// to make verification slightly more efficient?
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct VerificationKey {
|
||||
// TODO add gen2 as per the paper or imply it from the fact library is using bls381?
|
||||
pub(crate) alpha: G2Projective,
|
||||
pub(crate) beta: Vec<G2Projective>,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for VerificationKey {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<VerificationKey> {
|
||||
if bytes.len() < 96 * 2 + 8 || (bytes.len() - 8) % 96 != 0 {
|
||||
return Err(CoconutError::DeserializationInvalidLength {
|
||||
actual: bytes.len(),
|
||||
modulus_target: bytes.len() - 8,
|
||||
target: 96 * 2 + 8,
|
||||
modulus: 96,
|
||||
object: "secret key".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// this conversion will not fail as we are taking the same length of data
|
||||
let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap();
|
||||
let beta_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap());
|
||||
let actual_beta_len = (bytes.len() - 104) / 96;
|
||||
|
||||
if beta_len as usize != actual_beta_len {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
format!("Tried to deserialize verification key with inconsistent beta len (expected {}, got {})",
|
||||
beta_len, actual_beta_len
|
||||
)));
|
||||
}
|
||||
|
||||
let alpha = try_deserialize_g2_projective(
|
||||
&alpha_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize verification key G2 point (alpha)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut beta = Vec::with_capacity(actual_beta_len);
|
||||
for i in 0..actual_beta_len {
|
||||
let start = 104 + i * 96;
|
||||
let end = start + 96;
|
||||
let beta_i_bytes = bytes[start..end].try_into().unwrap();
|
||||
let beta_i = try_deserialize_g2_projective(
|
||||
&beta_i_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"Failed to deserialize verification key G2 point (beta)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
beta.push(beta_i)
|
||||
}
|
||||
|
||||
Ok(VerificationKey { alpha, beta })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> Add<&'b VerificationKey> for VerificationKey {
|
||||
type Output = VerificationKey;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: &'b VerificationKey) -> VerificationKey {
|
||||
// If you're trying to add two keys together that were created
|
||||
// for different number of attributes, just panic as it's a
|
||||
// nonsense operation.
|
||||
assert_eq!(
|
||||
self.beta.len(),
|
||||
rhs.beta.len(),
|
||||
"trying to add verification keys generated for different number of attributes"
|
||||
);
|
||||
|
||||
VerificationKey {
|
||||
alpha: self.alpha + rhs.alpha,
|
||||
beta: self
|
||||
.beta
|
||||
.iter()
|
||||
.zip(rhs.beta.iter())
|
||||
.map(|(self_beta, rhs_beta)| self_beta + rhs_beta)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Mul<Scalar> for &'a VerificationKey {
|
||||
type Output = VerificationKey;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, rhs: Scalar) -> Self::Output {
|
||||
VerificationKey {
|
||||
alpha: self.alpha * rhs,
|
||||
beta: self.beta.iter().map(|b_i| b_i * rhs).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Sum<T> for VerificationKey
|
||||
where
|
||||
T: Borrow<VerificationKey>,
|
||||
{
|
||||
#[inline]
|
||||
fn sum<I>(iter: I) -> Self
|
||||
where
|
||||
I: Iterator<Item = T>,
|
||||
{
|
||||
let mut peekable = iter.peekable();
|
||||
let head_attributes = match peekable.peek() {
|
||||
Some(head) => head.borrow().beta.len(),
|
||||
None => {
|
||||
// TODO: this is a really weird edge case. You're trying to sum an EMPTY iterator
|
||||
// of VerificationKey. So should it panic here or just return some nonsense value?
|
||||
return VerificationKey::identity(0);
|
||||
}
|
||||
};
|
||||
|
||||
peekable.fold(VerificationKey::identity(head_attributes), |acc, item| {
|
||||
acc + item.borrow()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VerificationKey {
|
||||
/// Create a (kinda) identity verification key using specified
|
||||
/// number of 'beta' elements
|
||||
pub(crate) fn identity(beta_size: usize) -> Self {
|
||||
VerificationKey {
|
||||
alpha: G2Projective::identity(),
|
||||
beta: vec![G2Projective::identity(); beta_size],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result<Self> {
|
||||
aggregate_verification_keys(sigs, indices)
|
||||
}
|
||||
|
||||
pub fn alpha(&self) -> &G2Projective {
|
||||
&self.alpha
|
||||
}
|
||||
|
||||
pub fn beta(&self) -> &Vec<G2Projective> {
|
||||
&self.beta
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let beta_len = self.beta.len() as u64;
|
||||
let mut bytes = Vec::with_capacity(8 + (beta_len + 1) as usize * 96);
|
||||
|
||||
bytes.extend_from_slice(&self.alpha.to_affine().to_compressed());
|
||||
bytes.extend_from_slice(&beta_len.to_le_bytes());
|
||||
for beta in self.beta.iter() {
|
||||
bytes.extend_from_slice(&beta.to_affine().to_compressed())
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<VerificationKey> {
|
||||
VerificationKey::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for VerificationKey {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
VerificationKey::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for VerificationKey {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct KeyPair {
|
||||
secret_key: SecretKey,
|
||||
verification_key: VerificationKey,
|
||||
|
||||
/// Optional index value specifying polynomial point used during threshold key generation.
|
||||
pub index: Option<SignerIndex>,
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
const MARKER_BYTES: &'static [u8] = b"coconutkeypair";
|
||||
|
||||
pub fn secret_key(&self) -> SecretKey {
|
||||
self.secret_key.clone()
|
||||
}
|
||||
|
||||
pub fn verification_key(&self) -> VerificationKey {
|
||||
self.verification_key.clone()
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
|
||||
self.to_byte_vec()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
KeyPair::try_from_byte_slice(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for KeyPair {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
// Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional
|
||||
let mut byts = vec![];
|
||||
let secret_key_bytes = self.secret_key.to_bytes();
|
||||
let secret_key_len = (secret_key_bytes.len() as u64).to_le_bytes();
|
||||
let verification_key_bytes = self.verification_key.to_bytes();
|
||||
let verification_key_len = (verification_key_bytes.len() as u64).to_le_bytes();
|
||||
byts.extend_from_slice(Self::MARKER_BYTES);
|
||||
byts.extend_from_slice(&secret_key_len);
|
||||
byts.extend_from_slice(&secret_key_bytes);
|
||||
byts.extend_from_slice(&verification_key_len);
|
||||
byts.extend_from_slice(&verification_key_bytes);
|
||||
if let Some(index) = self.index {
|
||||
byts.extend_from_slice(&index.to_le_bytes())
|
||||
}
|
||||
byts
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
KeyPair::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for KeyPair {}
|
||||
|
||||
impl TryFrom<&[u8]> for KeyPair {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<KeyPair> {
|
||||
let header_len = Self::MARKER_BYTES.len();
|
||||
|
||||
// we must be able to at the very least read the length of secret key which is past the header
|
||||
// and is 8 bytes long
|
||||
if bytes.len() < header_len + 8 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: header_len + 8,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let secret_key_len =
|
||||
u64::from_le_bytes(bytes[header_len..header_len + 8].try_into().unwrap()) as usize;
|
||||
let secret_key_start = header_len + 8;
|
||||
|
||||
let secret_key =
|
||||
SecretKey::try_from(&bytes[secret_key_start..secret_key_start + secret_key_len])?;
|
||||
|
||||
// we must be able to read the length of verification key
|
||||
if bytes.len() < secret_key_start + secret_key_len + 8 {
|
||||
return Err(CoconutError::DeserializationMinLength {
|
||||
min: secret_key_start + secret_key_len + 8,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let verification_key_len = u64::from_le_bytes(
|
||||
bytes[secret_key_start + secret_key_len..secret_key_start + secret_key_len + 8]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
) as usize;
|
||||
let verification_key_start = secret_key_start + secret_key_len + 8;
|
||||
|
||||
let verification_key = VerificationKey::try_from(
|
||||
&bytes[verification_key_start..verification_key_start + verification_key_len],
|
||||
)?;
|
||||
let consumed_bytes = verification_key_start + verification_key_len;
|
||||
let index = if consumed_bytes < bytes.len() && [consumed_bytes..].len() == 8 {
|
||||
Some(u64::from_le_bytes(
|
||||
bytes[consumed_bytes..consumed_bytes + 8]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(KeyPair {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)).
|
||||
/// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys
|
||||
/// that are independent of each other.
|
||||
#[cfg(test)]
|
||||
pub fn keygen(params: &Parameters) -> KeyPair {
|
||||
let attributes = params.gen_hs().len();
|
||||
|
||||
let x = params.random_scalar();
|
||||
let ys = params.n_random_scalars(attributes);
|
||||
|
||||
let secret_key = SecretKey { x, ys };
|
||||
let verification_key = secret_key.verification_key(params);
|
||||
|
||||
KeyPair {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a set of n Coconut keypairs [((x, y0, y1...), (g2^x, g2^y0, ...)), ...],
|
||||
/// such that they support threshold aggregation by `threshold` number of parties.
|
||||
/// It is expected that this procedure is executed by a Trusted Third Party.
|
||||
pub fn ttp_keygen(
|
||||
params: &Parameters,
|
||||
threshold: u64,
|
||||
num_authorities: u64,
|
||||
) -> Result<Vec<KeyPair>> {
|
||||
if threshold == 0 {
|
||||
return Err(CoconutError::Setup(
|
||||
"Tried to generate threshold keys with a 0 threshold value".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if threshold > num_authorities {
|
||||
return Err(
|
||||
CoconutError::Setup(
|
||||
"Tried to generate threshold keys for threshold value being higher than number of the signing authorities".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let attributes = params.gen_hs().len();
|
||||
|
||||
// generate polynomials
|
||||
let v = Polynomial::new_random(params, threshold - 1);
|
||||
let ws = (0..attributes)
|
||||
.map(|_| Polynomial::new_random(params, threshold - 1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// TODO: potentially if we had some known authority identifier we could use that instead
|
||||
// of the increasing (1,2,3,...) sequence
|
||||
let polynomial_indices = (1..=num_authorities).collect::<Vec<_>>();
|
||||
|
||||
// generate polynomial shares
|
||||
let x = polynomial_indices
|
||||
.iter()
|
||||
.map(|&id| v.evaluate(&Scalar::from(id)));
|
||||
let ys = polynomial_indices.iter().map(|&id| {
|
||||
ws.iter()
|
||||
.map(|w| w.evaluate(&Scalar::from(id)))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
// finally set the keys
|
||||
let secret_keys = x.zip(ys).map(|(x, ys)| SecretKey { x, ys });
|
||||
|
||||
let keypairs = secret_keys
|
||||
.zip(polynomial_indices.iter())
|
||||
.map(|(secret_key, index)| {
|
||||
let verification_key = secret_key.verification_key(params);
|
||||
KeyPair {
|
||||
secret_key,
|
||||
verification_key,
|
||||
index: Some(*index),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(keypairs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::scheme::setup::setup;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn keypair_bytes_roundtrip() {
|
||||
let mut params1 = setup(1).unwrap();
|
||||
let mut params5 = setup(5).unwrap();
|
||||
|
||||
let keypair1 = keygen(&mut params1);
|
||||
let keypair5 = keygen(&mut params5);
|
||||
|
||||
let bytes1 = keypair1.to_bytes();
|
||||
let bytes5 = keypair5.to_bytes();
|
||||
|
||||
assert_eq!(KeyPair::from_bytes(&bytes1).unwrap(), keypair1);
|
||||
assert_eq!(KeyPair::from_bytes(&bytes5).unwrap(), keypair5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_key_bytes_roundtrip() {
|
||||
let mut params1 = setup(1).unwrap();
|
||||
let mut params5 = setup(5).unwrap();
|
||||
|
||||
let keypair1 = keygen(&mut params1);
|
||||
let keypair5 = keygen(&mut params5);
|
||||
|
||||
let bytes1 = keypair1.secret_key.to_bytes();
|
||||
let bytes5 = keypair5.secret_key.to_bytes();
|
||||
|
||||
assert_eq!(SecretKey::from_bytes(&bytes1).unwrap(), keypair1.secret_key);
|
||||
assert_eq!(SecretKey::from_bytes(&bytes5).unwrap(), keypair5.secret_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_key_bytes_roundtrip() {
|
||||
let mut params1 = setup(1).unwrap();
|
||||
let mut params5 = setup(5).unwrap();
|
||||
|
||||
let keypair1 = &keygen(&mut params1);
|
||||
let keypair5 = &keygen(&mut params5);
|
||||
|
||||
let bytes1: Vec<u8> = keypair1.verification_key.to_bytes();
|
||||
let bytes5: Vec<u8> = keypair5.verification_key.to_bytes();
|
||||
|
||||
assert_eq!(
|
||||
VerificationKey::try_from(bytes1.as_slice()).unwrap(),
|
||||
keypair1.verification_key
|
||||
);
|
||||
assert_eq!(
|
||||
VerificationKey::try_from(bytes5.as_slice()).unwrap(),
|
||||
keypair5.verification_key
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// TODO: implement https://crates.io/crates/signature traits?
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar};
|
||||
use group::Curve;
|
||||
|
||||
pub use keygen::{SecretKey, VerificationKey};
|
||||
|
||||
use crate::elgamal::Ciphertext;
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::verification::check_bilinear_pairing;
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::try_deserialize_g1_projective;
|
||||
use crate::{elgamal, Attribute};
|
||||
|
||||
pub mod aggregation;
|
||||
pub mod issuance;
|
||||
pub mod keygen;
|
||||
pub mod setup;
|
||||
pub mod verification;
|
||||
|
||||
pub type SignerIndex = u64;
|
||||
|
||||
// (h, s)
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct Signature(pub(crate) G1Projective, pub(crate) G1Projective);
|
||||
|
||||
pub type PartialSignature = Signature;
|
||||
|
||||
impl TryFrom<&[u8]> for Signature {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Signature> {
|
||||
if bytes.len() != 96 {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"Signature must be exactly 96 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let sig1_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
|
||||
let sig2_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48");
|
||||
|
||||
let sig1 = try_deserialize_g1_projective(
|
||||
sig1_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed sig1".to_string()),
|
||||
)?;
|
||||
|
||||
let sig2 = try_deserialize_g1_projective(
|
||||
sig2_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed sig2".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(Signature(sig1, sig2))
|
||||
}
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
pub(crate) fn sig1(&self) -> &G1Projective {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub(crate) fn sig2(&self) -> &G1Projective {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn randomise(&self, params: &Parameters) -> (Signature, Scalar) {
|
||||
let r = params.random_scalar();
|
||||
let r_prime = params.random_scalar();
|
||||
let h_prime = self.0 * r_prime;
|
||||
let s_prime = (self.1 * r_prime) + (h_prime * r);
|
||||
(Signature(h_prime, s_prime), r)
|
||||
}
|
||||
|
||||
pub fn to_bytes(self) -> [u8; 96] {
|
||||
let mut bytes = [0u8; 96];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Signature> {
|
||||
Signature::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for Signature {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Signature::from_bytes(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Signature {}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct BlindedSignature(G1Projective, elgamal::Ciphertext);
|
||||
|
||||
impl Bytable for BlindedSignature {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Self::from_bytes(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for BlindedSignature {}
|
||||
|
||||
impl TryFrom<&[u8]> for BlindedSignature {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<BlindedSignature> {
|
||||
if bytes.len() != 144 {
|
||||
return Err(CoconutError::Deserialization(format!(
|
||||
"BlindedSignature must be exactly 144 bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
|
||||
|
||||
let h = try_deserialize_g1_projective(
|
||||
h_bytes,
|
||||
CoconutError::Deserialization("Failed to deserialize compressed h".to_string()),
|
||||
)?;
|
||||
let c_tilde = Ciphertext::try_from(&bytes[48..])?;
|
||||
|
||||
Ok(BlindedSignature(h, c_tilde))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlindedSignature {
|
||||
pub fn unblind(
|
||||
&self,
|
||||
params: &Parameters,
|
||||
private_key: &elgamal::PrivateKey,
|
||||
partial_verification_key: &VerificationKey,
|
||||
private_attributes: &[Attribute],
|
||||
public_attributes: &[Attribute],
|
||||
commitment_hash: &G1Projective,
|
||||
) -> Result<Signature> {
|
||||
// parse the signature
|
||||
let h = &self.0;
|
||||
let c = &self.1;
|
||||
let sig2 = private_key.decrypt(c);
|
||||
|
||||
// Verify the commitment hash
|
||||
if !(commitment_hash == h) {
|
||||
return Err(CoconutError::Unblind(
|
||||
"Verification of commitment hash from signature failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let alpha = partial_verification_key.alpha;
|
||||
|
||||
let tmp = private_attributes
|
||||
.iter()
|
||||
.chain(public_attributes.iter())
|
||||
.zip(partial_verification_key.beta.iter())
|
||||
.map(|(attr, beta_i)| beta_i * attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
// Verify the signature share
|
||||
if !check_bilinear_pairing(
|
||||
&h.to_affine(),
|
||||
&G2Prepared::from((alpha + tmp).to_affine()),
|
||||
&sig2.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) {
|
||||
return Err(CoconutError::Unblind(
|
||||
"Verification of signature share failed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Signature(self.0, sig2))
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> [u8; 144] {
|
||||
let mut bytes = [0u8; 144];
|
||||
bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed());
|
||||
bytes[48..].copy_from_slice(&self.1.to_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<BlindedSignature> {
|
||||
BlindedSignature::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// perhaps this should take signature by reference? we'll see how it goes
|
||||
pub struct SignatureShare {
|
||||
signature: Signature,
|
||||
index: SignerIndex,
|
||||
}
|
||||
|
||||
impl SignatureShare {
|
||||
pub fn new(signature: Signature, index: SignerIndex) -> Self {
|
||||
SignatureShare { signature, index }
|
||||
}
|
||||
|
||||
pub fn signature(&self) -> &Signature {
|
||||
&self.signature
|
||||
}
|
||||
|
||||
pub fn index(&self) -> SignerIndex {
|
||||
self.index
|
||||
}
|
||||
|
||||
// pub fn aggregate(shares: &[Self]) -> Result<Signature> {
|
||||
// aggregate_signature_shares(shares)
|
||||
// }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use group::GroupEncoding;
|
||||
|
||||
use crate::hash_to_scalar;
|
||||
use crate::scheme::aggregation::{aggregate_signatures, aggregate_verification_keys};
|
||||
use crate::scheme::issuance::{blind_sign, prepare_blind_sign, sign};
|
||||
use crate::scheme::keygen::{keygen, ttp_keygen};
|
||||
use crate::scheme::verification::{prove_bandwidth_credential, verify, verify_credential};
|
||||
use crate::utils::hash_g1;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unblind_returns_error_if_integrity_check_on_commitment_hash_fails() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let private_attributes = params.n_random_scalars(2 as usize);
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
|
||||
|
||||
let lambda =
|
||||
prepare_blind_sign(&mut params, &elgamal_keypair, &private_attributes, &[]).unwrap();
|
||||
|
||||
let keypair1 = keygen(&mut params);
|
||||
|
||||
let sig1 = blind_sign(
|
||||
&mut params,
|
||||
&keypair1.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let wrong_commitment_opening = params.random_scalar();
|
||||
let wrong_commitment = params.gen1() * wrong_commitment_opening;
|
||||
let fake_commitment_hash = hash_g1(wrong_commitment.to_bytes());
|
||||
assert!(sig1
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&fake_commitment_hash,
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unblind_returns_error_if_signature_verification_fails() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let private_attributes = vec![hash_to_scalar("Attribute1"), hash_to_scalar("Attribute2")];
|
||||
let private_attributes2 = vec![hash_to_scalar("Attribute3"), hash_to_scalar("Attribute4")];
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
|
||||
|
||||
let lambda =
|
||||
prepare_blind_sign(&mut params, &elgamal_keypair, &private_attributes, &[]).unwrap();
|
||||
|
||||
let keypair1 = keygen(&mut params);
|
||||
|
||||
let sig1 = blind_sign(
|
||||
&mut params,
|
||||
&keypair1.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(sig1
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair1.verification_key(),
|
||||
&private_attributes2,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_private_attributes() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
|
||||
|
||||
let keypair1 = keygen(&mut params);
|
||||
let keypair2 = keygen(&mut params);
|
||||
|
||||
let lambda =
|
||||
prepare_blind_sign(&mut params, &elgamal_keypair, &private_attributes, &[]).unwrap();
|
||||
|
||||
let sig1 = blind_sign(
|
||||
&mut params,
|
||||
&keypair1.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&[],
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig2 = blind_sign(
|
||||
&mut params,
|
||||
&keypair2.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&[],
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair2.verification_key(),
|
||||
&private_attributes,
|
||||
&[],
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta1 = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair1.verification_key(),
|
||||
&sig1,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta2 = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair2.verification_key(),
|
||||
&sig2,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&theta1,
|
||||
&[],
|
||||
));
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
&theta2,
|
||||
&[],
|
||||
));
|
||||
|
||||
assert!(!verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&theta2,
|
||||
&[],
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_attributes() {
|
||||
let mut params = Parameters::new(2).unwrap();
|
||||
let attributes = params.n_random_scalars(2);
|
||||
|
||||
let keypair1 = keygen(&mut params);
|
||||
let keypair2 = keygen(&mut params);
|
||||
let sig1 = sign(&mut params, &keypair1.secret_key(), &attributes).unwrap();
|
||||
let sig2 = sign(&mut params, &keypair2.secret_key(), &attributes).unwrap();
|
||||
|
||||
assert!(verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&attributes,
|
||||
&sig1,
|
||||
));
|
||||
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
&attributes,
|
||||
&sig1,
|
||||
));
|
||||
|
||||
assert!(!verify(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&attributes,
|
||||
&sig2,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_and_two_private_attributes() {
|
||||
let mut params = Parameters::new(4).unwrap();
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(&mut params);
|
||||
|
||||
let keypair1 = keygen(&mut params);
|
||||
let keypair2 = keygen(&mut params);
|
||||
|
||||
let lambda = prepare_blind_sign(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig1 = blind_sign(
|
||||
&mut params,
|
||||
&keypair1.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair1.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sig2 = blind_sign(
|
||||
&mut params,
|
||||
&keypair2.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair2.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta1 = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair1.verification_key(),
|
||||
&sig1,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let theta2 = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair2.verification_key(),
|
||||
&sig2,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&theta1,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&keypair2.verification_key(),
|
||||
&theta2,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
assert!(!verify_credential(
|
||||
¶ms,
|
||||
&keypair1.verification_key(),
|
||||
&theta2,
|
||||
&public_attributes,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_on_two_public_and_two_private_attributes_from_two_signers() {
|
||||
let mut params = Parameters::new(4).unwrap();
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let elgamal_keypair = elgamal::elgamal_keygen(¶ms);
|
||||
|
||||
let keypairs = ttp_keygen(&mut params, 2, 3).unwrap();
|
||||
|
||||
let lambda = prepare_blind_sign(
|
||||
&mut params,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sigs = keypairs
|
||||
.iter()
|
||||
.map(|keypair| {
|
||||
blind_sign(
|
||||
&mut params,
|
||||
&keypair.secret_key(),
|
||||
elgamal_keypair.public_key(),
|
||||
&lambda,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap()
|
||||
.unblind(
|
||||
¶ms,
|
||||
elgamal_keypair.private_key(),
|
||||
&keypair.verification_key(),
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&lambda.get_commitment_hash(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vks = keypairs
|
||||
.into_iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
attributes.extend_from_slice(&private_attributes);
|
||||
attributes.extend_from_slice(&public_attributes);
|
||||
|
||||
let aggr_vk = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap();
|
||||
let aggr_sig =
|
||||
aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[..2], Some(&[1, 2]))
|
||||
.unwrap();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&aggr_vk,
|
||||
&aggr_sig,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
// taking different subset of keys and credentials
|
||||
let aggr_vk = aggregate_verification_keys(&vks[1..], Some(&[2, 3])).unwrap();
|
||||
let aggr_sig =
|
||||
aggregate_signatures(¶ms, &aggr_vk, &attributes, &sigs[1..], Some(&[2, 3]))
|
||||
.unwrap();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&aggr_vk,
|
||||
&aggr_sig,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&aggr_vk,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
let signature = Signature(params.gen1() * r, params.gen1() * s);
|
||||
let bytes = signature.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
|
||||
let expected_bytes = [
|
||||
signature.0.to_affine().to_compressed(),
|
||||
signature.1.to_affine().to_compressed(),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(expected_bytes, bytes);
|
||||
assert_eq!(signature, Signature::try_from(&bytes[..]).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blinded_signature_bytes_roundtrip() {
|
||||
let params = Parameters::default();
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
let t = params.random_scalar();
|
||||
let blinded_sig = BlindedSignature(
|
||||
params.gen1() * t,
|
||||
Ciphertext(params.gen1() * r, params.gen1() * s),
|
||||
);
|
||||
let bytes = blinded_sig.to_bytes();
|
||||
|
||||
// also make sure it is equivalent to the internal g1 compressed bytes concatenated
|
||||
let expected_bytes = [
|
||||
blinded_sig.0.to_affine().to_compressed(),
|
||||
blinded_sig.1 .0.to_affine().to_compressed(),
|
||||
blinded_sig.1 .1.to_affine().to_compressed(),
|
||||
]
|
||||
.concat();
|
||||
assert_eq!(expected_bytes, bytes);
|
||||
assert_eq!(blinded_sig, BlindedSignature::try_from(&bytes[..]).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use bls12_381::{G1Affine, G2Affine, G2Prepared, Scalar};
|
||||
use ff::Field;
|
||||
use group::Curve;
|
||||
use rand::thread_rng;
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::utils::hash_g1;
|
||||
|
||||
/// System-wide parameters used for the protocol
|
||||
pub struct Parameters {
|
||||
/// Generator of the G1 group
|
||||
g1: G1Affine,
|
||||
|
||||
/// Additional generators of the G1 group
|
||||
hs: Vec<G1Affine>,
|
||||
|
||||
/// Generator of the G2 group
|
||||
g2: G2Affine,
|
||||
|
||||
/// Precomputed G2 generator used for the miller loop
|
||||
_g2_prepared_miller: G2Prepared,
|
||||
}
|
||||
|
||||
impl Parameters {
|
||||
pub fn new(num_attributes: u32) -> Result<Parameters> {
|
||||
if num_attributes == 0 {
|
||||
return Err(CoconutError::Setup(
|
||||
"Tried to setup the scheme for 0 attributes".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let hs = (1..=num_attributes)
|
||||
.map(|i| hash_g1(format!("h{}", i)).to_affine())
|
||||
.collect();
|
||||
|
||||
Ok(Parameters {
|
||||
g1: G1Affine::generator(),
|
||||
hs,
|
||||
g2: G2Affine::generator(),
|
||||
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn gen1(&self) -> &G1Affine {
|
||||
&self.g1
|
||||
}
|
||||
|
||||
pub(crate) fn gen2(&self) -> &G2Affine {
|
||||
&self.g2
|
||||
}
|
||||
|
||||
pub(crate) fn prepared_miller_g2(&self) -> &G2Prepared {
|
||||
&self._g2_prepared_miller
|
||||
}
|
||||
|
||||
pub(crate) fn gen_hs(&self) -> &[G1Affine] {
|
||||
&self.hs
|
||||
}
|
||||
|
||||
pub fn random_scalar(&self) -> Scalar {
|
||||
// lazily-initialized thread-local random number generator, seeded by the system
|
||||
let mut rng = thread_rng();
|
||||
Scalar::random(&mut rng)
|
||||
}
|
||||
|
||||
pub fn n_random_scalars(&self, n: usize) -> Vec<Scalar> {
|
||||
(0..n).map(|_| self.random_scalar()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(num_attributes: u32) -> Result<Parameters> {
|
||||
Parameters::new(num_attributes)
|
||||
}
|
||||
|
||||
// for ease of use in tests requiring params
|
||||
// TODO: not sure if this will have to go away when tests require some specific number of generators
|
||||
#[cfg(test)]
|
||||
impl Default for Parameters {
|
||||
fn default() -> Self {
|
||||
Parameters {
|
||||
g1: G1Affine::generator(),
|
||||
hs: Vec::new(),
|
||||
g2: G2Affine::generator(),
|
||||
_g2_prepared_miller: G2Prepared::from(G2Affine::generator()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use core::ops::Neg;
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::{multi_miller_loop, G1Affine, G2Prepared, G2Projective, Scalar};
|
||||
use group::{Curve, Group};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::proofs::ProofKappaZeta;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::Signature;
|
||||
use crate::scheme::VerificationKey;
|
||||
use crate::traits::{Base58, Bytable};
|
||||
use crate::utils::try_deserialize_g2_projective;
|
||||
use crate::Attribute;
|
||||
|
||||
// TODO NAMING: this whole thing
|
||||
// Theta
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct Theta {
|
||||
// blinded_message (kappa)
|
||||
pub blinded_message: G2Projective,
|
||||
// blinded serial number (zeta)
|
||||
pub blinded_serial_number: G2Projective,
|
||||
// sigma
|
||||
pub credential: Signature,
|
||||
// pi_v
|
||||
pub pi_v: ProofKappaZeta,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Theta {
|
||||
type Error = CoconutError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Theta> {
|
||||
if bytes.len() < 288 {
|
||||
return Err(
|
||||
CoconutError::Deserialization(
|
||||
format!("Tried to deserialize theta with insufficient number of bytes, expected >= 288, got {}", bytes.len()),
|
||||
));
|
||||
}
|
||||
|
||||
let blinded_message_bytes = bytes[..96].try_into().unwrap();
|
||||
let blinded_message = try_deserialize_g2_projective(
|
||||
&blinded_message_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"failed to deserialize the blinded message (kappa)".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
let blinded_serial_number_bytes = bytes[96..192].try_into().unwrap();
|
||||
let blinded_serial_number = try_deserialize_g2_projective(
|
||||
&blinded_serial_number_bytes,
|
||||
CoconutError::Deserialization(
|
||||
"failed to deserialize the blinded serial number (zeta)".to_string(),
|
||||
),
|
||||
)?;
|
||||
let credential = Signature::try_from(&bytes[192..288])?;
|
||||
|
||||
let pi_v = ProofKappaZeta::from_bytes(&bytes[288..])?;
|
||||
|
||||
Ok(Theta {
|
||||
blinded_message,
|
||||
blinded_serial_number,
|
||||
credential,
|
||||
pi_v,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Theta {
|
||||
fn verify_proof(&self, params: &Parameters, verification_key: &VerificationKey) -> bool {
|
||||
self.pi_v.verify(
|
||||
params,
|
||||
verification_key,
|
||||
&self.blinded_message,
|
||||
&self.blinded_serial_number,
|
||||
)
|
||||
}
|
||||
|
||||
// blinded message (kappa) || blinded serial number (zeta) || credential || pi_v
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let blinded_message_bytes = self.blinded_message.to_affine().to_compressed();
|
||||
let blinded_serial_number_bytes = self.blinded_serial_number.to_affine().to_compressed();
|
||||
let credential_bytes = self.credential.to_bytes();
|
||||
let proof_bytes = self.pi_v.to_bytes();
|
||||
|
||||
let mut bytes = Vec::with_capacity(288 + proof_bytes.len());
|
||||
bytes.extend_from_slice(&blinded_message_bytes);
|
||||
bytes.extend_from_slice(&blinded_serial_number_bytes);
|
||||
bytes.extend_from_slice(&credential_bytes);
|
||||
bytes.extend_from_slice(&proof_bytes);
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Theta> {
|
||||
Theta::try_from(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytable for Theta {
|
||||
fn to_byte_vec(&self) -> Vec<u8> {
|
||||
self.to_bytes()
|
||||
}
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self> {
|
||||
Theta::try_from(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Base58 for Theta {}
|
||||
|
||||
pub fn compute_kappa(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
private_attributes: &[Attribute],
|
||||
blinding_factor: Scalar,
|
||||
) -> G2Projective {
|
||||
params.gen2() * blinding_factor
|
||||
+ verification_key.alpha
|
||||
+ private_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
|
||||
.sum::<G2Projective>()
|
||||
}
|
||||
|
||||
pub fn compute_zeta(params: &Parameters, serial_number: Attribute) -> G2Projective {
|
||||
params.gen2() * serial_number
|
||||
}
|
||||
|
||||
pub fn prove_bandwidth_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
signature: &Signature,
|
||||
serial_number: Attribute,
|
||||
binding_number: Attribute,
|
||||
) -> Result<Theta> {
|
||||
if verification_key.beta.len() < 2 {
|
||||
return Err(
|
||||
CoconutError::Verification(
|
||||
format!("Tried to prove a credential for higher than supported by the provided verification key number of attributes (max: {}, requested: 2)",
|
||||
verification_key.beta.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// Randomize the signature
|
||||
let (signature_prime, sign_blinding_factor) = signature.randomise(params);
|
||||
|
||||
// blinded_message : kappa in the paper.
|
||||
// Value kappa is needed since we want to show a signature sigma'.
|
||||
// In order to verify sigma' we need both the verification key vk and the message m.
|
||||
// However, we do not want to reveal m to whomever we are showing the signature.
|
||||
// Thus, we need kappa which allows us to verify sigma'. In particular,
|
||||
// kappa is computed on m as input, but thanks to the use or random value r,
|
||||
// it does not reveal any information about m.
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
let blinded_message = compute_kappa(
|
||||
params,
|
||||
verification_key,
|
||||
&private_attributes,
|
||||
sign_blinding_factor,
|
||||
);
|
||||
|
||||
// zeta is a commitment to the serial number (i.e., a public value associated with the serial number)
|
||||
let blinded_serial_number = compute_zeta(params, serial_number);
|
||||
|
||||
let pi_v = ProofKappaZeta::construct(
|
||||
params,
|
||||
verification_key,
|
||||
&serial_number,
|
||||
&binding_number,
|
||||
&sign_blinding_factor,
|
||||
&blinded_message,
|
||||
&blinded_serial_number,
|
||||
);
|
||||
|
||||
Ok(Theta {
|
||||
blinded_message,
|
||||
blinded_serial_number,
|
||||
credential: signature_prime,
|
||||
pi_v,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks whether e(P, Q) * e(-R, S) == id
|
||||
pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2Prepared) -> bool {
|
||||
// checking e(P, Q) * e(-R, S) == id
|
||||
// is equivalent to checking e(P, Q) == e(R, S)
|
||||
// but requires only a single final exponentiation rather than two of them
|
||||
// and therefore, as seen via benchmarks.rs, is almost 50% faster
|
||||
// (1.47ms vs 2.45ms, tested on R9 5900X)
|
||||
|
||||
let multi_miller = multi_miller_loop(&[(p, q), (&r.neg(), s)]);
|
||||
multi_miller.final_exponentiation().is_identity().into()
|
||||
}
|
||||
|
||||
pub fn verify_credential(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
theta: &Theta,
|
||||
public_attributes: &[Attribute],
|
||||
) -> bool {
|
||||
if public_attributes.len() + theta.pi_v.private_attributes_len() > verification_key.beta.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !theta.verify_proof(params, verification_key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let kappa = if public_attributes.is_empty() {
|
||||
theta.blinded_message
|
||||
} else {
|
||||
let signed_public_attributes = public_attributes
|
||||
.iter()
|
||||
.zip(
|
||||
verification_key
|
||||
.beta
|
||||
.iter()
|
||||
.skip(theta.pi_v.private_attributes_len()),
|
||||
)
|
||||
.map(|(pub_attr, beta_i)| beta_i * pub_attr)
|
||||
.sum::<G2Projective>();
|
||||
|
||||
theta.blinded_message + signed_public_attributes
|
||||
};
|
||||
|
||||
check_bilinear_pairing(
|
||||
&theta.credential.0.to_affine(),
|
||||
&G2Prepared::from(kappa.to_affine()),
|
||||
&(theta.credential.1).to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) && !bool::from(theta.credential.0.is_identity())
|
||||
}
|
||||
|
||||
// Used in tests only
|
||||
#[cfg(test)]
|
||||
pub fn verify(
|
||||
params: &Parameters,
|
||||
verification_key: &VerificationKey,
|
||||
public_attributes: &[Attribute],
|
||||
sig: &Signature,
|
||||
) -> bool {
|
||||
let kappa = (verification_key.alpha
|
||||
+ public_attributes
|
||||
.iter()
|
||||
.zip(verification_key.beta.iter())
|
||||
.map(|(m_i, b_i)| b_i * m_i)
|
||||
.sum::<G2Projective>())
|
||||
.to_affine();
|
||||
|
||||
check_bilinear_pairing(
|
||||
&sig.0.to_affine(),
|
||||
&G2Prepared::from(kappa),
|
||||
&sig.1.to_affine(),
|
||||
params.prepared_miller_g2(),
|
||||
) && !bool::from(sig.0.is_identity())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::scheme::keygen::keygen;
|
||||
use crate::scheme::setup::setup;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn theta_bytes_roundtrip() {
|
||||
let mut params = setup(2).unwrap();
|
||||
|
||||
let keypair = keygen(&mut params);
|
||||
let r = params.random_scalar();
|
||||
let s = params.random_scalar();
|
||||
|
||||
let signature = Signature(params.gen1() * r, params.gen1() * s);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
&mut params,
|
||||
&keypair.verification_key(),
|
||||
&signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bytes = theta.to_bytes();
|
||||
assert_eq!(Theta::try_from(bytes.as_slice()).unwrap(), theta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use crate::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, blind_sign, elgamal_keygen,
|
||||
prepare_blind_sign, prove_bandwidth_credential, setup, ttp_keygen, verify_credential,
|
||||
CoconutError, Signature, SignatureShare, VerificationKey,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn main() -> Result<(), CoconutError> {
|
||||
let params = setup(5)?;
|
||||
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
let serial_number = params.random_scalar();
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
|
||||
let elgamal_keypair = elgamal_keygen(¶ms);
|
||||
|
||||
// generate commitment and encryption
|
||||
let blind_sign_request = prepare_blind_sign(
|
||||
¶ms,
|
||||
&elgamal_keypair,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
)?;
|
||||
|
||||
// generate_keys
|
||||
let coconut_keypairs = ttp_keygen(¶ms, 2, 3)?;
|
||||
|
||||
let verification_keys: Vec<VerificationKey> = coconut_keypairs
|
||||
.iter()
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.collect();
|
||||
|
||||
// aggregate verification keys
|
||||
let verification_key = aggregate_verification_keys(&verification_keys, Some(&[1, 2, 3]))?;
|
||||
|
||||
// generate blinded signatures
|
||||
let mut blinded_signatures = Vec::new();
|
||||
|
||||
for keypair in coconut_keypairs {
|
||||
let blinded_signature = blind_sign(
|
||||
¶ms,
|
||||
&keypair.secret_key(),
|
||||
&elgamal_keypair.public_key(),
|
||||
&blind_sign_request,
|
||||
&public_attributes,
|
||||
)?;
|
||||
blinded_signatures.push(blinded_signature)
|
||||
}
|
||||
|
||||
// Unblind
|
||||
|
||||
let unblinded_signatures: Vec<Signature> = blinded_signatures
|
||||
.into_iter()
|
||||
.zip(verification_keys.iter())
|
||||
.map(|(signature, verification_key)| {
|
||||
signature
|
||||
.unblind(
|
||||
¶ms,
|
||||
&elgamal_keypair.private_key(),
|
||||
&verification_key,
|
||||
&private_attributes,
|
||||
&public_attributes,
|
||||
&blind_sign_request.get_commitment_hash(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Aggregate signatures
|
||||
|
||||
let signature_shares: Vec<SignatureShare> = unblinded_signatures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64))
|
||||
.collect();
|
||||
|
||||
let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len());
|
||||
attributes.extend_from_slice(&private_attributes);
|
||||
attributes.extend_from_slice(&public_attributes);
|
||||
|
||||
// Randomize credentials and generate any cryptographic material to verify them
|
||||
let signature =
|
||||
aggregate_signature_shares(¶ms, &verification_key, &attributes, &signature_shares)?;
|
||||
|
||||
// Generate cryptographic material to verify them
|
||||
|
||||
let theta = prove_bandwidth_credential(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&signature,
|
||||
serial_number,
|
||||
binding_number,
|
||||
)?;
|
||||
|
||||
// Verify credentials
|
||||
|
||||
assert!(verify_credential(
|
||||
¶ms,
|
||||
&verification_key,
|
||||
&theta,
|
||||
&public_attributes,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
mod e2e;
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::CoconutError;
|
||||
|
||||
pub trait Bytable
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn to_byte_vec(&self) -> Vec<u8>;
|
||||
|
||||
fn try_from_byte_slice(slice: &[u8]) -> Result<Self, CoconutError>;
|
||||
}
|
||||
|
||||
pub trait Base58
|
||||
where
|
||||
Self: Bytable,
|
||||
{
|
||||
fn try_from_bs58<S: AsRef<str>>(x: S) -> Result<Self, CoconutError> {
|
||||
Self::try_from_byte_slice(&bs58::decode(x.as_ref()).into_vec().unwrap())
|
||||
}
|
||||
fn to_bs58(&self) -> String {
|
||||
bs58::encode(self.to_byte_vec()).into_string()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use core::iter::Sum;
|
||||
use core::ops::Mul;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField};
|
||||
use bls12_381::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar};
|
||||
use ff::Field;
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::SignerIndex;
|
||||
|
||||
pub struct Polynomial {
|
||||
coefficients: Vec<Scalar>,
|
||||
}
|
||||
|
||||
impl Polynomial {
|
||||
// for polynomial of degree n, we generate n+1 values
|
||||
// (for example for degree 1, like y = x + 2, we need [2,1])
|
||||
pub fn new_random(params: &Parameters, degree: u64) -> Self {
|
||||
Polynomial {
|
||||
coefficients: params.n_random_scalars((degree + 1) as usize),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the polynomial at point x.
|
||||
pub fn evaluate(&self, x: &Scalar) -> Scalar {
|
||||
if self.coefficients.is_empty() {
|
||||
Scalar::zero()
|
||||
// if x is zero then we can ignore most of the expensive computation and
|
||||
// just return the last term of the polynomial
|
||||
} else if x.is_zero() {
|
||||
// we checked that coefficients are not empty so unwrap here is fine
|
||||
*self.coefficients.first().unwrap()
|
||||
} else {
|
||||
self.coefficients
|
||||
.iter()
|
||||
.enumerate()
|
||||
// coefficient[n] * x ^ n
|
||||
.map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0]))
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec<Scalar> {
|
||||
let x = Scalar::zero();
|
||||
|
||||
points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, point_i)| {
|
||||
let mut numerator = Scalar::one();
|
||||
let mut denominator = Scalar::one();
|
||||
let xi = Scalar::from(*point_i);
|
||||
|
||||
for (j, point_j) in points.iter().enumerate() {
|
||||
if j != i {
|
||||
let xj = Scalar::from(*point_j);
|
||||
|
||||
// numerator = (x - xs[0]) * ... * (x - xs[j]), j != i
|
||||
numerator *= x - xj;
|
||||
|
||||
// denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i
|
||||
denominator *= xi - xj;
|
||||
}
|
||||
}
|
||||
// numerator / denominator
|
||||
numerator * denominator.invert().unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Performs a Lagrange interpolation at the origin for a polynomial defined by `points` and `values`.
|
||||
/// It can be used for Scalars, G1 and G2 points.
|
||||
pub(crate) fn perform_lagrangian_interpolation_at_origin<T>(
|
||||
points: &[SignerIndex],
|
||||
values: &[T],
|
||||
) -> Result<T>
|
||||
where
|
||||
T: Sum,
|
||||
for<'a> &'a T: Mul<Scalar, Output = T>,
|
||||
{
|
||||
if points.is_empty() || values.is_empty() {
|
||||
return Err(CoconutError::Interpolation(
|
||||
"Tried to perform lagrangian interpolation for an empty set of coordinates".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if points.len() != values.len() {
|
||||
return Err(CoconutError::Interpolation(
|
||||
"Tried to perform lagrangian interpolation for an incomplete set of coordinates"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let coefficients = generate_lagrangian_coefficients_at_origin(points);
|
||||
|
||||
Ok(coefficients
|
||||
.into_iter()
|
||||
.zip(values.iter())
|
||||
.map(|(coeff, val)| val * coeff)
|
||||
.sum())
|
||||
}
|
||||
|
||||
// A temporary way of hashing particular message into G1.
|
||||
// Implementation idea was taken from `threshold_crypto`:
|
||||
// https://github.com/poanetwork/threshold_crypto/blob/7709462f2df487ada3bb3243060504b5881f2628/src/lib.rs#L691
|
||||
// Eventually it should get replaced by, most likely, the osswu map
|
||||
// method once ideally it's implemented inside the pairing crate.
|
||||
|
||||
// note: I have absolutely no idea what are the correct domains for those. I just used whatever
|
||||
// was given in the test vectors of `Hashing to Elliptic Curves draft-irtf-cfrg-hash-to-curve-11`
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-J.9.1
|
||||
const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_";
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1
|
||||
const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander";
|
||||
|
||||
pub(crate) fn hash_g1<M: AsRef<[u8]>>(msg: M) -> G1Projective {
|
||||
<G1Projective as HashToCurve<ExpandMsgXmd<sha2::Sha256>>>::hash_to_curve(msg, G1_HASH_DOMAIN)
|
||||
}
|
||||
|
||||
pub fn hash_to_scalar<M: AsRef<[u8]>>(msg: M) -> Scalar {
|
||||
let mut output = vec![Scalar::zero()];
|
||||
|
||||
Scalar::hash_to_field::<ExpandMsgXmd<sha2::Sha256>>(
|
||||
msg.as_ref(),
|
||||
SCALAR_HASH_DOMAIN,
|
||||
&mut output,
|
||||
);
|
||||
output[0]
|
||||
}
|
||||
|
||||
pub(crate) fn try_deserialize_scalar_vec(
|
||||
expected_len: u64,
|
||||
bytes: &[u8],
|
||||
err: CoconutError,
|
||||
) -> Result<Vec<Scalar>> {
|
||||
if bytes.len() != expected_len as usize * 32 {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(expected_len as usize);
|
||||
for i in 0..expected_len as usize {
|
||||
let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap();
|
||||
let s = match Into::<Option<Scalar>>::into(Scalar::from_bytes(&s_bytes)) {
|
||||
None => return Err(err),
|
||||
Some(scalar) => scalar,
|
||||
};
|
||||
out.push(s)
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result<Scalar> {
|
||||
Into::<Option<Scalar>>::into(Scalar::from_bytes(bytes)).ok_or(err)
|
||||
}
|
||||
|
||||
pub(crate) fn try_deserialize_g1_projective(
|
||||
bytes: &[u8; 48],
|
||||
err: CoconutError,
|
||||
) -> Result<G1Projective> {
|
||||
Into::<Option<G1Affine>>::into(G1Affine::from_compressed(bytes))
|
||||
.ok_or(err)
|
||||
.map(G1Projective::from)
|
||||
}
|
||||
|
||||
pub(crate) fn try_deserialize_g2_projective(
|
||||
bytes: &[u8; 96],
|
||||
err: CoconutError,
|
||||
) -> Result<G2Projective> {
|
||||
Into::<Option<G2Affine>>::into(G2Affine::from_compressed(bytes))
|
||||
.ok_or(err)
|
||||
.map(G2Projective::from)
|
||||
}
|
||||
|
||||
// use core::fmt;
|
||||
// #[cfg(feature = "serde")]
|
||||
// use serde::de::Visitor;
|
||||
// #[cfg(feature = "serde")]
|
||||
// use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
|
||||
//
|
||||
// // #[cfg(feature = "serde")]
|
||||
// #[serde(remote = "Scalar")]
|
||||
// pub(crate) struct ScalarDef(pub Scalar);
|
||||
//
|
||||
// // #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
//
|
||||
// impl Serialize for ScalarDef {
|
||||
// fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
|
||||
// where
|
||||
// S: Serializer,
|
||||
// {
|
||||
// use serde::ser::SerializeTuple;
|
||||
// let mut tup = serializer.serialize_tuple(32)?;
|
||||
// for byte in self.0.to_bytes().iter() {
|
||||
// tup.serialize_element(byte)?;
|
||||
// }
|
||||
// tup.end()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl<'de> Deserialize<'de> for ScalarDef {
|
||||
// fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
|
||||
// where
|
||||
// D: Deserializer<'de>,
|
||||
// {
|
||||
// struct ScalarVisitor;
|
||||
//
|
||||
// impl<'de> Visitor<'de> for ScalarVisitor {
|
||||
// type Value = ScalarDef;
|
||||
//
|
||||
// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
// formatter.write_str("a 32-byte canonical bls12_381 scalar")
|
||||
// }
|
||||
//
|
||||
// fn visit_seq<A>(self, mut seq: A) -> core::result::Result<ScalarDef, A::Error>
|
||||
// where
|
||||
// A: serde::de::SeqAccess<'de>,
|
||||
// {
|
||||
// let mut bytes = [0u8; 32];
|
||||
// for i in 0..32 {
|
||||
// bytes[i] = seq
|
||||
// .next_element()?
|
||||
// .ok_or_else(|| serde::de::Error::invalid_length(i, &"expected 32 bytes"))?;
|
||||
// }
|
||||
//
|
||||
// let res = Scalar::from_bytes(&bytes);
|
||||
// if res.is_some().into() {
|
||||
// Ok(ScalarDef(res.unwrap()))
|
||||
// } else {
|
||||
// Err(serde::de::Error::custom(
|
||||
// &"scalar was not canonically encoded",
|
||||
// ))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// deserializer.deserialize_tuple(32, ScalarVisitor)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[cfg(feature = "serde")]
|
||||
// pub(crate) struct G1ProjectiveSerdeHelper(Scalar);
|
||||
//
|
||||
// #[cfg(feature = "serde")]
|
||||
// pub(crate) struct G2ProjectiveSerdeHelper(Scalar);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rand::RngCore;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn polynomial_evaluation() {
|
||||
// y = 42 (it should be 42 regardless of x)
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![Scalar::from(42)],
|
||||
};
|
||||
|
||||
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(1)));
|
||||
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(0)));
|
||||
assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(10)));
|
||||
|
||||
// y = x + 10, at x = 2 (exp: 12)
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![Scalar::from(10), Scalar::from(1)],
|
||||
};
|
||||
|
||||
assert_eq!(Scalar::from(12), poly.evaluate(&Scalar::from(2)));
|
||||
|
||||
// y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39)
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![
|
||||
(-Scalar::from(3)),
|
||||
Scalar::from(2),
|
||||
(-Scalar::from(5)),
|
||||
Scalar::zero(),
|
||||
Scalar::from(1),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(Scalar::from(39), poly.evaluate(&Scalar::from(3)));
|
||||
|
||||
// empty polynomial
|
||||
let poly = Polynomial {
|
||||
coefficients: vec![],
|
||||
};
|
||||
|
||||
// should always be 0
|
||||
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(1)));
|
||||
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(0)));
|
||||
assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn performing_lagrangian_scalar_interpolation_at_origin() {
|
||||
// x^2 + 3
|
||||
// x, f(x):
|
||||
// 1, 4,
|
||||
// 2, 7,
|
||||
// 3, 12,
|
||||
let points = vec![1, 2, 3];
|
||||
let values = vec![Scalar::from(4), Scalar::from(7), Scalar::from(12)];
|
||||
|
||||
assert_eq!(
|
||||
Scalar::from(3),
|
||||
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
|
||||
);
|
||||
|
||||
// x^3 + 3x^2 - 5x + 11
|
||||
// x, f(x):
|
||||
// 1, 10
|
||||
// 2, 21
|
||||
// 3, 50
|
||||
// 4, 103
|
||||
let points = vec![1, 2, 3, 4];
|
||||
let values = vec![
|
||||
Scalar::from(10),
|
||||
Scalar::from(21),
|
||||
Scalar::from(50),
|
||||
Scalar::from(103),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Scalar::from(11),
|
||||
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
|
||||
);
|
||||
|
||||
// more points than it is required
|
||||
// x^2 + x + 10
|
||||
// x, f(x)
|
||||
// 1, 12
|
||||
// 2, 16
|
||||
// 3, 22
|
||||
// 4, 30
|
||||
// 5, 40
|
||||
let points = vec![1, 2, 3, 4, 5];
|
||||
let values = vec![
|
||||
Scalar::from(12),
|
||||
Scalar::from(16),
|
||||
Scalar::from(22),
|
||||
Scalar::from(30),
|
||||
Scalar::from(40),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Scalar::from(10),
|
||||
perform_lagrangian_interpolation_at_origin(&points, &values).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_g1_sanity_check() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut msg1 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg1);
|
||||
let mut msg2 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg2);
|
||||
|
||||
assert_eq!(hash_g1(msg1), hash_g1(msg1));
|
||||
assert_eq!(hash_g1(msg2), hash_g1(msg2));
|
||||
assert_ne!(hash_g1(msg1), hash_g1(msg2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_scalar_sanity_check() {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut msg1 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg1);
|
||||
let mut msg2 = [0u8; 1024];
|
||||
rng.fill_bytes(&mut msg2);
|
||||
|
||||
assert_eq!(hash_to_scalar(msg1), hash_to_scalar(msg1));
|
||||
assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2));
|
||||
assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2));
|
||||
}
|
||||
}
|
||||
Generated
-1006
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
[workspace]
|
||||
members = ["erc20-bridge", "mixnet", "vesting"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
rpath = false
|
||||
lto = true
|
||||
debug-assertions = false
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
incremental = false
|
||||
overflow-checks = true
|
||||
@@ -5,9 +5,22 @@ edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[workspace] # adding a blank workspace to keep it out of the global workspace.
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
rpath = false
|
||||
lto = true
|
||||
debug-assertions = false
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
incremental = false
|
||||
overflow-checks = true
|
||||
|
||||
[features]
|
||||
# for more explicit tests, cargo test --features=backtraces
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
|
||||
Generated
+13
-677
@@ -2,45 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "Inflector"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ast_node"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e96d5444b02f3080edac8a144f6baf29b2fb6ff589ad4311559731a7c7529381"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"pmutil",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"swc_macros_common",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "az"
|
||||
version = "1.1.2"
|
||||
@@ -53,12 +14,6 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.7.3"
|
||||
@@ -89,12 +44,6 @@ dependencies = [
|
||||
"byte-tools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
|
||||
|
||||
[[package]]
|
||||
name = "byte-tools"
|
||||
version = "0.3.1"
|
||||
@@ -113,12 +62,6 @@ version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -248,38 +191,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
|
||||
name = "cw-storage-plus"
|
||||
version = "0.6.2"
|
||||
source = "git+https://github.com/jstuczyn/cw-plus?branch=feature/0.6.2-cosmwasm-fork#5d3e59c8327d1ab4a748c0be5ad3a376b710bfe5"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -309,45 +227,6 @@ dependencies = [
|
||||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dprint-core"
|
||||
version = "0.35.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93bd44f40b1881477837edc7112695d4b174f058c36c1cbc4c50f8d0482e2ac8"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"fnv",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dprint-plugin-typescript"
|
||||
version = "0.43.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67ba0077bd2ab9235848e793fbbfb563e6a04b4c8e4149827802a84063c15805"
|
||||
dependencies = [
|
||||
"dprint-core",
|
||||
"dprint-swc-ecma-ast-view",
|
||||
"fnv",
|
||||
"serde",
|
||||
"swc_common",
|
||||
"swc_ecmascript",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dprint-swc-ecma-ast-view"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecf692a2ee5c5f699ed0e95f21686cf6367f3a591e5d8e7bd3041bbf184651f9"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"fnv",
|
||||
"num-bigint",
|
||||
"swc_atoms",
|
||||
"swc_common",
|
||||
"swc_ecmascript",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.4"
|
||||
@@ -380,12 +259,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -402,18 +275,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum_kind"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b940da354ae81ef0926c5eaa428207b8f4f091d3956c891dfbd124162bed99"
|
||||
dependencies = [
|
||||
"pmutil",
|
||||
"proc-macro2",
|
||||
"swc_macros_common",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fake-simd"
|
||||
version = "0.1.2"
|
||||
@@ -439,15 +300,10 @@ dependencies = [
|
||||
"az",
|
||||
"bytemuck",
|
||||
"half",
|
||||
"serde",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.0.1"
|
||||
@@ -458,27 +314,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "from_variant"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0951635027ca477be98f8774abd6f0345233439d63f307e47101acb40c7cc63d"
|
||||
dependencies = [
|
||||
"pmutil",
|
||||
"proc-macro2",
|
||||
"swc_macros_common",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.4"
|
||||
@@ -504,7 +339,7 @@ version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
@@ -515,7 +350,7 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
]
|
||||
@@ -589,12 +424,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
@@ -606,28 +435,6 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-macro"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a322dd16d960e322c3d92f541b4c1a4f0a2e81e1fdeee430d8cecc8b72e8015f"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"pmutil",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
@@ -640,40 +447,25 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -688,12 +480,6 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||
|
||||
[[package]]
|
||||
name = "mixnet-contract"
|
||||
version = "0.1.0"
|
||||
@@ -707,7 +493,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"thiserror",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -718,6 +503,7 @@ dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cosmwasm-storage",
|
||||
"cw-storage-plus",
|
||||
"fixed",
|
||||
"mixnet-contract",
|
||||
"schemars",
|
||||
@@ -735,49 +521,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.3"
|
||||
@@ -790,40 +533,6 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "owning_ref"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
@@ -873,25 +582,6 @@ dependencies = [
|
||||
"sha-1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.7.6"
|
||||
@@ -902,29 +592,6 @@ dependencies = [
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pmutil"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3894e5d549cccbe44afecf72922f277f603cd4bb0219c8342631ef18fffbe004"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.32"
|
||||
@@ -949,30 +616,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc",
|
||||
"rand_pcg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
@@ -991,50 +634,6 @@ dependencies = [
|
||||
"getrandom 0.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_pcg"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
|
||||
dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.5"
|
||||
@@ -1065,18 +664,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.130"
|
||||
@@ -1158,7 +745,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug 0.3.0",
|
||||
@@ -1174,18 +761,6 @@ dependencies = [
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.4.1"
|
||||
@@ -1195,207 +770,18 @@ dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"new_debug_unreachable",
|
||||
"parking_lot",
|
||||
"phf_shared",
|
||||
"precomputed-hash",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache_codegen"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_enum"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f584cc881e9e5f1fd6bf827b0444aa94c30d8fe6378cf241071b5f5700b2871f"
|
||||
dependencies = [
|
||||
"pmutil",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"swc_macros_common",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "swc_atoms"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f5229fe227ff0060e13baa386d6e368797700eab909523f730008d191ee53ae"
|
||||
dependencies = [
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_common"
|
||||
version = "0.10.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c93df65683ec1a001e15ce1de438c7c2c226c0c2462d1cb93fa1bd2a7664170b"
|
||||
dependencies = [
|
||||
"ast_node",
|
||||
"cfg-if 0.1.10",
|
||||
"either",
|
||||
"from_variant",
|
||||
"fxhash",
|
||||
"log",
|
||||
"num-bigint",
|
||||
"once_cell",
|
||||
"owning_ref",
|
||||
"scoped-tls",
|
||||
"serde",
|
||||
"string_cache",
|
||||
"swc_eq_ignore_macros",
|
||||
"swc_visit",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_ecma_ast"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83eb6a73820660a5af3c24ae1d436e84e4d4c13822021140011361e678df247b"
|
||||
dependencies = [
|
||||
"is-macro",
|
||||
"num-bigint",
|
||||
"serde",
|
||||
"string_enum",
|
||||
"swc_atoms",
|
||||
"swc_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_ecma_parser"
|
||||
version = "0.52.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03250697857164f16fa98f8e1726f566652d13e52ea3f0c3ecea9deb63ee327"
|
||||
dependencies = [
|
||||
"either",
|
||||
"enum_kind",
|
||||
"fxhash",
|
||||
"log",
|
||||
"num-bigint",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"swc_atoms",
|
||||
"swc_common",
|
||||
"swc_ecma_ast",
|
||||
"swc_ecma_visit",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_ecma_visit"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd3d60b9dc97ae4f181d4d60f43142d8ac9669953db410bcedefb29a14627e19"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"swc_atoms",
|
||||
"swc_common",
|
||||
"swc_ecma_ast",
|
||||
"swc_visit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_ecmascript"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ffb53afe008c15d4dc4957e80148c4b457659f93e4d4e8736eaeae352e48ec8"
|
||||
dependencies = [
|
||||
"swc_ecma_ast",
|
||||
"swc_ecma_parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_eq_ignore_macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c8f200a2eaed938e7c1a685faaa66e6d42fa9e17da5f62572d3cbc335898f5e"
|
||||
dependencies = [
|
||||
"pmutil",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_macros_common"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf7c68e78ffbcba3d38abe6d0b76a0e1a37888b5c9301db3426537207090ada3"
|
||||
dependencies = [
|
||||
"pmutil",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_visit"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8511a4788ab29daf00bee23e425aac92c9be4eec74c98fec4a45d0e710be695"
|
||||
dependencies = [
|
||||
"either",
|
||||
"swc_visit_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swc_visit_macros"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b2825fee79f10d0166e8e650e79c7a862fb991db275743083f07555d7641f0"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"pmutil",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"swc_macros_common",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.81"
|
||||
@@ -1467,28 +853,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "369e48de67506679b3a576b0faf666fa9f9acf2fd00b4c61e28bdb6c8e08ec06"
|
||||
dependencies = [
|
||||
"dprint-plugin-typescript",
|
||||
"ts-rs-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f269e8fd28e26b4cdbd01f81f345aaf666131511e54a735a76a614b5062d0a5a"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.14.0"
|
||||
@@ -1528,12 +892,6 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
@@ -1570,28 +928,6 @@ version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.4.2"
|
||||
|
||||
@@ -12,9 +12,22 @@ exclude = [
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[workspace] # adding a blank workspace to keep it out of the global workspace.
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
rpath = false
|
||||
lto = true
|
||||
debug-assertions = false
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
incremental = false
|
||||
overflow-checks = true
|
||||
|
||||
[features]
|
||||
# for more explicit tests, cargo test --features=backtraces
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
@@ -22,12 +35,12 @@ backtraces = ["cosmwasm-std/backtraces"]
|
||||
[dependencies]
|
||||
mixnet-contract = { path = "../../common/mixnet-contract" }
|
||||
config = { path = "../../common/config"}
|
||||
vesting-contract = { path = "../vesting" }
|
||||
|
||||
# this branch is identical to 0.14.1 with addition of updated k256 dependency required to help poor cargo choose correct version
|
||||
cosmwasm-std = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", features = ["iterator"] }
|
||||
cosmwasm-storage = { git = "https://github.com/jstuczyn/cosmwasm", branch="0.14.1-updatedk256", features = ["iterator"] }
|
||||
|
||||
cw-storage-plus = { git = "https://github.com/jstuczyn/cw-plus", branch = "feature/0.6.2-cosmwasm-fork", features = ["iterator"] }
|
||||
#cosmwasm-std = { version = "0.14.1", features = ["iterator"] }
|
||||
#cosmwasm-storage = { version = "0.14.1", features = ["iterator"] }
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ extern crate mixnet_contract;
|
||||
|
||||
use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
|
||||
use mixnet_contract::{ExecuteMsg, InstantiateMsg, MixNodeBond, QueryMsg};
|
||||
use mixnet_contracts::state::State;
|
||||
use mixnet_contracts::mixnet_contract_settings::models::ContractSettings;
|
||||
use std::env::current_dir;
|
||||
use std::fs::create_dir_all;
|
||||
|
||||
@@ -18,6 +18,6 @@ fn main() {
|
||||
export_schema(&schema_for!(InstantiateMsg), &out_dir);
|
||||
export_schema(&schema_for!(ExecuteMsg), &out_dir);
|
||||
export_schema(&schema_for!(QueryMsg), &out_dir);
|
||||
export_schema(&schema_for!(State), &out_dir);
|
||||
export_schema(&schema_for!(ContractSettings), &out_dir);
|
||||
export_schema(&schema_for!(MixNodeBond), &out_dir);
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_state_params"
|
||||
"update_contract_settings"
|
||||
],
|
||||
"properties": {
|
||||
"update_state_params": {
|
||||
"update_contract_settings": {
|
||||
"$ref": "#/definitions/StateParams"
|
||||
}
|
||||
},
|
||||
@@ -346,4 +346,4 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,10 +101,10 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"state_params"
|
||||
"contract_settings_params"
|
||||
],
|
||||
"properties": {
|
||||
"state_params": {
|
||||
"contract_settings_params": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
@@ -321,4 +321,4 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,34 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::u128;
|
||||
|
||||
use crate::helpers::calculate_epoch_reward_rate;
|
||||
use crate::state::State;
|
||||
use crate::storage::{config, layer_distribution};
|
||||
use crate::{error::ContractError, queries, transactions};
|
||||
use crate::error::ContractError;
|
||||
use crate::gateways::queries::query_gateways_paged;
|
||||
use crate::gateways::queries::query_owns_gateway;
|
||||
use crate::mixnet_contract_settings::models::ContractSettings;
|
||||
use crate::mixnet_contract_settings::queries::query_contract_settings_params;
|
||||
use crate::mixnet_contract_settings::queries::query_rewarding_interval;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::bonding_queries as mixnode_queries;
|
||||
use crate::mixnodes::bonding_queries::query_mixnode_delegations_paged;
|
||||
use crate::mixnodes::bonding_queries::query_mixnodes_paged;
|
||||
use crate::mixnodes::delegation_queries::query_all_mixnode_delegations_paged;
|
||||
use crate::mixnodes::delegation_queries::query_mixnode_delegation;
|
||||
use crate::mixnodes::delegation_queries::query_reverse_mixnode_delegations_paged;
|
||||
use crate::mixnodes::layer_queries::query_layer_distribution;
|
||||
use crate::rewards::queries::query_reward_pool;
|
||||
use crate::rewards::queries::{query_circulating_supply, query_rewarding_status};
|
||||
use config::defaults::REWARDING_VALIDATOR_ADDRESS;
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_binary, Addr, Decimal, Deps, DepsMut, Env, MessageInfo, QueryResponse,
|
||||
Response, Uint128,
|
||||
entry_point, to_binary, Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128,
|
||||
};
|
||||
use mixnet_contract::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StateParams};
|
||||
|
||||
pub const INITIAL_DEFAULT_EPOCH_LENGTH: u32 = 2;
|
||||
use mixnet_contract::{ContractSettingsParams, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
use std::u128;
|
||||
|
||||
/// Constant specifying minimum of coin required to bond a gateway
|
||||
pub const INITIAL_GATEWAY_BOND: Uint128 = Uint128(100_000000);
|
||||
pub const INITIAL_GATEWAY_BOND: Uint128 = Uint128(100_000_000);
|
||||
|
||||
/// Constant specifying minimum of coin required to bond a mixnode
|
||||
pub const INITIAL_MIXNODE_BOND: Uint128 = Uint128(100_000000);
|
||||
|
||||
// percentage annual increase. Given starting value of x, we expect to have 1.1x at the end of the year
|
||||
pub const INITIAL_MIXNODE_BOND_REWARD_RATE: u64 = 110;
|
||||
pub const INITIAL_MIXNODE_DELEGATION_REWARD_RATE: u64 = 110;
|
||||
pub const INITIAL_MIXNODE_BOND: Uint128 = Uint128(100_000_000);
|
||||
|
||||
pub const INITIAL_MIXNODE_REWARDED_SET_SIZE: u32 = 200;
|
||||
pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100;
|
||||
@@ -33,38 +37,22 @@ pub const INITIAL_REWARD_POOL: u128 = 250_000_000_000_000;
|
||||
pub const EPOCH_REWARD_PERCENT: u8 = 2; // Used to calculate epoch reward pool
|
||||
pub const DEFAULT_SYBIL_RESISTANCE_PERCENT: u8 = 30;
|
||||
|
||||
// We'll be assuming a few more things, profit margin and cost function. Since we don't have relialable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate epoch costs to Nyms. We'll also assume a cost of 40$ per epoch(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms
|
||||
// We'll be assuming a few more things, profit margin and cost function. Since we don't have reliable package measurement, we'll be using uptime. We'll also set the value of 1 Nym to 1 $, to be able to translate epoch costs to Nyms. We'll also assume a cost of 40$ per epoch(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms
|
||||
pub const DEFAULT_COST_PER_EPOCH: u32 = 40_000_000;
|
||||
|
||||
pub const VESTING_CONTRACT_ADDR: &str = "vesting-contract-address";
|
||||
|
||||
fn default_initial_state(owner: Addr, env: Env) -> State {
|
||||
let mixnode_bond_reward_rate = Decimal::percent(INITIAL_MIXNODE_BOND_REWARD_RATE);
|
||||
let mixnode_delegation_reward_rate = Decimal::percent(INITIAL_MIXNODE_DELEGATION_REWARD_RATE);
|
||||
|
||||
State {
|
||||
fn default_initial_state(owner: Addr, env: Env) -> ContractSettings {
|
||||
ContractSettings {
|
||||
owner,
|
||||
rewarding_validator_address: Addr::unchecked(REWARDING_VALIDATOR_ADDRESS), // we trust our hardcoded value
|
||||
params: StateParams {
|
||||
epoch_length: INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
params: ContractSettingsParams {
|
||||
minimum_mixnode_bond: INITIAL_MIXNODE_BOND,
|
||||
minimum_gateway_bond: INITIAL_GATEWAY_BOND,
|
||||
mixnode_bond_reward_rate,
|
||||
mixnode_delegation_reward_rate,
|
||||
mixnode_rewarded_set_size: INITIAL_MIXNODE_REWARDED_SET_SIZE,
|
||||
mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE,
|
||||
},
|
||||
rewarding_interval_starting_block: env.block.height,
|
||||
latest_rewarding_interval_nonce: 0,
|
||||
rewarding_in_progress: false,
|
||||
mixnode_epoch_bond_reward: calculate_epoch_reward_rate(
|
||||
INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
mixnode_bond_reward_rate,
|
||||
),
|
||||
mixnode_epoch_delegation_reward: calculate_epoch_reward_rate(
|
||||
INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
mixnode_delegation_reward_rate,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +70,8 @@ pub fn instantiate(
|
||||
) -> Result<Response, ContractError> {
|
||||
let state = default_initial_state(info.sender, env);
|
||||
|
||||
config(deps.storage).save(&state)?;
|
||||
layer_distribution(deps.storage).save(&Default::default())?;
|
||||
mixnet_params_storage::CONTRACT_SETTINGS.save(deps.storage, &state)?;
|
||||
mixnet_params_storage::LAYERS.save(deps.storage, &Default::default())?;
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
@@ -97,33 +85,27 @@ pub fn execute(
|
||||
) -> Result<Response, ContractError> {
|
||||
match msg {
|
||||
ExecuteMsg::BondMixnode { mix_node } => {
|
||||
transactions::try_add_mixnode(deps, env, info, mix_node)
|
||||
crate::mixnodes::bonding_transactions::try_add_mixnode(deps, env, info, mix_node)
|
||||
}
|
||||
ExecuteMsg::UnbondMixnode {} => {
|
||||
crate::mixnodes::bonding_transactions::try_remove_mixnode(deps, info)
|
||||
}
|
||||
ExecuteMsg::UnbondMixnode {} => transactions::try_remove_mixnode(deps, info),
|
||||
ExecuteMsg::BondGateway { gateway } => {
|
||||
transactions::try_add_gateway(deps, env, info, gateway)
|
||||
crate::gateways::transactions::try_add_gateway(deps, env, info, gateway)
|
||||
}
|
||||
ExecuteMsg::UnbondGateway {} => transactions::try_remove_gateway(deps, info),
|
||||
ExecuteMsg::UpdateStateParams(params) => {
|
||||
transactions::try_update_state_params(deps, info, params)
|
||||
ExecuteMsg::UnbondGateway {} => {
|
||||
crate::gateways::transactions::try_remove_gateway(deps, info)
|
||||
}
|
||||
ExecuteMsg::UpdateContractSettings(params) => {
|
||||
crate::mixnet_contract_settings::transactions::try_update_contract_settings(
|
||||
deps, info, params,
|
||||
)
|
||||
}
|
||||
ExecuteMsg::RewardMixnode {
|
||||
identity,
|
||||
uptime,
|
||||
rewarding_interval_nonce,
|
||||
} => transactions::try_reward_mixnode(
|
||||
deps,
|
||||
env,
|
||||
info,
|
||||
identity,
|
||||
uptime,
|
||||
rewarding_interval_nonce,
|
||||
),
|
||||
ExecuteMsg::RewardMixnodeV2 {
|
||||
identity,
|
||||
params,
|
||||
rewarding_interval_nonce,
|
||||
} => transactions::try_reward_mixnode_v2(
|
||||
} => crate::rewards::transactions::try_reward_mixnode_v2(
|
||||
deps,
|
||||
env,
|
||||
info,
|
||||
@@ -132,42 +114,44 @@ pub fn execute(
|
||||
rewarding_interval_nonce,
|
||||
),
|
||||
ExecuteMsg::DelegateToMixnode { mix_identity } => {
|
||||
transactions::try_delegate_to_mixnode(deps, env, info, mix_identity)
|
||||
crate::mixnodes::delegation_transactions::try_delegate_to_mixnode(
|
||||
deps,
|
||||
env,
|
||||
info,
|
||||
mix_identity,
|
||||
)
|
||||
}
|
||||
ExecuteMsg::UndelegateFromMixnode { mix_identity } => {
|
||||
transactions::try_remove_delegation_from_mixnode(deps, info, mix_identity)
|
||||
crate::mixnodes::delegation_transactions::try_remove_delegation_from_mixnode(
|
||||
deps,
|
||||
info,
|
||||
mix_identity,
|
||||
)
|
||||
}
|
||||
ExecuteMsg::BeginMixnodeRewarding {
|
||||
rewarding_interval_nonce,
|
||||
} => transactions::try_begin_mixnode_rewarding(deps, env, info, rewarding_interval_nonce),
|
||||
ExecuteMsg::FinishMixnodeRewarding {
|
||||
rewarding_interval_nonce,
|
||||
} => transactions::try_finish_mixnode_rewarding(deps, info, rewarding_interval_nonce),
|
||||
ExecuteMsg::DelegateToMixnodeOnBehalf {
|
||||
mix_identity,
|
||||
delegate,
|
||||
} => transactions::try_delegate_to_mixnode_on_behalf(
|
||||
} => crate::rewards::transactions::try_begin_mixnode_rewarding(
|
||||
deps,
|
||||
env,
|
||||
info,
|
||||
mix_identity,
|
||||
delegate,
|
||||
rewarding_interval_nonce,
|
||||
),
|
||||
ExecuteMsg::UndelegateFromMixnodeOnBehalf {
|
||||
ExecuteMsg::FinishMixnodeRewarding {
|
||||
rewarding_interval_nonce,
|
||||
} => crate::rewards::transactions::try_finish_mixnode_rewarding(
|
||||
deps,
|
||||
info,
|
||||
rewarding_interval_nonce,
|
||||
),
|
||||
ExecuteMsg::RewardNextMixDelegators {
|
||||
mix_identity,
|
||||
delegate,
|
||||
} => transactions::try_remove_delegation_from_mixnode_on_behalf(
|
||||
rewarding_interval_nonce,
|
||||
} => crate::rewards::transactions::try_reward_next_mixnode_delegators_v2(
|
||||
deps,
|
||||
info,
|
||||
mix_identity,
|
||||
delegate,
|
||||
rewarding_interval_nonce,
|
||||
),
|
||||
ExecuteMsg::BondMixnodeOnBehalf { mix_node, owner } => {
|
||||
transactions::try_add_mixnode_on_behalf(deps, env, info, mix_node, owner)
|
||||
}
|
||||
ExecuteMsg::UnbondMixnodeOnBehalf { owner } => {
|
||||
transactions::try_remove_mixnode_on_behalf(deps, info, owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,40 +159,36 @@ pub fn execute(
|
||||
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
|
||||
let query_res = match msg {
|
||||
QueryMsg::GetMixNodes { start_after, limit } => {
|
||||
to_binary(&queries::query_mixnodes_paged(deps, start_after, limit)?)
|
||||
to_binary(&query_mixnodes_paged(deps, start_after, limit)?)
|
||||
}
|
||||
QueryMsg::GetGateways { limit, start_after } => {
|
||||
to_binary(&queries::query_gateways_paged(deps, start_after, limit)?)
|
||||
to_binary(&query_gateways_paged(deps, start_after, limit)?)
|
||||
}
|
||||
QueryMsg::OwnsMixnode { address } => {
|
||||
to_binary(&queries::query_owns_mixnode(deps, address)?)
|
||||
to_binary(&mixnode_queries::query_owns_mixnode(deps, address)?)
|
||||
}
|
||||
QueryMsg::OwnsGateway { address } => {
|
||||
to_binary(&queries::query_owns_gateway(deps, address)?)
|
||||
}
|
||||
QueryMsg::StateParams {} => to_binary(&queries::query_state_params(deps)),
|
||||
QueryMsg::CurrentRewardingInterval {} => {
|
||||
to_binary(&queries::query_rewarding_interval(deps))
|
||||
}
|
||||
QueryMsg::LayerDistribution {} => to_binary(&queries::query_layer_distribution(deps)),
|
||||
QueryMsg::OwnsGateway { address } => to_binary(&query_owns_gateway(deps, address)?),
|
||||
QueryMsg::StateParams {} => to_binary(&query_contract_settings_params(deps)?),
|
||||
QueryMsg::CurrentRewardingInterval {} => to_binary(&query_rewarding_interval(deps)?),
|
||||
QueryMsg::LayerDistribution {} => to_binary(&query_layer_distribution(deps)?),
|
||||
QueryMsg::GetMixDelegations {
|
||||
mix_identity,
|
||||
start_after,
|
||||
limit,
|
||||
} => to_binary(&queries::query_mixnode_delegations_paged(
|
||||
} => to_binary(&query_mixnode_delegations_paged(
|
||||
deps,
|
||||
mix_identity,
|
||||
start_after,
|
||||
limit,
|
||||
)?),
|
||||
QueryMsg::GetAllMixDelegations { start_after, limit } => to_binary(
|
||||
&queries::query_all_mixnode_delegations_paged(deps, start_after, limit)?,
|
||||
&query_all_mixnode_delegations_paged(deps, start_after, limit)?,
|
||||
),
|
||||
QueryMsg::GetReverseMixDelegations {
|
||||
delegation_owner,
|
||||
start_after,
|
||||
limit,
|
||||
} => to_binary(&queries::query_reverse_mixnode_delegations_paged(
|
||||
} => to_binary(&query_reverse_mixnode_delegations_paged(
|
||||
deps,
|
||||
delegation_owner,
|
||||
start_after,
|
||||
@@ -217,15 +197,19 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<QueryResponse, Cont
|
||||
QueryMsg::GetMixDelegation {
|
||||
mix_identity,
|
||||
address,
|
||||
} => to_binary(&queries::query_mixnode_delegation(
|
||||
deps,
|
||||
mix_identity,
|
||||
address,
|
||||
)?),
|
||||
QueryMsg::GetRewardPool {} => to_binary(&queries::query_reward_pool(deps)),
|
||||
QueryMsg::GetCirculatingSupply {} => to_binary(&queries::query_circulating_supply(deps)),
|
||||
} => to_binary(&query_mixnode_delegation(deps, mix_identity, address)?),
|
||||
QueryMsg::GetRewardPool {} => to_binary(&query_reward_pool(deps)),
|
||||
QueryMsg::GetCirculatingSupply {} => to_binary(&query_circulating_supply(deps)),
|
||||
QueryMsg::GetEpochRewardPercent {} => to_binary(&EPOCH_REWARD_PERCENT),
|
||||
QueryMsg::GetSybilResistancePercent {} => to_binary(&DEFAULT_SYBIL_RESISTANCE_PERCENT),
|
||||
QueryMsg::GetRewardingStatus {
|
||||
mix_identity,
|
||||
rewarding_interval_nonce,
|
||||
} => to_binary(&query_rewarding_status(
|
||||
deps,
|
||||
mix_identity,
|
||||
rewarding_interval_nonce,
|
||||
)?),
|
||||
};
|
||||
|
||||
Ok(query_res?)
|
||||
@@ -238,7 +222,7 @@ pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response,
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::helpers::*;
|
||||
use crate::support::tests::test_helpers;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
|
||||
use cosmwasm_std::{coins, from_binary};
|
||||
@@ -270,7 +254,7 @@ pub mod tests {
|
||||
// Contract balance should match what we initialized it as
|
||||
assert_eq!(
|
||||
coins(0, DENOM),
|
||||
query_contract_balance(env.contract.address, deps)
|
||||
test_helpers::query_contract_balance(env.contract.address, deps)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,15 +45,15 @@ pub enum ContractError {
|
||||
#[error("No coin was sent for the bonding, you must send {}", DENOM)]
|
||||
NoBondFound,
|
||||
|
||||
#[error("The bond reward rate for mixnode was set to be lower than 1")]
|
||||
DecreasingMixnodeBondReward,
|
||||
|
||||
#[error("The delegation reward rate for mixnode was set to be lower than 1")]
|
||||
DecreasingMixnodeDelegationReward,
|
||||
|
||||
#[error("Provided active set size is bigger than the demanded set")]
|
||||
InvalidActiveSetSize,
|
||||
|
||||
#[error("Provided active set size is zero")]
|
||||
ZeroActiveSet,
|
||||
|
||||
#[error("Provided rewarded set size is zero")]
|
||||
ZeroRewardedSet,
|
||||
|
||||
#[error("The node had uptime larger than 100%")]
|
||||
UnexpectedUptime,
|
||||
|
||||
@@ -80,15 +80,10 @@ pub enum ContractError {
|
||||
identity: IdentityKey,
|
||||
address: Addr,
|
||||
},
|
||||
#[error("Overflow error!")]
|
||||
Overflow(#[from] cosmwasm_std::OverflowError),
|
||||
|
||||
#[error("We tried to remove more funds then are available in the Reward pool. Wanted to remove {to_remove}, but have only {reward_pool}")]
|
||||
OutOfFunds { to_remove: u128, reward_pool: u128 },
|
||||
|
||||
#[error("Invalid ratio")]
|
||||
Ratio(#[from] mixnet_contract::error::MixnetContractError),
|
||||
|
||||
#[error("Received invalid rewarding interval nonce. Expected {expected}, received {received}")]
|
||||
InvalidRewardingIntervalNonce { received: u32, expected: u32 },
|
||||
|
||||
@@ -101,6 +96,9 @@ pub enum ContractError {
|
||||
#[error("Mixnode {identity} has already been rewarded during the current rewarding interval")]
|
||||
MixnodeAlreadyRewarded { identity: IdentityKey },
|
||||
|
||||
#[error("Proxy address mismatch, expected {existing}, got {incoming}")]
|
||||
ProxyMismatch{existing: String, incoming: String}
|
||||
#[error("Some of mixnodes {identity} delegators are still pending reward")]
|
||||
DelegatorsPendingReward { identity: IdentityKey },
|
||||
|
||||
#[error("Mixnode's {identity} operator has not been rewarded yet - cannot perform delegator rewarding until that happens")]
|
||||
MixnodeOperatorNotRewarded { identity: IdentityKey },
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod queries;
|
||||
pub mod storage;
|
||||
pub mod transactions;
|
||||
@@ -0,0 +1,217 @@
|
||||
use super::storage;
|
||||
use crate::mixnodes::storage::{BOND_PAGE_DEFAULT_LIMIT, BOND_PAGE_MAX_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant.
|
||||
use crate::query_support::calculate_start_value;
|
||||
use cosmwasm_std::{Addr, Deps, Order, StdResult};
|
||||
use mixnet_contract::{GatewayBond, GatewayOwnershipResponse, IdentityKey, PagedGatewayResponse};
|
||||
|
||||
pub(crate) fn query_gateways_paged(
|
||||
deps: Deps,
|
||||
start_after: Option<IdentityKey>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedGatewayResponse> {
|
||||
let limit = limit
|
||||
.unwrap_or(BOND_PAGE_DEFAULT_LIMIT)
|
||||
.min(BOND_PAGE_MAX_LIMIT) as usize;
|
||||
let start = calculate_start_value(start_after);
|
||||
|
||||
let nodes = storage::gateways_read(deps.storage)
|
||||
.range(start.as_deref(), None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|res| res.map(|item| item.1))
|
||||
.collect::<StdResult<Vec<GatewayBond>>>()?;
|
||||
|
||||
let start_next_after = nodes.last().map(|node| node.identity().clone());
|
||||
|
||||
Ok(PagedGatewayResponse::new(nodes, limit, start_next_after))
|
||||
}
|
||||
|
||||
pub(crate) fn query_owns_gateway(deps: Deps, address: Addr) -> StdResult<GatewayOwnershipResponse> {
|
||||
let has_gateway = storage::gateways_owners_read(deps.storage)
|
||||
.may_load(address.as_bytes())?
|
||||
.is_some();
|
||||
Ok(GatewayOwnershipResponse {
|
||||
address,
|
||||
has_gateway,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::gateways::storage;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::{Addr, Storage};
|
||||
use mixnet_contract::Gateway;
|
||||
|
||||
#[test]
|
||||
fn gateways_empty_on_init() {
|
||||
let deps = test_helpers::init_contract();
|
||||
let response = query_gateways_paged(deps.as_ref(), None, Option::from(2)).unwrap();
|
||||
assert_eq!(0, response.nodes.len());
|
||||
}
|
||||
|
||||
fn store_n_gateway_fixtures(n: u32, storage: &mut dyn Storage) {
|
||||
for i in 0..n {
|
||||
let key = format!("bond{}", i);
|
||||
let node = test_helpers::gateway_bond_fixture();
|
||||
storage::gateways(storage)
|
||||
.save(key.as_bytes(), &node)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateways_paged_retrieval_obeys_limits() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let storage = deps.as_mut().storage;
|
||||
let limit = 2;
|
||||
store_n_gateway_fixtures(100, storage);
|
||||
|
||||
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(limit)).unwrap();
|
||||
assert_eq!(limit, page1.nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateways_paged_retrieval_has_default_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let storage = deps.as_mut().storage;
|
||||
store_n_gateway_fixtures(10 * BOND_PAGE_DEFAULT_LIMIT, storage);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
let page1 = query_gateways_paged(deps.as_ref(), None, None).unwrap();
|
||||
|
||||
assert_eq!(BOND_PAGE_DEFAULT_LIMIT, page1.nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateways_paged_retrieval_has_max_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let storage = deps.as_mut().storage;
|
||||
store_n_gateway_fixtures(100, storage);
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000 * BOND_PAGE_DEFAULT_LIMIT;
|
||||
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = BOND_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_pagination_works() {
|
||||
let addr1 = "nym100";
|
||||
let addr2 = "nym101";
|
||||
let addr3 = "nym102";
|
||||
let addr4 = "nym103";
|
||||
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node = test_helpers::gateway_bond_fixture();
|
||||
|
||||
// TODO: note for JS when doing a deep review for the contract: add a similar test_helper as there is for mixnodes,
|
||||
// i.e. don't interact with storage here, but get the helper to call an actual transaction
|
||||
storage::gateways(&mut deps.storage)
|
||||
.save(addr1.as_bytes(), &node)
|
||||
.unwrap();
|
||||
|
||||
let per_page = 2;
|
||||
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.nodes.len());
|
||||
|
||||
// save another
|
||||
storage::gateways(&mut deps.storage)
|
||||
.save(addr2.as_bytes(), &node)
|
||||
.unwrap();
|
||||
|
||||
// page1 should have 2 results on it
|
||||
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
|
||||
assert_eq!(2, page1.nodes.len());
|
||||
|
||||
storage::gateways(&mut deps.storage)
|
||||
.save(addr3.as_bytes(), &node)
|
||||
.unwrap();
|
||||
|
||||
// page1 still has 2 results
|
||||
let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
|
||||
assert_eq!(2, page1.nodes.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after = String::from(addr2);
|
||||
let page2 = query_gateways_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.nodes.len());
|
||||
|
||||
// save another one
|
||||
storage::gateways(&mut deps.storage)
|
||||
.save(addr4.as_bytes(), &node)
|
||||
.unwrap();
|
||||
|
||||
let start_after = String::from(addr2);
|
||||
let page2 = query_gateways_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.nodes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_for_gateway_owner_works() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
// "fred" does not own a mixnode if there are no mixnodes
|
||||
let res = query_owns_gateway(deps.as_ref(), Addr::unchecked("fred")).unwrap();
|
||||
assert!(!res.has_gateway);
|
||||
|
||||
// mixnode was added to "bob", "fred" still does not own one
|
||||
let node = Gateway {
|
||||
identity_key: "bobsnode".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
};
|
||||
crate::gateways::transactions::try_add_gateway(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info("bob", &test_helpers::good_gateway_bond()),
|
||||
node,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = query_owns_gateway(deps.as_ref(), Addr::unchecked("fred")).unwrap();
|
||||
assert!(!res.has_gateway);
|
||||
|
||||
// "fred" now owns a gateway!
|
||||
let node = Gateway {
|
||||
identity_key: "fredsnode".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
};
|
||||
crate::gateways::transactions::try_add_gateway(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info("fred", &test_helpers::good_gateway_bond()),
|
||||
node,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = query_owns_gateway(deps.as_ref(), Addr::unchecked("fred")).unwrap();
|
||||
assert!(res.has_gateway);
|
||||
|
||||
// but after unbonding it, he doesn't own one anymore
|
||||
crate::gateways::transactions::try_remove_gateway(deps.as_mut(), mock_info("fred", &[]))
|
||||
.unwrap();
|
||||
|
||||
let res = query_owns_gateway(deps.as_ref(), Addr::unchecked("fred")).unwrap();
|
||||
assert!(!res.has_gateway);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use cosmwasm_std::Storage;
|
||||
use cosmwasm_storage::bucket;
|
||||
use cosmwasm_storage::bucket_read;
|
||||
use cosmwasm_storage::Bucket;
|
||||
use cosmwasm_storage::ReadonlyBucket;
|
||||
use mixnet_contract::GatewayBond;
|
||||
use mixnet_contract::IdentityKey;
|
||||
|
||||
// storage prefixes
|
||||
const PREFIX_GATEWAYS: &[u8] = b"gt";
|
||||
const PREFIX_GATEWAYS_OWNERS: &[u8] = b"go";
|
||||
|
||||
pub fn gateways(storage: &mut dyn Storage) -> Bucket<GatewayBond> {
|
||||
bucket(storage, PREFIX_GATEWAYS)
|
||||
}
|
||||
|
||||
pub fn gateways_read(storage: &dyn Storage) -> ReadonlyBucket<GatewayBond> {
|
||||
bucket_read(storage, PREFIX_GATEWAYS)
|
||||
}
|
||||
|
||||
// owner address -> node identity
|
||||
pub fn gateways_owners(storage: &mut dyn Storage) -> Bucket<IdentityKey> {
|
||||
bucket(storage, PREFIX_GATEWAYS_OWNERS)
|
||||
}
|
||||
|
||||
pub fn gateways_owners_read(storage: &dyn Storage) -> ReadonlyBucket<IdentityKey> {
|
||||
bucket_read(storage, PREFIX_GATEWAYS_OWNERS)
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cosmwasm_std::StdResult;
|
||||
use cosmwasm_std::Storage;
|
||||
|
||||
use super::super::storage;
|
||||
use crate::support::tests::test_helpers;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::testing::MockStorage;
|
||||
use cosmwasm_std::{coin, Addr, Uint128};
|
||||
use mixnet_contract::Gateway;
|
||||
use mixnet_contract::GatewayBond;
|
||||
use mixnet_contract::IdentityKey;
|
||||
|
||||
// currently this is only used in tests but may become useful later on
|
||||
pub(crate) fn read_gateway_bond(
|
||||
storage: &dyn Storage,
|
||||
identity: &[u8],
|
||||
) -> StdResult<cosmwasm_std::Uint128> {
|
||||
let bucket = storage::gateways_read(storage);
|
||||
let node = bucket.load(identity)?;
|
||||
Ok(node.bond_amount.amount)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = test_helpers::gateway_bond_fixture();
|
||||
let bond2 = test_helpers::gateway_bond_fixture();
|
||||
storage::gateways(&mut storage)
|
||||
.save(b"bond1", &bond1)
|
||||
.unwrap();
|
||||
storage::gateways(&mut storage)
|
||||
.save(b"bond2", &bond2)
|
||||
.unwrap();
|
||||
|
||||
let res1 = storage::gateways_read(&storage).load(b"bond1").unwrap();
|
||||
let res2 = storage::gateways_read(&storage).load(b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_gateway_bond() {
|
||||
let mut mock_storage = MockStorage::new();
|
||||
let node_owner: Addr = Addr::unchecked("node-owner");
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
|
||||
// produces an error if target gateway doesn't exist
|
||||
let res = read_gateway_bond(&mock_storage, node_owner.as_bytes());
|
||||
assert!(res.is_err());
|
||||
|
||||
// returns appropriate value otherwise
|
||||
let bond_value = 1000;
|
||||
|
||||
let gateway_bond = GatewayBond {
|
||||
bond_amount: coin(bond_value, DENOM),
|
||||
owner: node_owner.clone(),
|
||||
block_height: 12_345,
|
||||
gateway: Gateway {
|
||||
identity_key: node_identity.clone(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
storage::gateways(&mut mock_storage)
|
||||
.save(node_identity.as_bytes(), &gateway_bond)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(bond_value),
|
||||
read_gateway_bond(&mock_storage, node_identity.as_bytes()).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
use super::storage;
|
||||
use crate::error::ContractError;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{attr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128};
|
||||
use mixnet_contract::{Gateway, GatewayBond, Layer};
|
||||
|
||||
pub(crate) fn try_add_gateway(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
gateway: Gateway,
|
||||
) -> Result<Response, ContractError> {
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
|
||||
// if the client has an active bonded mixnode, don't allow gateway bonding
|
||||
if mixnodes_storage::mixnodes()
|
||||
.idx
|
||||
.owner
|
||||
.item(deps.storage, info.sender.to_string())?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ContractError::AlreadyOwnsMixnode);
|
||||
}
|
||||
|
||||
// if the client has an active bonded gateway, regardless of its identity, don't allow bonding
|
||||
if storage::gateways_owners_read(deps.storage)
|
||||
.may_load(sender_bytes)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ContractError::AlreadyOwnsGateway);
|
||||
}
|
||||
|
||||
// check if somebody else has already bonded a gateway with this identity
|
||||
if let Some(existing_bond) =
|
||||
storage::gateways_read(deps.storage).may_load(gateway.identity_key.as_bytes())?
|
||||
{
|
||||
if existing_bond.owner != info.sender {
|
||||
return Err(ContractError::DuplicateGateway {
|
||||
owner: existing_bond.owner,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let minimum_bond = mixnet_params_storage::CONTRACT_SETTINGS
|
||||
.load(deps.storage)?
|
||||
.params
|
||||
.minimum_gateway_bond;
|
||||
validate_gateway_bond(&info.funds, minimum_bond)?;
|
||||
|
||||
let bond = GatewayBond::new(
|
||||
info.funds[0].clone(),
|
||||
info.sender.clone(),
|
||||
env.block.height,
|
||||
gateway,
|
||||
);
|
||||
|
||||
let identity = bond.identity();
|
||||
storage::gateways(deps.storage).save(identity.as_bytes(), &bond)?;
|
||||
storage::gateways_owners(deps.storage).save(sender_bytes, identity)?;
|
||||
mixnet_params_storage::increment_layer_count(deps.storage, Layer::Gateway)?;
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
pub(crate) fn try_remove_gateway(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
) -> Result<Response, ContractError> {
|
||||
let sender_bytes = info.sender.as_str().as_bytes();
|
||||
|
||||
// try to find the identity of the sender's node
|
||||
let gateway_identity =
|
||||
match storage::gateways_owners_read(deps.storage).may_load(sender_bytes)? {
|
||||
Some(identity) => identity,
|
||||
None => return Err(ContractError::NoAssociatedGatewayBond { owner: info.sender }),
|
||||
};
|
||||
|
||||
// get the bond, since we found associated identity, the node MUST exist
|
||||
let gateway_bond = storage::gateways_read(deps.storage).load(gateway_identity.as_bytes())?;
|
||||
|
||||
// send bonded funds back to the bond owner
|
||||
let messages = vec![BankMsg::Send {
|
||||
to_address: info.sender.as_str().to_owned(),
|
||||
amount: vec![gateway_bond.bond_amount()],
|
||||
}
|
||||
.into()];
|
||||
|
||||
// remove the bond from the list of bonded gateways
|
||||
storage::gateways(deps.storage).remove(gateway_identity.as_bytes());
|
||||
// remove the node ownership
|
||||
storage::gateways_owners(deps.storage).remove(sender_bytes);
|
||||
// decrement layer count
|
||||
mixnet_params_storage::decrement_layer_count(deps.storage, Layer::Gateway)?;
|
||||
|
||||
// log our actions
|
||||
let attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr("address", info.sender),
|
||||
attr("gateway_bond", gateway_bond),
|
||||
];
|
||||
|
||||
Ok(Response {
|
||||
submessages: Vec::new(),
|
||||
messages,
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_gateway_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), ContractError> {
|
||||
// check if anything was put as bond
|
||||
if bond.is_empty() {
|
||||
return Err(ContractError::NoBondFound);
|
||||
}
|
||||
|
||||
if bond.len() > 1 {
|
||||
return Err(ContractError::MultipleDenoms);
|
||||
}
|
||||
|
||||
// check that the denomination is correct
|
||||
if bond[0].denom != DENOM {
|
||||
return Err(ContractError::WrongDenom {});
|
||||
}
|
||||
|
||||
// check that we have at least 100 coins in our bond
|
||||
if bond[0].amount < minimum_bond {
|
||||
return Err(ContractError::InsufficientGatewayBond {
|
||||
received: bond[0].amount.into(),
|
||||
minimum: minimum_bond.into(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::contract::{execute, query, INITIAL_GATEWAY_BOND};
|
||||
use crate::error::ContractError;
|
||||
use crate::gateways::transactions::try_add_gateway;
|
||||
use crate::gateways::transactions::validate_gateway_bond;
|
||||
use crate::support::tests::test_helpers;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::attr;
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::{coins, BankMsg, Response};
|
||||
use cosmwasm_std::{from_binary, Addr, Uint128};
|
||||
use mixnet_contract::Gateway;
|
||||
use mixnet_contract::MixNode;
|
||||
use mixnet_contract::{ExecuteMsg, PagedGatewayResponse, QueryMsg};
|
||||
|
||||
#[test]
|
||||
fn gateway_add() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
// if we fail validation (by say not sending enough funds
|
||||
let insufficient_bond = Into::<u128>::into(INITIAL_GATEWAY_BOND) - 1;
|
||||
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: test_helpers::gateway_fixture(),
|
||||
};
|
||||
|
||||
// we are informed that we didn't send enough funds
|
||||
let result = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientGatewayBond {
|
||||
received: insufficient_bond,
|
||||
minimum: INITIAL_GATEWAY_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// make sure no gateway was inserted into the topology
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetGateways {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedGatewayResponse = from_binary(&res).unwrap();
|
||||
assert_eq!(0, page.nodes.len());
|
||||
|
||||
// if we send enough funds
|
||||
let info = mock_info("anyone", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "anyonesgateway".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
// we get back a message telling us everything was OK
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(execute_response.is_ok());
|
||||
|
||||
// we can query topology and the new node is there
|
||||
let query_response = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetGateways {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedGatewayResponse = from_binary(&query_response).unwrap();
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!(
|
||||
&Gateway {
|
||||
identity_key: "anyonesgateway".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
page.nodes[0].gateway()
|
||||
);
|
||||
|
||||
// if there was already a gateway bonded by particular user
|
||||
let info = mock_info("foomper", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "foompersgateway".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
let info = mock_info("foomper", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "foompersgateway".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
// it fails
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(Err(ContractError::AlreadyOwnsGateway), execute_response);
|
||||
|
||||
// bonding fails if the user already owns a mixnode
|
||||
let info = mock_info("mixnode-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "ownersmix".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
let info = mock_info("mixnode-owner", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: test_helpers::gateway_fixture(),
|
||||
};
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(execute_response, Err(ContractError::AlreadyOwnsMixnode));
|
||||
|
||||
// but after he unbonds it, it's all fine again
|
||||
let info = mock_info("mixnode-owner", &[]);
|
||||
let msg = ExecuteMsg::UnbondMixnode {};
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
let info = mock_info("mixnode-owner", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: test_helpers::gateway_fixture(),
|
||||
};
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(execute_response.is_ok());
|
||||
|
||||
// adding another node from another account, but with the same IP, should fail (or we would have a weird state).
|
||||
// Is that right? Think about this, not sure yet.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_gateway_without_existing_owner() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let info = mock_info("gateway-owner", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "myAwesomeGateway".to_string(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
// before the execution the node had no associated owner
|
||||
assert!(storage::gateways_owners_read(deps.as_ref().storage)
|
||||
.may_load("gateway-owner".as_bytes())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// it's all fine, owner is saved
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(execute_response.is_ok());
|
||||
|
||||
assert_eq!(
|
||||
"myAwesomeGateway",
|
||||
storage::gateways_owners_read(deps.as_ref().storage)
|
||||
.load("gateway-owner".as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_gateway_with_existing_owner() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let info = mock_info("gateway-owner", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "myAwesomeGateway".to_string(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
// request fails giving the existing owner address in the message
|
||||
let info = mock_info(
|
||||
"gateway-owner-pretender",
|
||||
&test_helpers::good_gateway_bond(),
|
||||
);
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "myAwesomeGateway".to_string(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
Err(ContractError::DuplicateGateway {
|
||||
owner: Addr::unchecked("gateway-owner")
|
||||
}),
|
||||
execute_response
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_gateway_with_existing_unchanged_owner() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let info = mock_info("gateway-owner", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "myAwesomeGateway".to_string(),
|
||||
host: "1.1.1.1".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
let info = mock_info("gateway-owner", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "myAwesomeGateway".to_string(),
|
||||
host: "2.2.2.2".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
let res = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(Err(ContractError::AlreadyOwnsGateway), res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_remove() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
// try unbond when no nodes exist yet
|
||||
let info = mock_info("anyone", &[]);
|
||||
let msg = ExecuteMsg::UnbondGateway {};
|
||||
let result = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
|
||||
// we're told that there is no node for our address
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::NoAssociatedGatewayBond {
|
||||
owner: Addr::unchecked("anyone")
|
||||
})
|
||||
);
|
||||
|
||||
// let's add a node owned by bob
|
||||
test_helpers::add_gateway("bob", test_helpers::good_gateway_bond(), &mut deps);
|
||||
|
||||
// attempt to unbond fred's node, which doesn't exist
|
||||
let info = mock_info("fred", &[]);
|
||||
let msg = ExecuteMsg::UnbondGateway {};
|
||||
let result = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::NoAssociatedGatewayBond {
|
||||
owner: Addr::unchecked("fred")
|
||||
})
|
||||
);
|
||||
|
||||
// bob's node is still there
|
||||
let nodes = test_helpers::get_gateways(&mut deps);
|
||||
assert_eq!(1, nodes.len());
|
||||
|
||||
let first_node = &nodes[0];
|
||||
assert_eq!(&Addr::unchecked("bob"), first_node.owner());
|
||||
|
||||
// add a node owned by fred
|
||||
let info = mock_info("fred", &test_helpers::good_gateway_bond());
|
||||
try_add_gateway(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
info,
|
||||
Gateway {
|
||||
identity_key: "fredsgateway".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// let's make sure we now have 2 nodes:
|
||||
assert_eq!(2, test_helpers::get_gateways(&mut deps).len());
|
||||
|
||||
// unbond fred's node
|
||||
let info = mock_info("fred", &[]);
|
||||
let msg = ExecuteMsg::UnbondGateway {};
|
||||
let remove_fred = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
|
||||
|
||||
// we should see log messages come back showing an unbond message
|
||||
let expected_attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr("address", "fred"),
|
||||
attr(
|
||||
"gateway_bond",
|
||||
format!(
|
||||
"amount: {} {}, owner: fred, identity: fredsgateway",
|
||||
INITIAL_GATEWAY_BOND, DENOM
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
// we should see a funds transfer from the contract back to fred
|
||||
let expected_messages = vec![BankMsg::Send {
|
||||
to_address: String::from(info.sender),
|
||||
amount: test_helpers::good_gateway_bond(),
|
||||
}
|
||||
.into()];
|
||||
|
||||
// run the executer and check that we got back the correct results
|
||||
let expected = Response {
|
||||
submessages: Vec::new(),
|
||||
messages: expected_messages,
|
||||
attributes: expected_attributes,
|
||||
data: None,
|
||||
};
|
||||
assert_eq!(remove_fred, expected);
|
||||
|
||||
// only 1 node now exists, owned by bob:
|
||||
let gateway_bonds = test_helpers::get_gateways(&mut deps);
|
||||
assert_eq!(1, gateway_bonds.len());
|
||||
assert_eq!(&Addr::unchecked("bob"), gateway_bonds[0].owner());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_gateway_clears_ownership() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let info = mock_info("gateway-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "myAwesomeGateway".to_string(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(
|
||||
"myAwesomeGateway",
|
||||
storage::gateways_owners_read(deps.as_ref().storage)
|
||||
.load("gateway-owner".as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let info = mock_info("gateway-owner", &[]);
|
||||
let msg = ExecuteMsg::UnbondGateway {};
|
||||
|
||||
assert!(execute(deps.as_mut(), mock_env(), info, msg).is_ok());
|
||||
|
||||
assert!(storage::gateways_owners_read(deps.as_ref().storage)
|
||||
.may_load("gateway-owner".as_bytes())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// and since it's removed, it can be reclaimed
|
||||
let info = mock_info("gateway-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "myAwesomeGateway".to_string(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(execute(deps.as_mut(), mock_env(), info, msg).is_ok());
|
||||
assert_eq!(
|
||||
"myAwesomeGateway",
|
||||
storage::gateways_owners_read(deps.as_ref().storage)
|
||||
.load("gateway-owner".as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validating_gateway_bond() {
|
||||
// you must send SOME funds
|
||||
let result = validate_gateway_bond(&[], INITIAL_GATEWAY_BOND);
|
||||
assert_eq!(result, Err(ContractError::NoBondFound));
|
||||
|
||||
// you must send at least 100 coins...
|
||||
let mut bond = test_helpers::good_gateway_bond();
|
||||
bond[0].amount = INITIAL_GATEWAY_BOND.checked_sub(Uint128(1)).unwrap();
|
||||
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientGatewayBond {
|
||||
received: Into::<u128>::into(INITIAL_GATEWAY_BOND) - 1,
|
||||
minimum: INITIAL_GATEWAY_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// more than that is still fine
|
||||
let mut bond = test_helpers::good_gateway_bond();
|
||||
bond[0].amount = INITIAL_GATEWAY_BOND + Uint128(1);
|
||||
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// it must be sent in the defined denom!
|
||||
let mut bond = test_helpers::good_gateway_bond();
|
||||
bond[0].denom = "baddenom".to_string();
|
||||
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
|
||||
let mut bond = test_helpers::good_gateway_bond();
|
||||
bond[0].denom = "foomp".to_string();
|
||||
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
pub mod contract;
|
||||
pub mod error;
|
||||
pub(crate) mod helpers;
|
||||
pub mod queries;
|
||||
pub mod state;
|
||||
pub(crate) mod storage;
|
||||
pub mod gateways;
|
||||
pub mod mixnet_contract_settings;
|
||||
pub mod mixnodes;
|
||||
pub mod query_support;
|
||||
mod rewards;
|
||||
pub mod support;
|
||||
pub mod transactions;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod models;
|
||||
pub mod queries;
|
||||
pub mod storage;
|
||||
pub mod transactions;
|
||||
+4
-8
@@ -1,16 +1,16 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::{Addr, Decimal};
|
||||
use mixnet_contract::StateParams;
|
||||
use cosmwasm_std::Addr;
|
||||
use mixnet_contract::ContractSettingsParams;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct State {
|
||||
pub struct ContractSettings {
|
||||
pub owner: Addr, // only the owner account can update state
|
||||
pub rewarding_validator_address: Addr,
|
||||
pub params: StateParams,
|
||||
pub params: ContractSettingsParams,
|
||||
|
||||
// keep track of the changes to the current rewarding interval,
|
||||
// i.e. at which block has the latest rewarding occurred
|
||||
@@ -18,8 +18,4 @@ pub struct State {
|
||||
pub rewarding_interval_starting_block: u64,
|
||||
pub latest_rewarding_interval_nonce: u32,
|
||||
pub rewarding_in_progress: bool,
|
||||
|
||||
// helper values to avoid having to recalculate them on every single payment operation
|
||||
pub mixnode_epoch_bond_reward: Decimal, // reward per epoch expressed as a decimal like 0.05
|
||||
pub mixnode_epoch_delegation_reward: Decimal, // reward per epoch expressed as a decimal like 0.05
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use super::storage;
|
||||
use cosmwasm_std::{Deps, StdResult};
|
||||
use mixnet_contract::{ContractSettingsParams, RewardingIntervalResponse};
|
||||
|
||||
pub(crate) fn query_contract_settings_params(deps: Deps) -> StdResult<ContractSettingsParams> {
|
||||
storage::CONTRACT_SETTINGS
|
||||
.load(deps.storage)
|
||||
.map(|settings| settings.params)
|
||||
}
|
||||
|
||||
pub(crate) fn query_rewarding_interval(deps: Deps) -> StdResult<RewardingIntervalResponse> {
|
||||
let state = storage::CONTRACT_SETTINGS.load(deps.storage)?;
|
||||
|
||||
Ok(RewardingIntervalResponse {
|
||||
current_rewarding_interval_starting_block: state.rewarding_interval_starting_block,
|
||||
current_rewarding_interval_nonce: state.latest_rewarding_interval_nonce,
|
||||
rewarding_in_progress: state.rewarding_in_progress,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::mixnet_contract_settings::models::ContractSettings;
|
||||
use crate::support::tests::test_helpers;
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
|
||||
#[test]
|
||||
fn query_for_contract_settings_works() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let dummy_state = ContractSettings {
|
||||
owner: Addr::unchecked("someowner"),
|
||||
rewarding_validator_address: Addr::unchecked("monitor"),
|
||||
params: ContractSettingsParams {
|
||||
minimum_mixnode_bond: 123u128.into(),
|
||||
minimum_gateway_bond: 456u128.into(),
|
||||
mixnode_rewarded_set_size: 1000,
|
||||
mixnode_active_set_size: 500,
|
||||
},
|
||||
rewarding_interval_starting_block: 123,
|
||||
latest_rewarding_interval_nonce: 0,
|
||||
rewarding_in_progress: false,
|
||||
};
|
||||
|
||||
storage::CONTRACT_SETTINGS
|
||||
.save(deps.as_mut().storage, &dummy_state)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
dummy_state.params,
|
||||
query_contract_settings_params(deps.as_ref()).unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::mixnet_contract_settings::models::ContractSettings;
|
||||
use cosmwasm_std::StdResult;
|
||||
use cosmwasm_std::Storage;
|
||||
use cw_storage_plus::Item;
|
||||
use mixnet_contract::Layer;
|
||||
use mixnet_contract::LayerDistribution;
|
||||
|
||||
pub(crate) const CONTRACT_SETTINGS: Item<ContractSettings> = Item::new("config");
|
||||
pub(crate) const LAYERS: Item<LayerDistribution> = Item::new("layers");
|
||||
|
||||
pub fn increment_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> {
|
||||
LAYERS
|
||||
.update(storage, |mut distribution| {
|
||||
match layer {
|
||||
Layer::Gateway => distribution.gateways += 1,
|
||||
Layer::One => distribution.layer1 += 1,
|
||||
Layer::Two => distribution.layer2 += 1,
|
||||
Layer::Three => distribution.layer3 += 1,
|
||||
}
|
||||
Ok(distribution)
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn decrement_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> {
|
||||
LAYERS
|
||||
.update(storage, |mut distribution| {
|
||||
match layer {
|
||||
Layer::Gateway => {
|
||||
distribution.gateways = distribution
|
||||
.gateways
|
||||
.checked_sub(1)
|
||||
.expect("tried to subtract from unsigned zero!")
|
||||
}
|
||||
Layer::One => {
|
||||
distribution.layer1 = distribution
|
||||
.layer1
|
||||
.checked_sub(1)
|
||||
.expect("tried to subtract from unsigned zero!")
|
||||
}
|
||||
Layer::Two => {
|
||||
distribution.layer2 = distribution
|
||||
.layer2
|
||||
.checked_sub(1)
|
||||
.expect("tried to subtract from unsigned zero!")
|
||||
}
|
||||
Layer::Three => {
|
||||
distribution.layer3 = distribution
|
||||
.layer3
|
||||
.checked_sub(1)
|
||||
.expect("tried to subtract from unsigned zero!")
|
||||
}
|
||||
}
|
||||
Ok(distribution)
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::storage;
|
||||
use crate::error::ContractError;
|
||||
use cosmwasm_std::DepsMut;
|
||||
use cosmwasm_std::MessageInfo;
|
||||
use cosmwasm_std::Response;
|
||||
use mixnet_contract::ContractSettingsParams;
|
||||
|
||||
pub(crate) fn try_update_contract_settings(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
params: ContractSettingsParams,
|
||||
) -> Result<Response, ContractError> {
|
||||
let mut state = storage::CONTRACT_SETTINGS.load(deps.storage)?;
|
||||
|
||||
// check if this is executed by the owner, if not reject the transaction
|
||||
if info.sender != state.owner {
|
||||
return Err(ContractError::Unauthorized);
|
||||
}
|
||||
|
||||
if params.mixnode_rewarded_set_size == 0 {
|
||||
return Err(ContractError::ZeroRewardedSet);
|
||||
}
|
||||
|
||||
if params.mixnode_active_set_size == 0 {
|
||||
return Err(ContractError::ZeroActiveSet);
|
||||
}
|
||||
|
||||
// note: rewarded_set = active_set + idle_set
|
||||
// hence rewarded set must always be bigger than (or equal to) the active set
|
||||
if params.mixnode_rewarded_set_size < params.mixnode_active_set_size {
|
||||
return Err(ContractError::InvalidActiveSetSize);
|
||||
}
|
||||
|
||||
state.params = params;
|
||||
storage::CONTRACT_SETTINGS.save(deps.storage, &state)?;
|
||||
|
||||
// alternative:
|
||||
|
||||
// storage::CONTRACT_SETTINGS.update(deps.storage, |mut state| {
|
||||
// // check if this is executed by the owner, if not reject the transaction
|
||||
// if info.sender != state.owner {
|
||||
// return Err(ContractError::Unauthorized);
|
||||
// }
|
||||
//
|
||||
// if params.mixnode_rewarded_set_size == 0 {
|
||||
// return Err(ContractError::ZeroRewardedSet);
|
||||
// }
|
||||
//
|
||||
// if params.mixnode_active_set_size == 0 {
|
||||
// return Err(ContractError::ZeroActiveSet);
|
||||
// }
|
||||
//
|
||||
// // note: rewarded_set = active_set + idle_set
|
||||
// // hence rewarded set must always be bigger than (or equal to) the active set
|
||||
// if params.mixnode_rewarded_set_size < params.mixnode_active_set_size {
|
||||
// return Err(ContractError::InvalidActiveSetSize);
|
||||
// }
|
||||
//
|
||||
// state.params = params;
|
||||
// Ok(state)
|
||||
// });
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::contract::{INITIAL_GATEWAY_BOND, INITIAL_MIXNODE_BOND};
|
||||
use crate::error::ContractError;
|
||||
use crate::mixnet_contract_settings::transactions::try_update_contract_settings;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::Response;
|
||||
use mixnet_contract::ContractSettingsParams;
|
||||
|
||||
#[test]
|
||||
fn updating_contract_settings() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let new_params = ContractSettingsParams {
|
||||
minimum_mixnode_bond: INITIAL_MIXNODE_BOND,
|
||||
minimum_gateway_bond: INITIAL_GATEWAY_BOND,
|
||||
mixnode_rewarded_set_size: 100,
|
||||
mixnode_active_set_size: 50,
|
||||
};
|
||||
|
||||
// sanity check to ensure new_params are different than the default ones
|
||||
assert_ne!(
|
||||
new_params,
|
||||
storage::CONTRACT_SETTINGS
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap()
|
||||
.params
|
||||
);
|
||||
|
||||
// cannot be updated from non-owner account
|
||||
let info = mock_info("not-the-creator", &[]);
|
||||
let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone());
|
||||
assert_eq!(res, Err(ContractError::Unauthorized));
|
||||
|
||||
// but works fine from the creator account
|
||||
let info = mock_info("creator", &[]);
|
||||
let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone());
|
||||
assert_eq!(res, Ok(Response::default()));
|
||||
|
||||
// and the state is actually updated
|
||||
let current_state = storage::CONTRACT_SETTINGS
|
||||
.load(deps.as_ref().storage)
|
||||
.unwrap();
|
||||
assert_eq!(current_state.params, new_params);
|
||||
|
||||
// error is thrown if rewarded set is smaller than the active set
|
||||
let info = mock_info("creator", &[]);
|
||||
let mut new_params = current_state.params.clone();
|
||||
new_params.mixnode_rewarded_set_size = new_params.mixnode_active_set_size - 1;
|
||||
let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone());
|
||||
assert_eq!(Err(ContractError::InvalidActiveSetSize), res);
|
||||
|
||||
// error is thrown for 0 size rewarded set
|
||||
let info = mock_info("creator", &[]);
|
||||
let mut new_params = current_state.params.clone();
|
||||
new_params.mixnode_rewarded_set_size = 0;
|
||||
let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone());
|
||||
assert_eq!(Err(ContractError::ZeroRewardedSet), res);
|
||||
|
||||
// error is thrown for 0 size active set
|
||||
let info = mock_info("creator", &[]);
|
||||
let mut new_params = current_state.params.clone();
|
||||
new_params.mixnode_active_set_size = 0;
|
||||
let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone());
|
||||
assert_eq!(Err(ContractError::ZeroActiveSet), res);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::storage;
|
||||
use crate::query_support::calculate_start_value;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{coin, Addr, Deps, Order, StdResult};
|
||||
use cw_storage_plus::Bound;
|
||||
use mixnet_contract::{
|
||||
Delegation, IdentityKey, MixNodeBond, MixOwnershipResponse, PagedMixDelegationsResponse,
|
||||
PagedMixnodeResponse,
|
||||
};
|
||||
|
||||
pub fn query_mixnodes_paged(
|
||||
deps: Deps,
|
||||
start_after: Option<IdentityKey>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedMixnodeResponse> {
|
||||
let limit = limit
|
||||
.unwrap_or(storage::BOND_PAGE_DEFAULT_LIMIT)
|
||||
.min(storage::BOND_PAGE_MAX_LIMIT) as usize;
|
||||
|
||||
let start = start_after.map(Bound::exclusive);
|
||||
|
||||
let nodes = storage::mixnodes()
|
||||
.range(deps.storage, start, None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|res| res.map(|item| item.1))
|
||||
.map(|stored_bond| {
|
||||
// I really don't like this additional read per entry, but I don't see an obvious way to remove it
|
||||
stored_bond.map(|stored_bond| {
|
||||
let total_delegation = storage::total_delegation_read(deps.storage)
|
||||
.load(stored_bond.identity().as_bytes());
|
||||
total_delegation
|
||||
.map(|total_delegation| stored_bond.attach_delegation(total_delegation))
|
||||
})
|
||||
})
|
||||
.collect::<StdResult<StdResult<Vec<MixNodeBond>>>>()??;
|
||||
|
||||
let start_next_after = nodes.last().map(|node| node.identity().clone());
|
||||
|
||||
Ok(PagedMixnodeResponse::new(nodes, limit, start_next_after))
|
||||
}
|
||||
|
||||
pub fn query_owns_mixnode(deps: Deps, address: Addr) -> StdResult<MixOwnershipResponse> {
|
||||
let has_node = storage::mixnodes()
|
||||
.idx
|
||||
.owner
|
||||
.item(deps.storage, address.to_string())?
|
||||
.is_some();
|
||||
Ok(MixOwnershipResponse { address, has_node })
|
||||
}
|
||||
|
||||
pub(crate) fn query_mixnode_delegations_paged(
|
||||
deps: Deps,
|
||||
mix_identity: IdentityKey,
|
||||
start_after: Option<Addr>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedMixDelegationsResponse> {
|
||||
let limit = limit
|
||||
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
|
||||
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
|
||||
let start = calculate_start_value(start_after);
|
||||
|
||||
let delegations = storage::mix_delegations_read(deps.storage, &mix_identity)
|
||||
.range(start.as_deref(), None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|res| {
|
||||
res.map(|entry| {
|
||||
Delegation::new(
|
||||
Addr::unchecked(String::from_utf8(entry.0).expect(
|
||||
"Non-UTF8 address used as key in bucket. The storage is corrupted!",
|
||||
)),
|
||||
coin(entry.1.amount.u128(), DENOM),
|
||||
entry.1.block_height,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<StdResult<Vec<Delegation>>>()?;
|
||||
|
||||
let start_next_after = delegations.last().map(|delegation| delegation.owner());
|
||||
|
||||
Ok(PagedMixDelegationsResponse::new(
|
||||
mix_identity,
|
||||
delegations,
|
||||
start_next_after,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
use super::storage;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::Addr;
|
||||
use mixnet_contract::MixNode;
|
||||
|
||||
#[test]
|
||||
fn mixnodes_empty_on_init() {
|
||||
let deps = test_helpers::init_contract();
|
||||
let response = query_mixnodes_paged(deps.as_ref(), None, Option::from(2)).unwrap();
|
||||
assert_eq!(0, response.nodes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnodes_paged_retrieval_obeys_limits() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let limit = 2;
|
||||
for n in 0..10000 {
|
||||
let key = format!("bond{}", n);
|
||||
test_helpers::add_mixnode(&key, test_helpers::good_mixnode_bond(), deps.as_mut());
|
||||
}
|
||||
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(limit)).unwrap();
|
||||
assert_eq!(limit, page1.nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnodes_paged_retrieval_has_default_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
for n in 0..100 {
|
||||
let key = format!("bond{}", n);
|
||||
test_helpers::add_mixnode(&key, test_helpers::good_mixnode_bond(), deps.as_mut());
|
||||
}
|
||||
|
||||
// query without explicitly setting a limit
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, None).unwrap();
|
||||
|
||||
let expected_limit = 50;
|
||||
assert_eq!(expected_limit, page1.nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnodes_paged_retrieval_has_max_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
for n in 0..10000 {
|
||||
let key = format!("bond{}", n);
|
||||
test_helpers::add_mixnode(&key, test_helpers::good_mixnode_bond(), deps.as_mut());
|
||||
}
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000;
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = storage::BOND_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let addr1 = "nym100";
|
||||
let addr2 = "nym101";
|
||||
let addr3 = "nym102";
|
||||
let addr4 = "nym103";
|
||||
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let _identity1 =
|
||||
test_helpers::add_mixnode(&addr1, test_helpers::good_mixnode_bond(), deps.as_mut());
|
||||
|
||||
let per_page = 2;
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.nodes.len());
|
||||
|
||||
// save another
|
||||
let identity2 =
|
||||
test_helpers::add_mixnode(&addr2, test_helpers::good_mixnode_bond(), deps.as_mut());
|
||||
|
||||
// page1 should have 2 results on it
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
|
||||
assert_eq!(2, page1.nodes.len());
|
||||
|
||||
let _identity3 =
|
||||
test_helpers::add_mixnode(&addr3, test_helpers::good_mixnode_bond(), deps.as_mut());
|
||||
|
||||
// page1 still has 2 results
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
|
||||
assert_eq!(2, page1.nodes.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after = identity2.clone();
|
||||
let page2 = query_mixnodes_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.nodes.len());
|
||||
|
||||
// save another one
|
||||
test_helpers::add_mixnode(&addr4, test_helpers::good_mixnode_bond(), deps.as_mut());
|
||||
|
||||
let start_after = identity2;
|
||||
let page2 = query_mixnodes_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.nodes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_for_mixnode_owner_works() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
// "fred" does not own a mixnode if there are no mixnodes
|
||||
let res = query_owns_mixnode(deps.as_ref(), Addr::unchecked("fred")).unwrap();
|
||||
assert!(!res.has_node);
|
||||
|
||||
// mixnode was added to "bob", "fred" still does not own one
|
||||
let node = MixNode {
|
||||
identity_key: "bobsnode".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
};
|
||||
crate::mixnodes::bonding_transactions::try_add_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info("bob", &test_helpers::good_mixnode_bond()),
|
||||
node,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = query_owns_mixnode(deps.as_ref(), Addr::unchecked("fred")).unwrap();
|
||||
assert!(!res.has_node);
|
||||
|
||||
// "fred" now owns a mixnode!
|
||||
let node = MixNode {
|
||||
identity_key: "fredsnode".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
};
|
||||
crate::mixnodes::bonding_transactions::try_add_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info("fred", &test_helpers::good_mixnode_bond()),
|
||||
node,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = query_owns_mixnode(deps.as_ref(), Addr::unchecked("fred")).unwrap();
|
||||
assert!(res.has_node);
|
||||
|
||||
// but after unbonding it, he doesn't own one anymore
|
||||
crate::mixnodes::bonding_transactions::try_remove_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_info("fred", &[]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = query_owns_mixnode(deps.as_ref(), Addr::unchecked("fred")).unwrap();
|
||||
assert!(!res.has_node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::storage;
|
||||
use crate::error::ContractError;
|
||||
use crate::gateways::storage as gateways_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::layer_queries::query_layer_distribution;
|
||||
use crate::mixnodes::storage::StoredMixnodeBond;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{attr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128};
|
||||
use mixnet_contract::MixNode;
|
||||
|
||||
pub(crate) fn try_add_mixnode(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_node: MixNode,
|
||||
) -> Result<Response, ContractError> {
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
|
||||
// if the client has an active bonded gateway, don't allow mixnode bonding
|
||||
if gateways_storage::gateways_owners_read(deps.storage)
|
||||
.may_load(sender_bytes)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ContractError::AlreadyOwnsGateway);
|
||||
}
|
||||
|
||||
// if the client has an active bonded mixnode, regardless of its identity, don't allow bonding
|
||||
if storage::mixnodes()
|
||||
.idx
|
||||
.owner
|
||||
.item(deps.storage, info.sender.to_string())?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ContractError::AlreadyOwnsMixnode);
|
||||
}
|
||||
|
||||
// check if somebody else has already bonded a mixnode with this identity
|
||||
if let Some(existing_bond) =
|
||||
storage::mixnodes().may_load(deps.storage, mix_node.identity_key.as_bytes())?
|
||||
{
|
||||
if existing_bond.owner != info.sender {
|
||||
return Err(ContractError::DuplicateMixnode {
|
||||
owner: existing_bond.owner,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let minimum_bond = mixnet_params_storage::CONTRACT_SETTINGS
|
||||
.load(deps.storage)?
|
||||
.params
|
||||
.minimum_mixnode_bond;
|
||||
validate_mixnode_bond(&info.funds, minimum_bond)?;
|
||||
|
||||
let layer_distribution = query_layer_distribution(deps.as_ref())?;
|
||||
let layer = layer_distribution.choose_with_fewest();
|
||||
|
||||
let stored_bond = StoredMixnodeBond::new(
|
||||
info.funds[0].clone(),
|
||||
info.sender.clone(),
|
||||
layer,
|
||||
env.block.height,
|
||||
mix_node,
|
||||
None,
|
||||
);
|
||||
|
||||
// technically we don't have to set the total_delegation bucket, but it makes things easier
|
||||
// in different places that we can guarantee that if node exists, so does the data behind the total delegation
|
||||
let identity = stored_bond.identity().as_bytes();
|
||||
storage::mixnodes().save(deps.storage, identity, &stored_bond)?;
|
||||
storage::total_delegation(deps.storage).save(identity, &Uint128::zero())?;
|
||||
mixnet_params_storage::increment_layer_count(deps.storage, stored_bond.layer)?;
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
pub(crate) fn try_remove_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
) -> Result<Response, ContractError> {
|
||||
// try to find the node of the sender
|
||||
let (entry_key, mixnode_bond) = match storage::mixnodes()
|
||||
.idx
|
||||
.owner
|
||||
.item(deps.storage, info.sender.to_string())?
|
||||
{
|
||||
Some(node) => (node.0, node.1),
|
||||
None => return Err(ContractError::NoAssociatedMixNodeBond { owner: info.sender }),
|
||||
};
|
||||
|
||||
// send bonded funds back to the bond owner
|
||||
let messages = vec![BankMsg::Send {
|
||||
to_address: info.sender.as_str().to_owned(),
|
||||
amount: vec![mixnode_bond.bond_amount()],
|
||||
}
|
||||
.into()];
|
||||
|
||||
// remove the bond
|
||||
storage::mixnodes().remove(deps.storage, &entry_key)?;
|
||||
|
||||
// decrement layer count
|
||||
mixnet_params_storage::decrement_layer_count(deps.storage, mixnode_bond.layer)?;
|
||||
|
||||
// log our actions
|
||||
let attributes = vec![attr("action", "unbond"), attr("mixnode_bond", mixnode_bond)];
|
||||
|
||||
Ok(Response {
|
||||
submessages: Vec::new(),
|
||||
messages,
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_mixnode_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), ContractError> {
|
||||
// check if anything was put as bond
|
||||
if bond.is_empty() {
|
||||
return Err(ContractError::NoBondFound);
|
||||
}
|
||||
|
||||
if bond.len() > 1 {
|
||||
return Err(ContractError::MultipleDenoms);
|
||||
}
|
||||
|
||||
// check that the denomination is correct
|
||||
if bond[0].denom != DENOM {
|
||||
return Err(ContractError::WrongDenom {});
|
||||
}
|
||||
|
||||
// check that we have at least MIXNODE_BOND coins in our bond
|
||||
if bond[0].amount < minimum_bond {
|
||||
return Err(ContractError::InsufficientMixNodeBond {
|
||||
received: bond[0].amount.into(),
|
||||
minimum: minimum_bond.into(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::contract::{execute, query, INITIAL_MIXNODE_BOND};
|
||||
use crate::error::ContractError;
|
||||
use crate::mixnodes::bonding_transactions::try_add_mixnode;
|
||||
use crate::mixnodes::bonding_transactions::validate_mixnode_bond;
|
||||
use crate::support::tests::test_helpers;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::attr;
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::{coins, BankMsg, Response};
|
||||
use cosmwasm_std::{from_binary, Addr, Uint128};
|
||||
use mixnet_contract::Gateway;
|
||||
use mixnet_contract::Layer;
|
||||
use mixnet_contract::MixNode;
|
||||
use mixnet_contract::{ExecuteMsg, LayerDistribution, PagedMixnodeResponse, QueryMsg};
|
||||
|
||||
#[test]
|
||||
fn mixnode_add() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
// if we don't send enough funds
|
||||
let insufficient_bond = Into::<u128>::into(INITIAL_MIXNODE_BOND) - 1;
|
||||
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "anyonesmixnode".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
// we are informed that we didn't send enough funds
|
||||
let result = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientMixNodeBond {
|
||||
received: insufficient_bond,
|
||||
minimum: INITIAL_MIXNODE_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// no mixnode was inserted into the topology
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedMixnodeResponse = from_binary(&res).unwrap();
|
||||
assert_eq!(0, page.nodes.len());
|
||||
|
||||
// if we send enough funds
|
||||
let info = mock_info("anyone", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "anyonesmixnode".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
// we get back a message telling us everything was OK
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(execute_response.is_ok());
|
||||
|
||||
// we can query topology and the new node is there
|
||||
let query_response = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedMixnodeResponse = from_binary(&query_response).unwrap();
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!(
|
||||
&MixNode {
|
||||
identity_key: "anyonesmixnode".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
page.nodes[0].mix_node()
|
||||
);
|
||||
|
||||
// if there was already a mixnode bonded by particular user
|
||||
let info = mock_info("foomper", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "foompermixnode".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
let info = mock_info("foomper", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "foompermixnode".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
// it fails
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(Err(ContractError::AlreadyOwnsMixnode), execute_response);
|
||||
|
||||
// bonding fails if the user already owns a gateway
|
||||
let info = mock_info("gateway-owner", &test_helpers::good_gateway_bond());
|
||||
let msg = ExecuteMsg::BondGateway {
|
||||
gateway: Gateway {
|
||||
identity_key: "ownersgateway".into(),
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
};
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
let info = mock_info("gateway-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "ownersmixnode".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(execute_response, Err(ContractError::AlreadyOwnsGateway));
|
||||
|
||||
// but after he unbonds it, it's all fine again
|
||||
let info = mock_info("gateway-owner", &[]);
|
||||
let msg = ExecuteMsg::UnbondGateway {};
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
let info = mock_info("gateway-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "ownersmixnode".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(execute_response.is_ok());
|
||||
|
||||
// adding another node from another account, but with the same IP, should fail (or we would have a weird state). Is that right? Think about this, not sure yet.
|
||||
// if we attempt to register a second node from the same address, should we get an error? It would probably be polite.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_mixnode_without_existing_owner() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let info = mock_info("mix-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "myAwesomeMixnode".to_string(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
// before the execution the node had no associated owner
|
||||
assert!(storage::mixnodes()
|
||||
.idx
|
||||
.owner
|
||||
.item(deps.as_ref().storage, "mix-owner".to_string())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// it's all fine, owner is saved
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert!(execute_response.is_ok());
|
||||
|
||||
assert_eq!(
|
||||
"myAwesomeMixnode",
|
||||
storage::mixnodes()
|
||||
.idx
|
||||
.owner
|
||||
.item(deps.as_ref().storage, "mix-owner".to_string())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.1
|
||||
.identity()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_mixnode_with_existing_owner() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let info = mock_info("mix-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "myAwesomeMixnode".to_string(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
// request fails giving the existing owner address in the message
|
||||
let info = mock_info("mix-owner-pretender", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "myAwesomeMixnode".to_string(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
let execute_response = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
Err(ContractError::DuplicateMixnode {
|
||||
owner: Addr::unchecked("mix-owner")
|
||||
}),
|
||||
execute_response
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_mixnode_with_existing_unchanged_owner() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let info = mock_info("mix-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "myAwesomeMixnode".to_string(),
|
||||
host: "1.1.1.1:1789".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
|
||||
let info = mock_info("mix-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "myAwesomeMixnode".to_string(),
|
||||
host: "2.2.2.2:1789".into(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
let res = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(Err(ContractError::AlreadyOwnsMixnode), res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_mixnode_updates_layer_distribution() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
assert_eq!(
|
||||
LayerDistribution::default(),
|
||||
mixnet_params_storage::LAYERS.load(&deps.storage).unwrap(),
|
||||
);
|
||||
|
||||
let info = mock_info("mix-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "mix1".to_string(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(
|
||||
LayerDistribution {
|
||||
layer1: 1,
|
||||
..Default::default()
|
||||
},
|
||||
mixnet_params_storage::LAYERS.load(&deps.storage).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_remove() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
// try un-registering when no nodes exist yet
|
||||
let info = mock_info("anyone", &[]);
|
||||
let msg = ExecuteMsg::UnbondMixnode {};
|
||||
let result = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
|
||||
// we're told that there is no node for our address
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::NoAssociatedMixNodeBond {
|
||||
owner: Addr::unchecked("anyone")
|
||||
})
|
||||
);
|
||||
|
||||
// let's add a node owned by bob
|
||||
test_helpers::add_mixnode("bob", test_helpers::good_mixnode_bond(), deps.as_mut());
|
||||
|
||||
// attempt to un-register fred's node, which doesn't exist
|
||||
let info = mock_info("fred", &[]);
|
||||
let msg = ExecuteMsg::UnbondMixnode {};
|
||||
let result = execute(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::NoAssociatedMixNodeBond {
|
||||
owner: Addr::unchecked("fred")
|
||||
})
|
||||
);
|
||||
|
||||
// bob's node is still there
|
||||
let nodes = test_helpers::get_mix_nodes(&mut deps);
|
||||
assert_eq!(1, nodes.len());
|
||||
assert_eq!("bob", nodes[0].owner().clone());
|
||||
|
||||
// add a node owned by fred
|
||||
let info = mock_info("fred", &test_helpers::good_mixnode_bond());
|
||||
try_add_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
info,
|
||||
MixNode {
|
||||
identity_key: "fredsmixnode".to_string(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// let's make sure we now have 2 nodes:
|
||||
assert_eq!(2, test_helpers::get_mix_nodes(&mut deps).len());
|
||||
|
||||
// un-register fred's node
|
||||
let info = mock_info("fred", &[]);
|
||||
let msg = ExecuteMsg::UnbondMixnode {};
|
||||
let remove_fred = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
|
||||
|
||||
// we should see log messages come back showing an unbond message
|
||||
let expected_attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr(
|
||||
"mixnode_bond",
|
||||
format!(
|
||||
"amount: {}{}, owner: fred, identity: fredsmixnode",
|
||||
INITIAL_MIXNODE_BOND, DENOM
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
// we should see a funds transfer from the contract back to fred
|
||||
let expected_messages = vec![BankMsg::Send {
|
||||
to_address: String::from(info.sender),
|
||||
amount: test_helpers::good_mixnode_bond(),
|
||||
}
|
||||
.into()];
|
||||
|
||||
// run the executor and check that we got back the correct results
|
||||
let expected = Response {
|
||||
submessages: Vec::new(),
|
||||
messages: expected_messages,
|
||||
attributes: expected_attributes,
|
||||
data: None,
|
||||
};
|
||||
assert_eq!(remove_fred, expected);
|
||||
|
||||
// only 1 node now exists, owned by bob:
|
||||
let mix_node_bonds = test_helpers::get_mix_nodes(&mut deps);
|
||||
assert_eq!(1, mix_node_bonds.len());
|
||||
assert_eq!(&Addr::unchecked("bob"), mix_node_bonds[0].owner());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_mixnode_clears_ownership() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let info = mock_info("mix-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "myAwesomeMixnode".to_string(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(
|
||||
"myAwesomeMixnode",
|
||||
storage::mixnodes()
|
||||
.idx
|
||||
.owner
|
||||
.item(deps.as_ref().storage, "mix-owner".to_string())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.1
|
||||
.identity()
|
||||
);
|
||||
|
||||
let info = mock_info("mix-owner", &[]);
|
||||
let msg = ExecuteMsg::UnbondMixnode {};
|
||||
|
||||
assert!(execute(deps.as_mut(), mock_env(), info, msg).is_ok());
|
||||
|
||||
assert!(storage::mixnodes()
|
||||
.idx
|
||||
.owner
|
||||
.item(deps.as_ref().storage, "mix-owner".to_string())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// and since it's removed, it can be reclaimed
|
||||
let info = mock_info("mix-owner", &test_helpers::good_mixnode_bond());
|
||||
let msg = ExecuteMsg::BondMixnode {
|
||||
mix_node: MixNode {
|
||||
identity_key: "myAwesomeMixnode".to_string(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(execute(deps.as_mut(), mock_env(), info, msg).is_ok());
|
||||
assert_eq!(
|
||||
"myAwesomeMixnode",
|
||||
storage::mixnodes()
|
||||
.idx
|
||||
.owner
|
||||
.item(deps.as_ref().storage, "mix-owner".to_string())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.1
|
||||
.identity()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validating_mixnode_bond() {
|
||||
// you must send SOME funds
|
||||
let result = validate_mixnode_bond(&[], INITIAL_MIXNODE_BOND);
|
||||
assert_eq!(result, Err(ContractError::NoBondFound));
|
||||
|
||||
// you must send at least 100 coins...
|
||||
let mut bond = test_helpers::good_mixnode_bond();
|
||||
bond[0].amount = INITIAL_MIXNODE_BOND.checked_sub(Uint128(1)).unwrap();
|
||||
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(ContractError::InsufficientMixNodeBond {
|
||||
received: Into::<u128>::into(INITIAL_MIXNODE_BOND) - 1,
|
||||
minimum: INITIAL_MIXNODE_BOND.into(),
|
||||
})
|
||||
);
|
||||
|
||||
// more than that is still fine
|
||||
let mut bond = test_helpers::good_mixnode_bond();
|
||||
bond[0].amount = INITIAL_MIXNODE_BOND + Uint128(1);
|
||||
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// it must be sent in the defined denom!
|
||||
let mut bond = test_helpers::good_mixnode_bond();
|
||||
bond[0].denom = "baddenom".to_string();
|
||||
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
|
||||
let mut bond = test_helpers::good_mixnode_bond();
|
||||
bond[0].denom = "foomp".to_string();
|
||||
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
|
||||
assert_eq!(result, Err(ContractError::WrongDenom {}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choose_layer_mix_node() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
for owner in ["alice", "bob"] {
|
||||
try_add_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(owner, &test_helpers::good_mixnode_bond()),
|
||||
MixNode {
|
||||
identity_key: owner.to_string(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let bonded_mix_nodes = test_helpers::get_mix_nodes(&mut deps);
|
||||
let alice_node = bonded_mix_nodes.get(0).unwrap().clone();
|
||||
let bob_node = bonded_mix_nodes.get(1).unwrap().clone();
|
||||
assert_eq!(alice_node.mix_node.identity_key, "alice");
|
||||
assert_eq!(alice_node.layer, Layer::One);
|
||||
assert_eq!(bob_node.mix_node.identity_key, "bob");
|
||||
assert_eq!(bob_node.layer, mixnet_contract::Layer::Two);
|
||||
}
|
||||
}
|
||||
+81
-201
@@ -1,120 +1,13 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::ContractError;
|
||||
use crate::transactions::OLD_DELEGATIONS_CHUNK_SIZE;
|
||||
use cosmwasm_std::{Decimal, Order, StdError, StdResult, Uint128};
|
||||
use cosmwasm_std::{Addr, Order, StdError, StdResult};
|
||||
use cosmwasm_storage::ReadonlyBucket;
|
||||
use mixnet_contract::{Addr, IdentityKey, PagedAllDelegationsResponse, UnpackedDelegation};
|
||||
use mixnet_contract::IdentityKey;
|
||||
use mixnet_contract::PagedAllDelegationsResponse;
|
||||
use mixnet_contract::UnpackedDelegation;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::ops::Sub;
|
||||
|
||||
// for time being completely ignore concept of a leap year and assume each year is exactly 365 days
|
||||
// i.e. 8760 hours
|
||||
const HOURS_IN_YEAR: u128 = 8760;
|
||||
|
||||
const DECIMAL_FRACTIONAL: Uint128 = Uint128(1_000_000_000_000_000_000u128);
|
||||
|
||||
// cosmwasm bucket internal value
|
||||
const NAMESPACE_LENGTH: usize = 2;
|
||||
|
||||
pub fn decimal_to_uint128(value: Decimal) -> Uint128 {
|
||||
value * DECIMAL_FRACTIONAL
|
||||
}
|
||||
|
||||
pub fn uint128_to_decimal(value: Uint128) -> Decimal {
|
||||
Decimal::from_ratio(value, DECIMAL_FRACTIONAL)
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_epoch_reward_rate(
|
||||
epoch_length: u32,
|
||||
annual_reward_rate: Decimal,
|
||||
) -> Decimal {
|
||||
// this is more of a sanity check as the contract does not allow setting annual reward rates
|
||||
// to be lower than 1.
|
||||
debug_assert!(annual_reward_rate >= Decimal::one());
|
||||
|
||||
// converts reward rate, like 1.25 into the expected gain, like 0.25
|
||||
let annual_reward = annual_reward_rate.sub(Decimal::one());
|
||||
// do a simple cross-multiplication:
|
||||
// `annual_reward` - `HOURS_IN_YEAR`
|
||||
// x - `epoch_length`
|
||||
//
|
||||
// x = `annual_reward` * `epoch_length` / `HOURS_IN_YEAR`
|
||||
|
||||
// converts reward, like 0.25 into 250000000000000000
|
||||
let annual_reward_uint128 = decimal_to_uint128(annual_reward);
|
||||
|
||||
// calculates `annual_reward_uint128` * `epoch_length` / `HOURS_IN_YEAR`
|
||||
let epoch_reward_uint128 = annual_reward_uint128.multiply_ratio(epoch_length, HOURS_IN_YEAR);
|
||||
|
||||
// note: this returns a % reward, like 0.05 rather than reward rate (like 1.05)
|
||||
uint128_to_decimal(epoch_reward_uint128)
|
||||
}
|
||||
|
||||
pub(crate) fn scale_reward_by_uptime(
|
||||
reward: Decimal,
|
||||
uptime: u32,
|
||||
) -> Result<Decimal, ContractError> {
|
||||
if uptime > 100 {
|
||||
return Err(ContractError::UnexpectedUptime);
|
||||
}
|
||||
let uptime_ratio = Decimal::from_ratio(uptime, 100u128);
|
||||
// if we do not convert into a more precise representation, we might end up with, for example,
|
||||
// reward 0.05 and uptime of 50% which would produce 0.50 * 0.05 = 0 (because of u128 representation)
|
||||
// and also the above would be impossible to compute as Mul<Decimal> for Decimal is not implemented
|
||||
//
|
||||
// but with the intermediate conversion, we would have
|
||||
// 0.50 * 50_000_000_000_000_000 = 25_000_000_000_000_000
|
||||
// which converted back would give us the proper 0.025
|
||||
let uptime_ratio_u128 = decimal_to_uint128(uptime_ratio);
|
||||
let scaled = reward * uptime_ratio_u128;
|
||||
Ok(uint128_to_decimal(scaled))
|
||||
}
|
||||
|
||||
// Extracts the node identity and owner of a delegation from the bytes used as
|
||||
// key in the delegation buckets.
|
||||
fn extract_identity_and_owner(bytes: Vec<u8>) -> StdResult<(Addr, IdentityKey)> {
|
||||
if bytes.len() < NAMESPACE_LENGTH {
|
||||
return Err(StdError::parse_err(
|
||||
"mixnet_contract::types::IdentityKey",
|
||||
"Invalid type",
|
||||
));
|
||||
}
|
||||
let identity_size = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
|
||||
let identity_bytes: Vec<u8> = bytes
|
||||
.iter()
|
||||
.skip(NAMESPACE_LENGTH)
|
||||
.take(identity_size)
|
||||
.copied()
|
||||
.collect();
|
||||
let identity = IdentityKey::from_utf8(identity_bytes)
|
||||
.map_err(|_| StdError::parse_err("mixnet_contract::types::IdentityKey", "Invalid type"))?;
|
||||
let owner_bytes: Vec<u8> = bytes
|
||||
.iter()
|
||||
.skip(NAMESPACE_LENGTH + identity_size)
|
||||
.copied()
|
||||
.collect();
|
||||
let owner = Addr::unchecked(
|
||||
String::from_utf8(owner_bytes)
|
||||
.map_err(|_| StdError::parse_err("cosmwasm_std::addresses::Addr", "Invalid type"))?,
|
||||
);
|
||||
|
||||
Ok((owner, identity))
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
// Converts the node identity and owner of a delegation into the bytes used as
|
||||
// key in the delegation buckets.
|
||||
pub(crate) fn identity_and_owner_to_bytes(identity: &IdentityKey, owner: &Addr) -> Vec<u8> {
|
||||
let mut bytes = u16::to_be_bytes(identity.len() as u16).to_vec();
|
||||
bytes.append(&mut identity.as_bytes().to_vec());
|
||||
bytes.append(&mut owner.as_bytes().to_vec());
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_delegations_paged<T>(
|
||||
bucket: &ReadonlyBucket<T>,
|
||||
@@ -153,6 +46,44 @@ where
|
||||
))
|
||||
}
|
||||
|
||||
// Extracts the node identity and owner of a delegation from the bytes used as
|
||||
// key in the delegation buckets.
|
||||
fn extract_identity_and_owner(bytes: Vec<u8>) -> StdResult<(Addr, IdentityKey)> {
|
||||
// cosmwasm bucket internal value
|
||||
const NAMESPACE_LENGTH: usize = 2;
|
||||
|
||||
if bytes.len() < NAMESPACE_LENGTH {
|
||||
return Err(StdError::parse_err(
|
||||
"mixnet_contract::types::IdentityKey",
|
||||
"Invalid type",
|
||||
));
|
||||
}
|
||||
let identity_size = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
|
||||
let identity_bytes: Vec<u8> = bytes
|
||||
.iter()
|
||||
.skip(NAMESPACE_LENGTH)
|
||||
.take(identity_size)
|
||||
.copied()
|
||||
.collect();
|
||||
let identity = IdentityKey::from_utf8(identity_bytes)
|
||||
.map_err(|_| StdError::parse_err("mixnet_contract::types::IdentityKey", "Invalid type"))?;
|
||||
let owner_bytes: Vec<u8> = bytes
|
||||
.iter()
|
||||
.skip(NAMESPACE_LENGTH + identity_size)
|
||||
.copied()
|
||||
.collect();
|
||||
let owner = Addr::unchecked(
|
||||
String::from_utf8(owner_bytes)
|
||||
.map_err(|_| StdError::parse_err("cosmwasm_std::addresses::Addr", "Invalid type"))?,
|
||||
);
|
||||
|
||||
Ok((owner, identity))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) const OLD_DELEGATIONS_CHUNK_SIZE: usize = 500;
|
||||
|
||||
#[cfg(test)]
|
||||
pub struct Delegations<'a, T: Clone + Serialize + DeserializeOwned> {
|
||||
delegations_bucket: ReadonlyBucket<'a, T>,
|
||||
curr_delegations: Vec<UnpackedDelegation<T>>,
|
||||
@@ -161,6 +92,7 @@ pub struct Delegations<'a, T: Clone + Serialize + DeserializeOwned> {
|
||||
last_page: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<'a, T: Clone + Serialize + DeserializeOwned> Delegations<'a, T> {
|
||||
pub fn new(delegations_bucket: ReadonlyBucket<'a, T>) -> Self {
|
||||
Delegations {
|
||||
@@ -173,6 +105,7 @@ impl<'a, T: Clone + Serialize + DeserializeOwned> Delegations<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<'a, T: Clone + Serialize + DeserializeOwned> Iterator for Delegations<'a, T> {
|
||||
type Item = UnpackedDelegation<T>;
|
||||
|
||||
@@ -208,16 +141,38 @@ impl<'a, T: Clone + Serialize + DeserializeOwned> Iterator for Delegations<'a, T
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::queries::tests::store_n_mix_delegations;
|
||||
use crate::storage::{all_mix_delegations_read, mix_delegations};
|
||||
use crate::support::tests::helpers;
|
||||
use crate::mixnodes::delegation_queries::tests::store_n_mix_delegations;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::support::tests::test_helpers;
|
||||
use crate::support::tests::test_helpers::identity_and_owner_to_bytes;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use mixnet_contract::RawDelegationData;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn identity_and_owner_serialization() {
|
||||
let identity: IdentityKey = "gateway".into();
|
||||
let owner = Addr::unchecked("bob");
|
||||
assert_eq!(
|
||||
vec![0, 7, 103, 97, 116, 101, 119, 97, 121, 98, 111, 98],
|
||||
identity_and_owner_to_bytes(&identity, &owner)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_and_owner_deserialization() {
|
||||
assert!(extract_identity_and_owner(vec![]).is_err());
|
||||
assert!(extract_identity_and_owner(vec![0]).is_err());
|
||||
let (owner, identity) = extract_identity_and_owner(vec![
|
||||
0, 7, 109, 105, 120, 110, 111, 100, 101, 97, 108, 105, 99, 101,
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(owner, "alice");
|
||||
assert_eq!(identity, "mixnode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delegations_iterator() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
|
||||
store_n_mix_delegations(
|
||||
@@ -225,7 +180,8 @@ mod tests {
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
let mix_bucket = all_mix_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let mix_bucket =
|
||||
mixnodes_storage::all_mix_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let mut delegations = Delegations::new(mix_bucket);
|
||||
assert!(delegations.curr_delegations.is_empty());
|
||||
assert_eq!(delegations.curr_index, OLD_DELEGATIONS_CHUNK_SIZE);
|
||||
@@ -249,83 +205,6 @@ mod tests {
|
||||
assert!(delegations.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculating_epoch_reward_rate() {
|
||||
// 1.10
|
||||
let annual_reward_rate = Decimal::from_ratio(110u128, 100u128);
|
||||
|
||||
// if the epoch is (for some reason) exactly one year,
|
||||
// the reward rate should be unchanged
|
||||
let per_epoch_rate = calculate_epoch_reward_rate(HOURS_IN_YEAR as u32, annual_reward_rate);
|
||||
// 0.10
|
||||
let expected = annual_reward_rate.sub(Decimal::one());
|
||||
assert_eq!(expected, per_epoch_rate);
|
||||
|
||||
// 24 hours
|
||||
let per_epoch_rate = calculate_epoch_reward_rate(24, annual_reward_rate);
|
||||
// 0.1 / 365
|
||||
let expected = Decimal::from_ratio(1u128, 3650u128);
|
||||
assert_eq!(expected, per_epoch_rate);
|
||||
|
||||
let expected_per_epoch_rate_excel = Decimal::from_str("0.000273972602739726").unwrap();
|
||||
assert_eq!(expected_per_epoch_rate_excel, per_epoch_rate);
|
||||
|
||||
// 1 hour
|
||||
let per_epoch_rate = calculate_epoch_reward_rate(1, annual_reward_rate);
|
||||
// 0.1 / 8760
|
||||
let expected = Decimal::from_ratio(1u128, 87600u128);
|
||||
assert_eq!(expected, per_epoch_rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaling_reward_by_uptime() {
|
||||
// 0.05
|
||||
let epoch_reward = Decimal::from_ratio(5u128, 100u128);
|
||||
|
||||
// scaling by 100 does nothing
|
||||
let scaled = scale_reward_by_uptime(epoch_reward, 100).unwrap();
|
||||
assert_eq!(epoch_reward, scaled);
|
||||
|
||||
// scaling by 0 makes the reward 0
|
||||
let scaled = scale_reward_by_uptime(epoch_reward, 0).unwrap();
|
||||
assert_eq!(Decimal::zero(), scaled);
|
||||
|
||||
// 50 halves it
|
||||
let scaled = scale_reward_by_uptime(epoch_reward, 50).unwrap();
|
||||
let expected = Decimal::from_ratio(25u128, 1000u128);
|
||||
assert_eq!(expected, scaled);
|
||||
|
||||
// 10 takes 1/10th
|
||||
let scaled = scale_reward_by_uptime(epoch_reward, 10).unwrap();
|
||||
let expected = Decimal::from_ratio(5u128, 1000u128);
|
||||
assert_eq!(expected, scaled);
|
||||
|
||||
// anything larger than 100 returns an error
|
||||
assert!(scale_reward_by_uptime(epoch_reward, 101).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_and_owner_deserialization() {
|
||||
assert!(extract_identity_and_owner(vec![]).is_err());
|
||||
assert!(extract_identity_and_owner(vec![0]).is_err());
|
||||
let (owner, identity) = extract_identity_and_owner(vec![
|
||||
0, 7, 109, 105, 120, 110, 111, 100, 101, 97, 108, 105, 99, 101,
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(owner, "alice");
|
||||
assert_eq!(identity, "mixnode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_and_owner_serialization() {
|
||||
let identity: IdentityKey = "gateway".into();
|
||||
let owner = Addr::unchecked("bob");
|
||||
assert_eq!(
|
||||
vec![0, 7, 103, 97, 116, 101, 119, 97, 121, 98, 111, 98],
|
||||
identity_and_owner_to_bytes(&identity, &owner)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_mix_delegations() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
@@ -333,14 +212,14 @@ mod tests {
|
||||
let delegation_owner1 = Addr::unchecked("bar1");
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
let raw_delegation = RawDelegationData::new(1000u128.into(), 42, None);
|
||||
let raw_delegation = RawDelegationData::new(1000u128.into(), 42);
|
||||
let mut start_after = None;
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity1)
|
||||
mixnodes_storage::mix_delegations(&mut deps.storage, &node_identity1)
|
||||
.save(delegation_owner1.as_bytes(), &raw_delegation)
|
||||
.unwrap();
|
||||
|
||||
let bucket = all_mix_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let bucket = mixnodes_storage::all_mix_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let response =
|
||||
get_all_delegations_paged::<RawDelegationData>(&bucket, &start_after, 10).unwrap();
|
||||
start_after = response.start_next_after;
|
||||
@@ -355,11 +234,11 @@ mod tests {
|
||||
)
|
||||
);
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity2)
|
||||
mixnodes_storage::mix_delegations(&mut deps.storage, &node_identity2)
|
||||
.save(delegation_owner2.as_bytes(), &raw_delegation)
|
||||
.unwrap();
|
||||
|
||||
let bucket = all_mix_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let bucket = mixnodes_storage::all_mix_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let response =
|
||||
get_all_delegations_paged::<RawDelegationData>(&bucket, &start_after, 10).unwrap();
|
||||
start_after = response.start_next_after;
|
||||
@@ -374,9 +253,10 @@ mod tests {
|
||||
)
|
||||
);
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity1).remove(delegation_owner1.as_bytes());
|
||||
mixnodes_storage::mix_delegations(&mut deps.storage, &node_identity1)
|
||||
.remove(delegation_owner1.as_bytes());
|
||||
|
||||
let bucket = all_mix_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let bucket = mixnodes_storage::all_mix_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let response =
|
||||
get_all_delegations_paged::<RawDelegationData>(&bucket, &start_after, 10).unwrap();
|
||||
let delegations = response.delegations;
|
||||
@@ -0,0 +1,618 @@
|
||||
use super::delegation_helpers;
|
||||
use super::storage;
|
||||
use crate::error::ContractError;
|
||||
use crate::query_support::calculate_start_value;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::coin;
|
||||
use cosmwasm_std::Addr;
|
||||
use cosmwasm_std::Deps;
|
||||
use cosmwasm_std::Order;
|
||||
use cosmwasm_std::StdResult;
|
||||
use mixnet_contract::Delegation;
|
||||
use mixnet_contract::IdentityKey;
|
||||
use mixnet_contract::PagedAllDelegationsResponse;
|
||||
use mixnet_contract::PagedReverseMixDelegationsResponse;
|
||||
use mixnet_contract::RawDelegationData;
|
||||
|
||||
pub(crate) fn query_all_mixnode_delegations_paged(
|
||||
deps: Deps,
|
||||
start_after: Option<Vec<u8>>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedAllDelegationsResponse<RawDelegationData>> {
|
||||
let limit = limit
|
||||
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
|
||||
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
|
||||
|
||||
let bucket = storage::all_mix_delegations_read::<RawDelegationData>(deps.storage);
|
||||
let start = start_after.map(|mut v| {
|
||||
v.push(0);
|
||||
v
|
||||
});
|
||||
delegation_helpers::get_all_delegations_paged::<RawDelegationData>(&bucket, &start, limit)
|
||||
}
|
||||
|
||||
pub(crate) fn query_reverse_mixnode_delegations_paged(
|
||||
deps: Deps,
|
||||
delegation_owner: Addr,
|
||||
start_after: Option<IdentityKey>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedReverseMixDelegationsResponse> {
|
||||
let limit = limit
|
||||
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
|
||||
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
|
||||
let start = calculate_start_value(start_after);
|
||||
|
||||
let delegations = storage::reverse_mix_delegations_read(deps.storage, &delegation_owner)
|
||||
.range(start.as_deref(), None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|res| {
|
||||
res.map(|entry| {
|
||||
String::from_utf8(entry.0)
|
||||
.expect("Non-UTF8 address used as key in bucket. The storage is corrupted!")
|
||||
})
|
||||
})
|
||||
.collect::<StdResult<Vec<IdentityKey>>>()?;
|
||||
|
||||
let start_next_after = delegations.last().cloned();
|
||||
|
||||
Ok(PagedReverseMixDelegationsResponse::new(
|
||||
delegation_owner,
|
||||
delegations,
|
||||
start_next_after,
|
||||
))
|
||||
}
|
||||
|
||||
// queries for delegation value of given address for particular node
|
||||
pub(crate) fn query_mixnode_delegation(
|
||||
deps: Deps,
|
||||
mix_identity: IdentityKey,
|
||||
address: Addr,
|
||||
) -> Result<Delegation, ContractError> {
|
||||
match storage::mix_delegations_read(deps.storage, &mix_identity).may_load(address.as_bytes())? {
|
||||
Some(delegation_value) => Ok(Delegation::new(
|
||||
address,
|
||||
coin(delegation_value.amount.u128(), DENOM),
|
||||
delegation_value.block_height,
|
||||
)),
|
||||
None => Err(ContractError::NoMixnodeDelegationFound {
|
||||
identity: mix_identity,
|
||||
address,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::{Addr, Storage};
|
||||
use storage::mix_delegations;
|
||||
|
||||
pub fn store_n_mix_delegations(n: u32, storage: &mut dyn Storage, node_identity: &str) {
|
||||
for i in 0..n {
|
||||
let address = format!("address{}", i);
|
||||
mix_delegations(storage, node_identity)
|
||||
.save(
|
||||
address.as_bytes(),
|
||||
&test_helpers::raw_delegation_fixture(42),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod querying_for_mixnode_delegations_paged {
|
||||
use super::*;
|
||||
use crate::mixnodes::bonding_queries::query_mixnode_delegations_paged;
|
||||
use mixnet_contract::IdentityKey;
|
||||
|
||||
#[test]
|
||||
fn retrieval_obeys_limits() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let limit = 2;
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_mix_delegations(100, &mut deps.storage, &node_identity);
|
||||
|
||||
let page1 = query_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity,
|
||||
None,
|
||||
Option::from(limit),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(limit, page1.delegations.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_default_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_mix_delegations(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
let page1 =
|
||||
query_mixnode_delegations_paged(deps.as_ref(), node_identity, None, None).unwrap();
|
||||
assert_eq!(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT,
|
||||
page1.delegations.len() as u32
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_max_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_mix_delegations(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT;
|
||||
let page1 = query_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity,
|
||||
None,
|
||||
Option::from(crazy_limit),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.delegations.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save("1".as_bytes(), &test_helpers::raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
let per_page = 2;
|
||||
let page1 = query_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.delegations.len());
|
||||
|
||||
// save another
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save("2".as_bytes(), &test_helpers::raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
// page1 should have 2 results on it
|
||||
let page1 = query_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegations.len());
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save("3".as_bytes(), &test_helpers::raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
// page1 still has 2 results
|
||||
let page1 = query_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegations.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after = Addr::unchecked("2");
|
||||
let page2 = query_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity.clone(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.delegations.len());
|
||||
|
||||
// save another one
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save("4".as_bytes(), &test_helpers::raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
let start_after = Addr::unchecked("2");
|
||||
let page2 = query_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
node_identity,
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.delegations.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod querying_for_all_mixnode_delegations_paged {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers;
|
||||
use mixnet_contract::IdentityKey;
|
||||
|
||||
#[test]
|
||||
fn retrieval_obeys_limits() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let limit = 2;
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_mix_delegations(100, &mut deps.storage, &node_identity);
|
||||
|
||||
let page1 =
|
||||
query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(limit))
|
||||
.unwrap();
|
||||
assert_eq!(limit, page1.delegations.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_default_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_mix_delegations(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
let page1 = query_all_mixnode_delegations_paged(deps.as_ref(), None, None).unwrap();
|
||||
assert_eq!(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT,
|
||||
page1.delegations.len() as u32
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_max_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_mix_delegations(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT;
|
||||
let page1 =
|
||||
query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(crazy_limit))
|
||||
.unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.delegations.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save("1".as_bytes(), &test_helpers::raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
let per_page = 2;
|
||||
let page1 =
|
||||
query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(per_page))
|
||||
.unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.delegations.len());
|
||||
|
||||
// save another
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save("2".as_bytes(), &test_helpers::raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
// page1 should have 2 results on it
|
||||
let page1 =
|
||||
query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(per_page))
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegations.len());
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save("3".as_bytes(), &test_helpers::raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
// page1 still has 2 results
|
||||
let page1 =
|
||||
query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(per_page))
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegations.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after =
|
||||
test_helpers::identity_and_owner_to_bytes(&node_identity, &Addr::unchecked("2"));
|
||||
let page2 = query_all_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after.clone()),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.delegations.len());
|
||||
|
||||
// save another one
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save("4".as_bytes(), &test_helpers::raw_delegation_fixture(42))
|
||||
.unwrap();
|
||||
|
||||
let page2 = query_all_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.delegations.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mix_deletion_query_returns_current_delegation_value() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
let delegation_owner = Addr::unchecked("bar");
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegation_owner.as_bytes(),
|
||||
&RawDelegationData::new(42u128.into(), 12_345),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Ok(Delegation::new(
|
||||
delegation_owner.clone(),
|
||||
coin(42, DENOM),
|
||||
12_345
|
||||
)),
|
||||
query_mixnode_delegation(deps.as_ref(), node_identity, delegation_owner)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mix_deletion_query_returns_error_if_delegation_doesnt_exist() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar");
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
|
||||
assert_eq!(
|
||||
Err(ContractError::NoMixnodeDelegationFound {
|
||||
identity: node_identity1.clone(),
|
||||
address: delegation_owner1.clone(),
|
||||
}),
|
||||
query_mixnode_delegation(
|
||||
deps.as_ref(),
|
||||
node_identity1.clone(),
|
||||
delegation_owner1.clone()
|
||||
)
|
||||
);
|
||||
|
||||
// add delegation from a different address
|
||||
mix_delegations(&mut deps.storage, &node_identity1)
|
||||
.save(
|
||||
delegation_owner2.as_bytes(),
|
||||
&test_helpers::raw_delegation_fixture(42),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Err(ContractError::NoMixnodeDelegationFound {
|
||||
identity: node_identity1.clone(),
|
||||
address: delegation_owner1.clone(),
|
||||
}),
|
||||
query_mixnode_delegation(
|
||||
deps.as_ref(),
|
||||
node_identity1.clone(),
|
||||
delegation_owner1.clone()
|
||||
)
|
||||
);
|
||||
|
||||
// add delegation for a different node
|
||||
mix_delegations(&mut deps.storage, &node_identity2)
|
||||
.save(
|
||||
delegation_owner1.as_bytes(),
|
||||
&test_helpers::raw_delegation_fixture(42),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Err(ContractError::NoMixnodeDelegationFound {
|
||||
identity: node_identity1.clone(),
|
||||
address: delegation_owner1.clone()
|
||||
}),
|
||||
query_mixnode_delegation(deps.as_ref(), node_identity1.clone(), delegation_owner1)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod querying_for_reverse_mixnode_delegations_paged {
|
||||
use super::*;
|
||||
use crate::mixnodes::delegation_queries::query_reverse_mixnode_delegations_paged;
|
||||
use storage::reverse_mix_delegations;
|
||||
|
||||
fn store_n_reverse_delegations(n: u32, storage: &mut dyn Storage, delegation_owner: &Addr) {
|
||||
for i in 0..n {
|
||||
let node_identity = format!("node{}", i);
|
||||
reverse_mix_delegations(storage, delegation_owner)
|
||||
.save(node_identity.as_bytes(), &())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_obeys_limits() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let limit = 2;
|
||||
let delegation_owner = Addr::unchecked("foo");
|
||||
store_n_reverse_delegations(100, &mut deps.storage, &delegation_owner);
|
||||
|
||||
let page1 = query_reverse_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner,
|
||||
None,
|
||||
Option::from(limit),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(limit, page1.delegated_nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_default_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let delegation_owner = Addr::unchecked("foo");
|
||||
store_n_reverse_delegations(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&delegation_owner,
|
||||
);
|
||||
|
||||
// query without explicitly setting a limit
|
||||
let page1 = query_reverse_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT,
|
||||
page1.delegated_nodes.len() as u32
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retrieval_has_max_limit() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let delegation_owner = Addr::unchecked("foo");
|
||||
store_n_reverse_delegations(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&delegation_owner,
|
||||
);
|
||||
|
||||
// query with a crazy high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT;
|
||||
let page1 = query_reverse_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner,
|
||||
None,
|
||||
Option::from(crazy_limit),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.delegated_nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let delegation_owner = Addr::unchecked("bar");
|
||||
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save("1".as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
let per_page = 2;
|
||||
let page1 = query_reverse_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.delegated_nodes.len());
|
||||
|
||||
// save another
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save("2".as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
// page1 should have 2 results on it
|
||||
let page1 = query_reverse_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegated_nodes.len());
|
||||
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save("3".as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
// page1 still has 2 results
|
||||
let page1 = query_reverse_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.delegated_nodes.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after: IdentityKey = String::from("2");
|
||||
let page2 = query_reverse_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner.clone(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.delegated_nodes.len());
|
||||
|
||||
// save another one
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save("4".as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
let start_after = String::from("2");
|
||||
let page2 = query_reverse_mixnode_delegations_paged(
|
||||
deps.as_ref(),
|
||||
delegation_owner,
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.delegated_nodes.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::storage;
|
||||
use crate::error::ContractError;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{coins, BankMsg, Coin, DepsMut, Env, MessageInfo, Response};
|
||||
use mixnet_contract::IdentityKey;
|
||||
use mixnet_contract::RawDelegationData;
|
||||
|
||||
fn validate_delegation_stake(delegation: &[Coin]) -> Result<(), ContractError> {
|
||||
// check if anything was put as delegation
|
||||
if delegation.is_empty() {
|
||||
return Err(ContractError::EmptyDelegation);
|
||||
}
|
||||
|
||||
if delegation.len() > 1 {
|
||||
return Err(ContractError::MultipleDenoms);
|
||||
}
|
||||
|
||||
// check that the denomination is correct
|
||||
if delegation[0].denom != DENOM {
|
||||
return Err(ContractError::WrongDenom {});
|
||||
}
|
||||
|
||||
// check that we have provided a non-zero amount in the delegation
|
||||
if delegation[0].amount.is_zero() {
|
||||
return Err(ContractError::EmptyDelegation);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn try_delegate_to_mixnode(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_identity: IdentityKey,
|
||||
) -> Result<Response, ContractError> {
|
||||
// check if the delegation contains any funds of the appropriate denomination
|
||||
validate_delegation_stake(&info.funds)?;
|
||||
|
||||
// check if the target node actually exists
|
||||
if storage::mixnodes()
|
||||
.load(deps.storage, mix_identity.as_bytes())
|
||||
.is_err()
|
||||
{
|
||||
return Err(ContractError::MixNodeBondNotFound {
|
||||
identity: mix_identity,
|
||||
});
|
||||
}
|
||||
|
||||
// update delegation of this delegator
|
||||
storage::mix_delegations(deps.storage, &mix_identity).update::<_, ContractError>(
|
||||
info.sender.as_bytes(),
|
||||
|existing_delegation| {
|
||||
// if no delegation existed, use default, i.e. 0
|
||||
let existing_delegation_amount = existing_delegation
|
||||
.map(|existing_delegation| existing_delegation.amount)
|
||||
.unwrap_or_default();
|
||||
|
||||
// the block height is reset, if it existed
|
||||
Ok(RawDelegationData::new(
|
||||
existing_delegation_amount + info.funds[0].amount,
|
||||
env.block.height,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
|
||||
// update total_delegation of this node
|
||||
storage::total_delegation(deps.storage).update::<_, ContractError>(
|
||||
mix_identity.as_bytes(),
|
||||
|total_delegation| {
|
||||
// since we know that the target node exists and because the total_delegation bucket
|
||||
// entry is created whenever the node itself is added, the unwrap here is fine
|
||||
// as the entry MUST exist
|
||||
Ok(total_delegation.unwrap() + info.funds[0].amount)
|
||||
},
|
||||
)?;
|
||||
|
||||
// save information about delegations of this sender
|
||||
storage::reverse_mix_delegations(deps.storage, &info.sender)
|
||||
.save(mix_identity.as_bytes(), &())?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
pub(crate) fn try_remove_delegation_from_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
mix_identity: IdentityKey,
|
||||
) -> Result<Response, ContractError> {
|
||||
let mut delegation_bucket = storage::mix_delegations(deps.storage, &mix_identity);
|
||||
let sender_bytes = info.sender.as_bytes();
|
||||
|
||||
if let Some(delegation) = delegation_bucket.may_load(sender_bytes)? {
|
||||
// remove all delegation associated with this delegator
|
||||
delegation_bucket.remove(sender_bytes);
|
||||
storage::reverse_mix_delegations(deps.storage, &info.sender)
|
||||
.remove(mix_identity.as_bytes());
|
||||
|
||||
// send delegated funds back to the delegation owner
|
||||
let messages = vec![BankMsg::Send {
|
||||
to_address: info.sender.to_string(),
|
||||
amount: coins(delegation.amount.u128(), DENOM),
|
||||
}
|
||||
.into()];
|
||||
|
||||
// update total_delegation of this node
|
||||
storage::total_delegation(deps.storage).update::<_, ContractError>(
|
||||
mix_identity.as_bytes(),
|
||||
|total_delegation| {
|
||||
// the first unwrap is fine because the delegation information MUST exist, otherwise we would
|
||||
// have never gotten here in the first place
|
||||
// the second unwrap is also fine because we should NEVER underflow here,
|
||||
// if we do, it means we have some serious error in our logic
|
||||
Ok(total_delegation
|
||||
.unwrap()
|
||||
.checked_sub(delegation.amount)
|
||||
.unwrap())
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(Response {
|
||||
submessages: Vec::new(),
|
||||
messages,
|
||||
attributes: Vec::new(),
|
||||
data: None,
|
||||
})
|
||||
} else {
|
||||
Err(ContractError::NoMixnodeDelegationFound {
|
||||
identity: mix_identity,
|
||||
address: info.sender,
|
||||
})
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::storage;
|
||||
use super::*;
|
||||
use crate::mixnodes::delegation_transactions::try_delegate_to_mixnode;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::coins;
|
||||
|
||||
#[cfg(test)]
|
||||
mod delegation_stake_validation {
|
||||
use super::*;
|
||||
use crate::mixnodes::delegation_transactions::validate_delegation_stake;
|
||||
use cosmwasm_std::coin;
|
||||
#[test]
|
||||
fn stake_cant_be_empty() {
|
||||
assert_eq!(
|
||||
Err(ContractError::EmptyDelegation),
|
||||
validate_delegation_stake(&[])
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn stake_must_have_single_coin_type() {
|
||||
assert_eq!(
|
||||
Err(ContractError::MultipleDenoms),
|
||||
validate_delegation_stake(&[coin(123, DENOM), coin(123, "BTC"), coin(123, "DOGE")])
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn stake_coin_must_be_of_correct_type() {
|
||||
assert_eq!(
|
||||
Err(ContractError::WrongDenom {}),
|
||||
validate_delegation_stake(&[coin(123, "DOGE")])
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn stake_coin_must_have_value_greater_than_zero() {
|
||||
assert_eq!(
|
||||
Err(ContractError::EmptyDelegation),
|
||||
validate_delegation_stake(&[coin(0, DENOM)])
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn stake_can_have_any_positive_value() {
|
||||
// this might change in the future, but right now an arbitrary (positive) value can be delegated
|
||||
assert!(validate_delegation_stake(&[coin(1, DENOM)]).is_ok());
|
||||
assert!(validate_delegation_stake(&[coin(123, DENOM)]).is_ok());
|
||||
assert!(validate_delegation_stake(&[coin(10000000000, DENOM)]).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod mix_stake_delegation {
|
||||
use super::storage;
|
||||
use super::*;
|
||||
use crate::mixnodes::bonding_transactions::try_remove_mixnode;
|
||||
use crate::support::tests::test_helpers::good_mixnode_bond;
|
||||
use cosmwasm_std::coin;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::Addr;
|
||||
|
||||
#[test]
|
||||
fn fails_if_node_doesnt_exist() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
assert_eq!(
|
||||
Err(ContractError::MixNodeBondNotFound {
|
||||
identity: "non-existent-mix-identity".into()
|
||||
}),
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info("sender", &coins(123, DENOM)),
|
||||
"non-existent-mix-identity".into()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn succeeds_for_existing_node() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
let delegation = coin(123, DENOM);
|
||||
assert!(try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
|
||||
identity.clone()
|
||||
)
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
RawDelegationData::new(delegation.amount, mock_env().block.height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
storage::reverse_mix_delegations_read(&deps.storage, &delegation_owner)
|
||||
.load(identity.as_bytes())
|
||||
.is_ok()
|
||||
);
|
||||
// node's "total_delegation" is increased
|
||||
assert_eq!(
|
||||
delegation.amount,
|
||||
storage::total_delegation_read(&deps.storage)
|
||||
.load(identity.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_if_node_unbonded() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
|
||||
assert_eq!(
|
||||
Err(ContractError::MixNodeBondNotFound {
|
||||
identity: identity.clone()
|
||||
}),
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &coins(123, DENOM)),
|
||||
identity
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn succeeds_if_node_rebonded() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation = coin(123, DENOM);
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
assert!(try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
|
||||
identity.clone()
|
||||
)
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
RawDelegationData::new(delegation.amount, mock_env().block.height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
storage::reverse_mix_delegations_read(&deps.storage, &delegation_owner)
|
||||
.load(identity.as_bytes())
|
||||
.is_ok()
|
||||
);
|
||||
// node's "total_delegation" is increased
|
||||
assert_eq!(
|
||||
delegation.amount,
|
||||
storage::total_delegation_read(&deps.storage)
|
||||
.load(identity.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_possible_for_an_already_delegated_node() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
let delegation1 = coin(100, DENOM);
|
||||
let delegation2 = coin(50, DENOM);
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &[delegation1.clone()]),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &[delegation2.clone()]),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
RawDelegationData::new(
|
||||
delegation1.amount + delegation2.amount,
|
||||
mock_env().block.height
|
||||
),
|
||||
storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
storage::reverse_mix_delegations_read(&deps.storage, &delegation_owner)
|
||||
.load(identity.as_bytes())
|
||||
.is_ok()
|
||||
);
|
||||
// node's "total_delegation" is sum of both
|
||||
assert_eq!(
|
||||
delegation1.amount + delegation2.amount,
|
||||
storage::total_delegation_read(&deps.storage)
|
||||
.load(identity.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn block_height_is_updated_on_new_delegation() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
let delegation = coin(100, DENOM);
|
||||
let env1 = mock_env();
|
||||
let mut env2 = mock_env();
|
||||
let initial_height = env1.block.height;
|
||||
let updated_height = initial_height + 42;
|
||||
// second env has grown in block height
|
||||
env2.block.height = updated_height;
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
env1,
|
||||
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
RawDelegationData::new(delegation.amount, initial_height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
env2,
|
||||
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
RawDelegationData::new(delegation.amount + delegation.amount, updated_height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_height_is_not_updated_on_different_delegator() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner1 = Addr::unchecked("sender1");
|
||||
let delegation_owner2 = Addr::unchecked("sender2");
|
||||
let delegation1 = coin(100, DENOM);
|
||||
let delegation2 = coin(120, DENOM);
|
||||
let env1 = mock_env();
|
||||
let mut env2 = mock_env();
|
||||
let initial_height = env1.block.height;
|
||||
let second_height = initial_height + 42;
|
||||
// second env has grown in block height
|
||||
env2.block.height = second_height;
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
env1,
|
||||
mock_info(delegation_owner1.as_str(), &[delegation1.clone()]),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
RawDelegationData::new(delegation1.amount, initial_height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.load(delegation_owner1.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
env2,
|
||||
mock_info(delegation_owner2.as_str(), &[delegation2.clone()]),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
RawDelegationData::new(delegation1.amount, initial_height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.load(delegation_owner1.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
RawDelegationData::new(delegation2.amount, second_height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.load(delegation_owner2.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_disallowed_for_already_delegated_node_if_it_unbonded() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &coins(100, DENOM)),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
|
||||
assert_eq!(
|
||||
Err(ContractError::MixNodeBondNotFound {
|
||||
identity: identity.clone()
|
||||
}),
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &coins(50, DENOM)),
|
||||
identity
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_allowed_for_multiple_nodes() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner1 = "bob";
|
||||
let mixnode_owner2 = "fred";
|
||||
let identity1 =
|
||||
test_helpers::add_mixnode(mixnode_owner1, good_mixnode_bond(), deps.as_mut());
|
||||
let identity2 =
|
||||
test_helpers::add_mixnode(mixnode_owner2, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
assert!(try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &coins(123, DENOM)),
|
||||
identity1.clone()
|
||||
)
|
||||
.is_ok());
|
||||
assert!(try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &coins(42, DENOM)),
|
||||
identity2.clone()
|
||||
)
|
||||
.is_ok());
|
||||
assert_eq!(
|
||||
RawDelegationData::new(123u128.into(), mock_env().block.height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity1)
|
||||
.load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
storage::reverse_mix_delegations_read(&deps.storage, &delegation_owner)
|
||||
.load(identity1.as_bytes())
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
RawDelegationData::new(42u128.into(), mock_env().block.height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity2)
|
||||
.load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
storage::reverse_mix_delegations_read(&deps.storage, &delegation_owner)
|
||||
.load(identity2.as_bytes())
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_allowed_by_multiple_users() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation1 = coin(123, DENOM);
|
||||
let delegation2 = coin(234, DENOM);
|
||||
assert!(try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info("sender1", &[delegation1.clone()]),
|
||||
identity.clone()
|
||||
)
|
||||
.is_ok());
|
||||
assert!(try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info("sender2", &[delegation2.clone()]),
|
||||
identity.clone()
|
||||
)
|
||||
.is_ok());
|
||||
// node's "total_delegation" is sum of both
|
||||
assert_eq!(
|
||||
delegation1.amount + delegation2.amount,
|
||||
storage::total_delegation_read(&deps.storage)
|
||||
.load(identity.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn delegation_is_not_removed_if_node_unbonded() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &coins(100, DENOM)),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
|
||||
assert_eq!(
|
||||
RawDelegationData::new(100u128.into(), mock_env().block.height),
|
||||
storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
storage::reverse_mix_delegations_read(&deps.storage, &delegation_owner)
|
||||
.load(identity.as_bytes())
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod removing_mix_stake_delegation {
|
||||
use super::storage;
|
||||
use super::*;
|
||||
use crate::mixnodes::bonding_transactions::try_remove_mixnode;
|
||||
use crate::support::tests::test_helpers::good_mixnode_bond;
|
||||
use cosmwasm_std::coin;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::Addr;
|
||||
use cosmwasm_std::Uint128;
|
||||
|
||||
#[test]
|
||||
fn fails_if_delegation_never_existed() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
assert_eq!(
|
||||
Err(ContractError::NoMixnodeDelegationFound {
|
||||
identity: identity.clone(),
|
||||
address: delegation_owner.clone(),
|
||||
}),
|
||||
try_remove_delegation_from_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_info(delegation_owner.as_str(), &[]),
|
||||
identity,
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn succeeds_if_delegation_existed() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &coins(100, DENOM)),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
Ok(Response {
|
||||
submessages: vec![],
|
||||
messages: vec![BankMsg::Send {
|
||||
to_address: delegation_owner.clone().into(),
|
||||
amount: coins(100, DENOM),
|
||||
}
|
||||
.into()],
|
||||
attributes: Vec::new(),
|
||||
data: None,
|
||||
}),
|
||||
try_remove_delegation_from_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_info(delegation_owner.as_str(), &[]),
|
||||
identity.clone(),
|
||||
)
|
||||
);
|
||||
assert!(storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.may_load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(
|
||||
storage::reverse_mix_delegations_read(&deps.storage, &delegation_owner)
|
||||
.may_load(identity.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
// and total delegation is cleared
|
||||
assert_eq!(
|
||||
Uint128::zero(),
|
||||
storage::total_delegation_read(&deps.storage)
|
||||
.load(identity.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
fn succeeds_if_delegation_existed_even_if_node_unbonded() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner = Addr::unchecked("sender");
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner.as_str(), &coins(100, DENOM)),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
|
||||
assert_eq!(
|
||||
Ok(Response {
|
||||
submessages: vec![],
|
||||
messages: vec![BankMsg::Send {
|
||||
to_address: delegation_owner.clone().into(),
|
||||
amount: coins(100, DENOM),
|
||||
}
|
||||
.into()],
|
||||
attributes: Vec::new(),
|
||||
data: None,
|
||||
}),
|
||||
try_remove_delegation_from_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_info(delegation_owner.as_str(), &[]),
|
||||
identity.clone(),
|
||||
)
|
||||
);
|
||||
assert!(storage::mix_delegations_read(&deps.storage, &identity)
|
||||
.may_load(delegation_owner.as_bytes())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(
|
||||
storage::reverse_mix_delegations_read(&deps.storage, &delegation_owner)
|
||||
.may_load(identity.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn total_delegation_is_preserved_if_only_some_undelegate() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mixnode_owner = "bob";
|
||||
let identity =
|
||||
test_helpers::add_mixnode(mixnode_owner, good_mixnode_bond(), deps.as_mut());
|
||||
let delegation_owner1 = Addr::unchecked("sender1");
|
||||
let delegation_owner2 = Addr::unchecked("sender2");
|
||||
let delegation1 = coin(123, DENOM);
|
||||
let delegation2 = coin(234, DENOM);
|
||||
assert!(try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner1.as_str(), &[delegation1.clone()]),
|
||||
identity.clone()
|
||||
)
|
||||
.is_ok());
|
||||
assert!(try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_env(),
|
||||
mock_info(delegation_owner2.as_str(), &[delegation2.clone()]),
|
||||
identity.clone()
|
||||
)
|
||||
.is_ok());
|
||||
// sender1 undelegates
|
||||
try_remove_delegation_from_mixnode(
|
||||
deps.as_mut(),
|
||||
mock_info(delegation_owner1.as_str(), &[]),
|
||||
identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
// but total delegation should still equal to what sender2 sent
|
||||
// node's "total_delegation" is sum of both
|
||||
assert_eq!(
|
||||
delegation2.amount,
|
||||
storage::total_delegation_read(&deps.storage)
|
||||
.load(identity.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod multi_delegations {
|
||||
use super::*;
|
||||
use crate::mixnodes::delegation_helpers;
|
||||
use crate::mixnodes::delegation_queries::tests::store_n_mix_delegations;
|
||||
use crate::support::tests::test_helpers;
|
||||
use mixnet_contract::IdentityKey;
|
||||
use mixnet_contract::RawDelegationData;
|
||||
|
||||
#[test]
|
||||
fn multiple_page_delegations() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
store_n_mix_delegations(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
&mut deps.storage,
|
||||
&node_identity,
|
||||
);
|
||||
let mix_bucket = storage::all_mix_delegations_read::<RawDelegationData>(&deps.storage);
|
||||
let mix_delegations = delegation_helpers::Delegations::new(mix_bucket);
|
||||
assert_eq!(
|
||||
storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10,
|
||||
mix_delegations.count() as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use cosmwasm_std::{Deps, StdResult};
|
||||
use mixnet_contract::LayerDistribution;
|
||||
|
||||
pub(crate) fn query_layer_distribution(deps: Deps) -> StdResult<LayerDistribution> {
|
||||
mixnet_params_storage::LAYERS.load(deps.storage)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod bonding_queries;
|
||||
pub mod bonding_transactions;
|
||||
pub mod delegation_helpers;
|
||||
pub mod delegation_queries;
|
||||
pub mod delegation_transactions;
|
||||
pub mod layer_queries;
|
||||
pub mod storage;
|
||||
@@ -0,0 +1,378 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{StdResult, Storage, Uint128};
|
||||
use cosmwasm_storage::{bucket, bucket_read, Bucket, ReadonlyBucket};
|
||||
use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex};
|
||||
use mixnet_contract::{
|
||||
Addr, Coin, IdentityKey, IdentityKeyRef, Layer, MixNode, MixNodeBond, RawDelegationData,
|
||||
RewardingStatus,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
// storage prefixes
|
||||
// const PREFIX_MIXNODES: &[u8] = b"mn";
|
||||
// const PREFIX_MIXNODES_OWNERS: &[u8] = b"mo";
|
||||
const PREFIX_MIX_DELEGATION: &[u8] = b"md";
|
||||
const PREFIX_REVERSE_MIX_DELEGATION: &[u8] = b"dm";
|
||||
pub const PREFIX_REWARDED_MIXNODES: &[u8] = b"rm";
|
||||
|
||||
// paged retrieval limits for all queries and transactions
|
||||
// currently the maximum limit before running into memory issue is somewhere between 1150 and 1200
|
||||
pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 500;
|
||||
pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 250;
|
||||
pub(crate) const BOND_PAGE_MAX_LIMIT: u32 = 75;
|
||||
pub(crate) const BOND_PAGE_DEFAULT_LIMIT: u32 = 50;
|
||||
|
||||
const PREFIX_TOTAL_DELEGATION: &[u8] = b"td";
|
||||
|
||||
pub(crate) struct MixnodeBondIndex<'a> {
|
||||
pub(crate) identity: UniqueIndex<'a, IdentityKey, StoredMixnodeBond>,
|
||||
|
||||
// somehow PrimaryKey is not implemented for Addr but is for String?
|
||||
// maybe it's an omission in this version and is fixed in the cosmwasm 1.0 compatible release?
|
||||
pub(crate) owner: UniqueIndex<'a, String, StoredMixnodeBond>,
|
||||
}
|
||||
|
||||
// IndexList is just boilerplate code for fetching a struct's indexes
|
||||
impl<'a> IndexList<StoredMixnodeBond> for MixnodeBondIndex<'a> {
|
||||
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<StoredMixnodeBond>> + '_> {
|
||||
let v: Vec<&dyn Index<StoredMixnodeBond>> = vec![&self.identity, &self.owner];
|
||||
Box::new(v.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
// mixnodes() is the storage access function.
|
||||
pub(crate) fn mixnodes<'a>() -> IndexedMap<'a, &'a [u8], StoredMixnodeBond, MixnodeBondIndex<'a>> {
|
||||
let indexes = MixnodeBondIndex {
|
||||
identity: UniqueIndex::new(|d| d.mix_node.identity_key.clone(), "mni"),
|
||||
owner: UniqueIndex::new(|d| d.owner.to_string(), "mno"),
|
||||
};
|
||||
IndexedMap::new("mn", indexes)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub(crate) struct StoredMixnodeBond {
|
||||
pub bond_amount: Coin,
|
||||
pub owner: Addr,
|
||||
pub layer: Layer,
|
||||
pub block_height: u64,
|
||||
pub mix_node: MixNode,
|
||||
pub profit_margin_percent: Option<u8>,
|
||||
}
|
||||
|
||||
impl StoredMixnodeBond {
|
||||
pub(crate) fn new(
|
||||
bond_amount: Coin,
|
||||
owner: Addr,
|
||||
layer: Layer,
|
||||
block_height: u64,
|
||||
mix_node: MixNode,
|
||||
profit_margin_percent: Option<u8>,
|
||||
) -> Self {
|
||||
StoredMixnodeBond {
|
||||
bond_amount,
|
||||
owner,
|
||||
layer,
|
||||
block_height,
|
||||
mix_node,
|
||||
profit_margin_percent,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attach_delegation(self, total_delegation: Uint128) -> MixNodeBond {
|
||||
MixNodeBond {
|
||||
total_delegation: Coin {
|
||||
denom: self.bond_amount.denom.clone(),
|
||||
amount: total_delegation,
|
||||
},
|
||||
bond_amount: self.bond_amount,
|
||||
owner: self.owner,
|
||||
layer: self.layer,
|
||||
block_height: self.block_height,
|
||||
mix_node: self.mix_node,
|
||||
profit_margin_percent: self.profit_margin_percent,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn identity(&self) -> &String {
|
||||
&self.mix_node.identity_key
|
||||
}
|
||||
|
||||
pub(crate) fn bond_amount(&self) -> Coin {
|
||||
self.bond_amount.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for StoredMixnodeBond {
|
||||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"amount: {}, owner: {}, identity: {}",
|
||||
self.bond_amount, self.owner, self.mix_node.identity_key
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Mixnode-related stuff
|
||||
|
||||
pub fn total_delegation(storage: &mut dyn Storage) -> Bucket<Uint128> {
|
||||
bucket(storage, PREFIX_TOTAL_DELEGATION)
|
||||
}
|
||||
|
||||
pub fn total_delegation_read(storage: &dyn Storage) -> ReadonlyBucket<Uint128> {
|
||||
bucket_read(storage, PREFIX_TOTAL_DELEGATION)
|
||||
}
|
||||
|
||||
// we want to treat this bucket as a set so we don't really care about what type of data is being stored.
|
||||
// I went with u8 as after serialization it takes only a single byte of space, while if a `()` was used,
|
||||
// it would have taken 4 bytes (representation of 'null')
|
||||
pub(crate) fn rewarded_mixnodes(
|
||||
storage: &mut dyn Storage,
|
||||
rewarding_interval_nonce: u32,
|
||||
) -> Bucket<RewardingStatus> {
|
||||
Bucket::multilevel(
|
||||
storage,
|
||||
&[
|
||||
rewarding_interval_nonce.to_be_bytes().as_ref(),
|
||||
PREFIX_REWARDED_MIXNODES,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn rewarded_mixnodes_read(
|
||||
storage: &dyn Storage,
|
||||
rewarding_interval_nonce: u32,
|
||||
) -> ReadonlyBucket<RewardingStatus> {
|
||||
ReadonlyBucket::multilevel(
|
||||
storage,
|
||||
&[
|
||||
rewarding_interval_nonce.to_be_bytes().as_ref(),
|
||||
PREFIX_REWARDED_MIXNODES,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn read_mixnode_bond(
|
||||
storage: &dyn Storage,
|
||||
mix_identity: IdentityKeyRef,
|
||||
) -> StdResult<Option<MixNodeBond>> {
|
||||
let stored_bond = mixnodes().may_load(storage, mix_identity.as_bytes())?;
|
||||
match stored_bond {
|
||||
None => Ok(None),
|
||||
Some(stored_bond) => {
|
||||
let total_delegation =
|
||||
total_delegation_read(storage).may_load(mix_identity.as_bytes())?;
|
||||
Ok(Some(MixNodeBond {
|
||||
bond_amount: stored_bond.bond_amount,
|
||||
total_delegation: Coin {
|
||||
denom: DENOM.to_owned(),
|
||||
amount: total_delegation.unwrap_or_default(),
|
||||
},
|
||||
owner: stored_bond.owner,
|
||||
layer: stored_bond.layer,
|
||||
block_height: stored_bond.block_height,
|
||||
mix_node: stored_bond.mix_node,
|
||||
profit_margin_percent: stored_bond.profit_margin_percent,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delegation related
|
||||
pub fn all_mix_delegations_read<T>(storage: &dyn Storage) -> ReadonlyBucket<T>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
bucket_read(storage, PREFIX_MIX_DELEGATION)
|
||||
}
|
||||
|
||||
pub fn mix_delegations<'a>(
|
||||
storage: &'a mut dyn Storage,
|
||||
mix_identity: IdentityKeyRef,
|
||||
) -> Bucket<'a, RawDelegationData> {
|
||||
Bucket::multilevel(storage, &[PREFIX_MIX_DELEGATION, mix_identity.as_bytes()])
|
||||
}
|
||||
|
||||
pub fn mix_delegations_read<'a>(
|
||||
storage: &'a dyn Storage,
|
||||
mix_identity: IdentityKeyRef,
|
||||
) -> ReadonlyBucket<'a, RawDelegationData> {
|
||||
ReadonlyBucket::multilevel(storage, &[PREFIX_MIX_DELEGATION, mix_identity.as_bytes()])
|
||||
}
|
||||
|
||||
// TODO: note for JS when doing a deep review for the contract. Don't store it as (), instead do it as u8
|
||||
pub fn reverse_mix_delegations<'a>(storage: &'a mut dyn Storage, owner: &Addr) -> Bucket<'a, ()> {
|
||||
Bucket::multilevel(storage, &[PREFIX_REVERSE_MIX_DELEGATION, owner.as_bytes()])
|
||||
}
|
||||
|
||||
pub fn reverse_mix_delegations_read<'a>(
|
||||
storage: &'a dyn Storage,
|
||||
owner: &Addr,
|
||||
) -> ReadonlyBucket<'a, ()> {
|
||||
ReadonlyBucket::multilevel(storage, &[PREFIX_REVERSE_MIX_DELEGATION, owner.as_bytes()])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::storage;
|
||||
use super::*;
|
||||
use crate::mixnodes::bonding_transactions::try_add_mixnode;
|
||||
use crate::support::tests::test_helpers;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockStorage};
|
||||
use cosmwasm_std::{coin, Addr, Uint128};
|
||||
use mixnet_contract::IdentityKey;
|
||||
use mixnet_contract::MixNode;
|
||||
use mixnet_contract::RawDelegationData;
|
||||
|
||||
#[test]
|
||||
fn mixnode_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = test_helpers::stored_mixnode_bond_fixture("owner1");
|
||||
let bond2 = test_helpers::stored_mixnode_bond_fixture("owner2");
|
||||
mixnodes().save(&mut storage, b"bond1", &bond1).unwrap();
|
||||
mixnodes().save(&mut storage, b"bond2", &bond2).unwrap();
|
||||
|
||||
let res1 = mixnodes().load(&storage, b"bond1").unwrap();
|
||||
let res2 = mixnodes().load(&storage, b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_mixnode_bond() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_owner: Addr = Addr::unchecked("node-owner");
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
|
||||
// produces a None if target mixnode doesn't exist
|
||||
let res = storage::read_mixnode_bond(deps.as_ref().storage, node_owner.as_str()).unwrap();
|
||||
assert!(res.is_none());
|
||||
|
||||
// returns appropriate value otherwise
|
||||
let bond_value = 1000000000;
|
||||
|
||||
let mixnode = MixNode {
|
||||
identity_key: node_identity.clone(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
};
|
||||
|
||||
let info = mock_info(node_owner.as_str(), &vec![coin(bond_value, DENOM)]);
|
||||
try_add_mixnode(deps.as_mut(), mock_env(), info, mixnode).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(bond_value),
|
||||
storage::read_mixnode_bond(deps.as_ref().storage, node_identity.as_str())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.bond_amount
|
||||
.amount
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_mixnode_delegations_read_retrieval() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar1");
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
let raw_delegation1 = RawDelegationData::new(1u128.into(), 1000);
|
||||
let raw_delegation2 = RawDelegationData::new(2u128.into(), 2000);
|
||||
|
||||
storage::mix_delegations(&mut deps.storage, &node_identity1)
|
||||
.save(delegation_owner1.as_bytes(), &raw_delegation1)
|
||||
.unwrap();
|
||||
storage::mix_delegations(&mut deps.storage, &node_identity2)
|
||||
.save(delegation_owner2.as_bytes(), &raw_delegation2)
|
||||
.unwrap();
|
||||
|
||||
let res1 = storage::all_mix_delegations_read::<RawDelegationData>(&deps.storage)
|
||||
.load(&*test_helpers::identity_and_owner_to_bytes(
|
||||
&node_identity1,
|
||||
&delegation_owner1,
|
||||
))
|
||||
.unwrap();
|
||||
let res2 = storage::all_mix_delegations_read::<RawDelegationData>(&deps.storage)
|
||||
.load(&*test_helpers::identity_and_owner_to_bytes(
|
||||
&node_identity2,
|
||||
&delegation_owner2,
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(raw_delegation1, res1);
|
||||
assert_eq!(raw_delegation2, res2);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod reverse_mix_delegations {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers;
|
||||
|
||||
#[test]
|
||||
fn reverse_mix_delegation_exists() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
let delegation_owner = Addr::unchecked("bar");
|
||||
|
||||
storage::reverse_mix_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save(node_identity.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(storage::reverse_mix_delegations_read(
|
||||
deps.as_ref().storage,
|
||||
&delegation_owner
|
||||
)
|
||||
.may_load(node_identity.as_bytes())
|
||||
.unwrap()
|
||||
.is_some(),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_mix_delegation_returns_none_if_delegation_doesnt_exist() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar");
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
|
||||
assert!(storage::reverse_mix_delegations_read(
|
||||
deps.as_ref().storage,
|
||||
&delegation_owner1
|
||||
)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// add delegation for a different node
|
||||
storage::reverse_mix_delegations(&mut deps.storage, &delegation_owner1)
|
||||
.save(node_identity2.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(storage::reverse_mix_delegations_read(
|
||||
deps.as_ref().storage,
|
||||
&delegation_owner1
|
||||
)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// add delegation from a different owner
|
||||
storage::reverse_mix_delegations(&mut deps.storage, &delegation_owner2)
|
||||
.save(node_identity1.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(storage::reverse_mix_delegations_read(
|
||||
deps.as_ref().storage,
|
||||
&delegation_owner1
|
||||
)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// Adds a 0 byte to terminate the `start_after` value given. This allows CosmWasm
|
||||
/// to get the succeeding key as the start of the next page.
|
||||
// S works for both `String` and `Addr` and that's what we wanted
|
||||
pub fn calculate_start_value<S: AsRef<str>>(start_after: Option<S>) -> Option<Vec<u8>> {
|
||||
start_after.as_ref().map(|identity| {
|
||||
identity
|
||||
.as_ref()
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod queries;
|
||||
pub mod storage;
|
||||
pub mod transactions;
|
||||
@@ -0,0 +1,287 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::storage;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use cosmwasm_std::Uint128;
|
||||
use cosmwasm_std::{Deps, StdResult};
|
||||
use mixnet_contract::{IdentityKey, MixnodeRewardingStatusResponse};
|
||||
|
||||
pub(crate) fn query_reward_pool(deps: Deps) -> Uint128 {
|
||||
storage::reward_pool_value(deps.storage)
|
||||
}
|
||||
|
||||
pub(crate) fn query_circulating_supply(deps: Deps) -> Uint128 {
|
||||
storage::circulating_supply(deps.storage)
|
||||
}
|
||||
|
||||
pub(crate) fn query_rewarding_status(
|
||||
deps: Deps,
|
||||
mix_identity: IdentityKey,
|
||||
rewarding_interval_nonce: u32,
|
||||
) -> StdResult<MixnodeRewardingStatusResponse> {
|
||||
let status = mixnodes_storage::rewarded_mixnodes_read(deps.storage, rewarding_interval_nonce)
|
||||
.may_load(mix_identity.as_bytes())?;
|
||||
|
||||
Ok(MixnodeRewardingStatusResponse { status })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use mixnet_contract::MixNode;
|
||||
|
||||
#[cfg(test)]
|
||||
mod querying_for_rewarding_status {
|
||||
use super::storage;
|
||||
use super::*;
|
||||
use crate::mixnodes::bonding_transactions::try_add_mixnode;
|
||||
use crate::mixnodes::delegation_transactions::try_delegate_to_mixnode;
|
||||
use crate::rewards::transactions::{
|
||||
try_begin_mixnode_rewarding, try_finish_mixnode_rewarding, try_reward_mixnode_v2,
|
||||
try_reward_next_mixnode_delegators_v2,
|
||||
};
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::coin;
|
||||
use mixnet_contract::{RewardingResult, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT};
|
||||
|
||||
#[test]
|
||||
fn returns_empty_status_for_unrewarded_nodes() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let env = mock_env();
|
||||
let current_state = mixnet_params_storage::CONTRACT_SETTINGS
|
||||
.load(deps.as_mut().storage)
|
||||
.unwrap();
|
||||
let rewarding_validator_address = current_state.rewarding_validator_address;
|
||||
|
||||
let node_identity =
|
||||
test_helpers::add_mixnode("bob", test_helpers::good_mixnode_bond(), deps.as_mut());
|
||||
|
||||
assert!(
|
||||
query_rewarding_status(deps.as_ref(), node_identity.clone(), 1)
|
||||
.unwrap()
|
||||
.status
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// node was rewarded but for different epoch
|
||||
let info = mock_info(rewarding_validator_address.as_ref(), &[]);
|
||||
try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap();
|
||||
try_reward_mixnode_v2(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
info.clone(),
|
||||
node_identity.clone(),
|
||||
test_helpers::node_rewarding_params_fixture(100),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
try_finish_mixnode_rewarding(deps.as_mut(), info.clone(), 1).unwrap();
|
||||
|
||||
assert!(query_rewarding_status(deps.as_ref(), node_identity, 2)
|
||||
.unwrap()
|
||||
.status
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_complete_status_for_fully_rewarded_node() {
|
||||
// with single page
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mut env = mock_env();
|
||||
let current_state = mixnet_params_storage::CONTRACT_SETTINGS
|
||||
.load(deps.as_mut().storage)
|
||||
.unwrap();
|
||||
let rewarding_validator_address = current_state.rewarding_validator_address;
|
||||
|
||||
let node_identity = "bobsnode".to_string();
|
||||
try_add_mixnode(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
mock_info("bob", &test_helpers::good_mixnode_bond()),
|
||||
MixNode {
|
||||
identity_key: node_identity.clone(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING;
|
||||
|
||||
let info = mock_info(rewarding_validator_address.as_ref(), &[]);
|
||||
try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap();
|
||||
try_reward_mixnode_v2(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
info.clone(),
|
||||
node_identity.clone(),
|
||||
test_helpers::node_rewarding_params_fixture(100),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
try_finish_mixnode_rewarding(deps.as_mut(), info.clone(), 1).unwrap();
|
||||
|
||||
let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap();
|
||||
assert!(matches!(res.status, Some(RewardingStatus::Complete(..))));
|
||||
|
||||
match res.status.unwrap() {
|
||||
RewardingStatus::Complete(result) => {
|
||||
assert_ne!(
|
||||
RewardingResult::default().operator_reward,
|
||||
result.operator_reward
|
||||
);
|
||||
assert_eq!(
|
||||
RewardingResult::default().total_delegator_reward,
|
||||
result.total_delegator_reward
|
||||
);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// with multiple pages
|
||||
|
||||
let node_identity = "alicesnode".to_string();
|
||||
try_add_mixnode(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
mock_info("alice", &test_helpers::good_mixnode_bond()),
|
||||
MixNode {
|
||||
identity_key: node_identity.clone(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 123 {
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
mock_info(
|
||||
&*format!("delegator{:04}", i),
|
||||
&vec![coin(200_000000, DENOM)],
|
||||
),
|
||||
node_identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING;
|
||||
|
||||
let info = mock_info(rewarding_validator_address.as_ref(), &[]);
|
||||
try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 2).unwrap();
|
||||
|
||||
try_reward_mixnode_v2(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
info.clone(),
|
||||
node_identity.clone(),
|
||||
test_helpers::node_rewarding_params_fixture(100),
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// rewards all pending
|
||||
try_reward_next_mixnode_delegators_v2(
|
||||
deps.as_mut(),
|
||||
info.clone(),
|
||||
node_identity.to_string(),
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = query_rewarding_status(deps.as_ref(), node_identity, 2).unwrap();
|
||||
assert!(matches!(res.status, Some(RewardingStatus::Complete(..))));
|
||||
|
||||
match res.status.unwrap() {
|
||||
RewardingStatus::Complete(result) => {
|
||||
assert_ne!(
|
||||
RewardingResult::default().operator_reward,
|
||||
result.operator_reward
|
||||
);
|
||||
assert_ne!(
|
||||
RewardingResult::default().total_delegator_reward,
|
||||
result.total_delegator_reward
|
||||
);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_pending_next_delegator_page_status_when_there_are_more_delegators_to_reward() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mut env = mock_env();
|
||||
let current_state = mixnet_params_storage::CONTRACT_SETTINGS
|
||||
.load(deps.as_mut().storage)
|
||||
.unwrap();
|
||||
let rewarding_validator_address = current_state.rewarding_validator_address;
|
||||
|
||||
let node_identity = "bobsnode".to_string();
|
||||
try_add_mixnode(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
mock_info("bob", &test_helpers::good_mixnode_bond()),
|
||||
MixNode {
|
||||
identity_key: node_identity.clone(),
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 123 {
|
||||
try_delegate_to_mixnode(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
mock_info(
|
||||
&*format!("delegator{:04}", i),
|
||||
&vec![coin(200_000000, DENOM)],
|
||||
),
|
||||
node_identity.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
env.block.height += storage::MINIMUM_BLOCK_AGE_FOR_REWARDING;
|
||||
|
||||
let info = mock_info(rewarding_validator_address.as_ref(), &[]);
|
||||
try_begin_mixnode_rewarding(deps.as_mut(), env.clone(), info.clone(), 1).unwrap();
|
||||
|
||||
try_reward_mixnode_v2(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
info,
|
||||
node_identity.clone(),
|
||||
test_helpers::node_rewarding_params_fixture(100),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap();
|
||||
assert!(matches!(
|
||||
res.status,
|
||||
Some(RewardingStatus::PendingNextDelegatorPage(..))
|
||||
));
|
||||
|
||||
match res.status.unwrap() {
|
||||
RewardingStatus::PendingNextDelegatorPage(result) => {
|
||||
assert_ne!(
|
||||
RewardingResult::default().operator_reward,
|
||||
result.running_results.operator_reward
|
||||
);
|
||||
assert_ne!(
|
||||
RewardingResult::default().total_delegator_reward,
|
||||
result.running_results.total_delegator_reward
|
||||
);
|
||||
assert_eq!(
|
||||
&*format!("delegator{:04}", MIXNODE_DELEGATORS_PAGE_LIMIT),
|
||||
result.next_start
|
||||
);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::INITIAL_REWARD_POOL;
|
||||
use crate::error::ContractError;
|
||||
use config::defaults::TOTAL_SUPPLY;
|
||||
use cosmwasm_std::{Storage, Uint128};
|
||||
use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton};
|
||||
|
||||
const REWARD_POOL_PREFIX: &[u8] = b"pool";
|
||||
|
||||
// approximately 1 day (assuming 5s per block)
|
||||
pub(crate) const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 17280;
|
||||
|
||||
// approximately 30min (assuming 5s per block)
|
||||
pub(crate) const MAX_REWARDING_DURATION_IN_BLOCKS: u64 = 360;
|
||||
|
||||
fn reward_pool(storage: &dyn Storage) -> ReadonlySingleton<Uint128> {
|
||||
singleton_read(storage, REWARD_POOL_PREFIX)
|
||||
}
|
||||
|
||||
pub fn mut_reward_pool(storage: &mut dyn Storage) -> Singleton<Uint128> {
|
||||
singleton(storage, REWARD_POOL_PREFIX)
|
||||
}
|
||||
|
||||
pub fn reward_pool_value(storage: &dyn Storage) -> Uint128 {
|
||||
match reward_pool(storage).load() {
|
||||
Ok(value) => value,
|
||||
Err(_e) => Uint128(INITIAL_REWARD_POOL),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn incr_reward_pool(
|
||||
amount: Uint128,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Uint128, ContractError> {
|
||||
let stake = reward_pool_value(storage).saturating_add(amount);
|
||||
mut_reward_pool(storage).save(&stake)?;
|
||||
Ok(stake)
|
||||
}
|
||||
|
||||
pub fn decr_reward_pool(
|
||||
amount: Uint128,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Uint128, ContractError> {
|
||||
let stake = match reward_pool_value(storage).checked_sub(amount) {
|
||||
Ok(stake) => stake,
|
||||
Err(_e) => {
|
||||
return Err(ContractError::OutOfFunds {
|
||||
to_remove: amount.u128(),
|
||||
reward_pool: reward_pool_value(storage).u128(),
|
||||
})
|
||||
}
|
||||
};
|
||||
mut_reward_pool(storage).save(&stake)?;
|
||||
Ok(stake)
|
||||
}
|
||||
|
||||
pub fn circulating_supply(storage: &dyn Storage) -> Uint128 {
|
||||
let reward_pool = reward_pool_value(storage).u128();
|
||||
Uint128(TOTAL_SUPPLY - reward_pool)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,829 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
use crate::contract::INITIAL_REWARD_POOL;
|
||||
use crate::state::State;
|
||||
use crate::transactions::MINIMUM_BLOCK_AGE_FOR_REWARDING;
|
||||
use crate::{error::ContractError, queries};
|
||||
use config::defaults::TOTAL_SUPPLY;
|
||||
use cosmwasm_std::{Decimal, Order, StdResult, Storage, Uint128};
|
||||
use cosmwasm_storage::{
|
||||
bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
use mixnet_contract::mixnode::NodeRewardParams;
|
||||
use mixnet_contract::{
|
||||
Addr, GatewayBond, IdentityKey, IdentityKeyRef, Layer, LayerDistribution, MixNodeBond,
|
||||
RawDelegationData, StateParams,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
// storage prefixes
|
||||
// all of them must be unique and presumably not be a prefix of a different one
|
||||
// keeping them as short as possible is also desirable as they are part of each stored key
|
||||
// it's not as important for singletons, but is a nice optimisation for buckets
|
||||
|
||||
// singletons
|
||||
const CONFIG_KEY: &[u8] = b"config";
|
||||
const LAYER_DISTRIBUTION_KEY: &[u8] = b"layers";
|
||||
const REWARD_POOL_PREFIX: &[u8] = b"pool";
|
||||
|
||||
// buckets
|
||||
pub const PREFIX_MIXNODES: &[u8] = b"mn";
|
||||
const PREFIX_MIXNODES_OWNERS: &[u8] = b"mo";
|
||||
const PREFIX_GATEWAYS: &[u8] = b"gt";
|
||||
const PREFIX_GATEWAYS_OWNERS: &[u8] = b"go";
|
||||
|
||||
const PREFIX_MIX_DELEGATION: &[u8] = b"md";
|
||||
const PREFIX_REVERSE_MIX_DELEGATION: &[u8] = b"dm";
|
||||
|
||||
const PREFIX_REWARDED_MIXNODES: &[u8] = b"rm";
|
||||
|
||||
// Contract-level stuff
|
||||
|
||||
// TODO Unify bucket and mixnode storage functions
|
||||
pub fn generate_storage_key(address: &Addr, proxy: Option<&Addr>) -> Vec<u8> {
|
||||
if let Some(proxy) = &proxy {
|
||||
address
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.zip(proxy.as_bytes())
|
||||
.map(|(x, y)| x ^ y)
|
||||
.collect()
|
||||
} else {
|
||||
address.as_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(storage: &mut dyn Storage) -> Singleton<State> {
|
||||
singleton(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<State> {
|
||||
singleton_read(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
fn reward_pool(storage: &dyn Storage) -> ReadonlySingleton<Uint128> {
|
||||
singleton_read(storage, REWARD_POOL_PREFIX)
|
||||
}
|
||||
|
||||
pub fn mut_reward_pool(storage: &mut dyn Storage) -> Singleton<Uint128> {
|
||||
singleton(storage, REWARD_POOL_PREFIX)
|
||||
}
|
||||
|
||||
pub fn reward_pool_value(storage: &dyn Storage) -> Uint128 {
|
||||
match reward_pool(storage).load() {
|
||||
Ok(value) => value,
|
||||
Err(_e) => Uint128(INITIAL_REWARD_POOL),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn incr_reward_pool(
|
||||
amount: Uint128,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Uint128, ContractError> {
|
||||
let stake = reward_pool_value(storage).saturating_add(amount);
|
||||
mut_reward_pool(storage).save(&stake)?;
|
||||
Ok(stake)
|
||||
}
|
||||
|
||||
pub fn decr_reward_pool(
|
||||
amount: Uint128,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Uint128, ContractError> {
|
||||
let stake = match reward_pool_value(storage).checked_sub(amount) {
|
||||
Ok(stake) => stake,
|
||||
Err(_e) => {
|
||||
return Err(ContractError::OutOfFunds {
|
||||
to_remove: amount.u128(),
|
||||
reward_pool: reward_pool_value(storage).u128(),
|
||||
})
|
||||
}
|
||||
};
|
||||
mut_reward_pool(storage).save(&stake)?;
|
||||
Ok(stake)
|
||||
}
|
||||
|
||||
pub fn circulating_supply(storage: &dyn Storage) -> Uint128 {
|
||||
let reward_pool = reward_pool_value(storage).u128();
|
||||
Uint128(TOTAL_SUPPLY - reward_pool)
|
||||
}
|
||||
|
||||
pub(crate) fn read_state_params(storage: &dyn Storage) -> StateParams {
|
||||
// note: In any other case, I wouldn't have attempted to unwrap this result, but in here
|
||||
// if we fail to load the stored state we would already be in the undefined behaviour land,
|
||||
// so we better just blow up immediately.
|
||||
config_read(storage).load().unwrap().params
|
||||
}
|
||||
|
||||
pub fn layer_distribution(storage: &mut dyn Storage) -> Singleton<LayerDistribution> {
|
||||
singleton(storage, LAYER_DISTRIBUTION_KEY)
|
||||
}
|
||||
|
||||
pub fn layer_distribution_read(storage: &dyn Storage) -> ReadonlySingleton<LayerDistribution> {
|
||||
singleton_read(storage, LAYER_DISTRIBUTION_KEY)
|
||||
}
|
||||
|
||||
pub(crate) fn read_layer_distribution(storage: &dyn Storage) -> LayerDistribution {
|
||||
// note: In any other case, I wouldn't have attempted to unwrap this result, but in here
|
||||
// if we fail to load the stored state we would already be in the undefined behaviour land,
|
||||
// so we better just blow up immediately.
|
||||
layer_distribution_read(storage).load().unwrap()
|
||||
}
|
||||
|
||||
pub fn increment_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> {
|
||||
let mut distribution = layer_distribution(storage).load()?;
|
||||
match layer {
|
||||
Layer::Gateway => distribution.gateways += 1,
|
||||
Layer::One => distribution.layer1 += 1,
|
||||
Layer::Two => distribution.layer2 += 1,
|
||||
Layer::Three => distribution.layer3 += 1,
|
||||
}
|
||||
layer_distribution(storage).save(&distribution)
|
||||
}
|
||||
|
||||
pub fn decrement_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> {
|
||||
let mut distribution = layer_distribution(storage).load()?;
|
||||
// It can't possibly go below zero, if it does, it means there's a serious error in the contract logic
|
||||
match layer {
|
||||
Layer::Gateway => {
|
||||
distribution.gateways = distribution
|
||||
.gateways
|
||||
.checked_sub(1)
|
||||
.expect("tried to subtract from unsigned zero!")
|
||||
}
|
||||
Layer::One => {
|
||||
distribution.layer1 = distribution
|
||||
.layer1
|
||||
.checked_sub(1)
|
||||
.expect("tried to subtract from unsigned zero!")
|
||||
}
|
||||
Layer::Two => {
|
||||
distribution.layer2 = distribution
|
||||
.layer2
|
||||
.checked_sub(1)
|
||||
.expect("tried to subtract from unsigned zero!")
|
||||
}
|
||||
Layer::Three => {
|
||||
distribution.layer3 = distribution
|
||||
.layer3
|
||||
.checked_sub(1)
|
||||
.expect("tried to subtract from unsigned zero!")
|
||||
}
|
||||
};
|
||||
layer_distribution(storage).save(&distribution)
|
||||
}
|
||||
|
||||
// Mixnode-related stuff
|
||||
|
||||
pub fn mixnodes(storage: &mut dyn Storage) -> Bucket<MixNodeBond> {
|
||||
bucket(storage, PREFIX_MIXNODES)
|
||||
}
|
||||
|
||||
pub fn mixnodes_read(storage: &dyn Storage) -> ReadonlyBucket<MixNodeBond> {
|
||||
bucket_read(storage, PREFIX_MIXNODES)
|
||||
}
|
||||
|
||||
// owner address -> node identity
|
||||
pub fn mixnodes_owners(storage: &mut dyn Storage) -> Bucket<IdentityKey> {
|
||||
bucket(storage, PREFIX_MIXNODES_OWNERS)
|
||||
}
|
||||
|
||||
pub fn mixnodes_owners_read(storage: &dyn Storage) -> ReadonlyBucket<IdentityKey> {
|
||||
bucket_read(storage, PREFIX_MIXNODES_OWNERS)
|
||||
}
|
||||
|
||||
// we want to treat this bucket as a set so we don't really care about what type of data is being stored.
|
||||
// I went with u8 as after serialization it takes only a single byte of space, while if a `()` was used,
|
||||
// it would have taken 4 bytes (representation of 'null')
|
||||
pub fn rewarded_mixnodes(storage: &mut dyn Storage, rewarding_interval_nonce: u32) -> Bucket<u8> {
|
||||
Bucket::multilevel(
|
||||
storage,
|
||||
&[
|
||||
rewarding_interval_nonce.to_be_bytes().as_ref(),
|
||||
PREFIX_REWARDED_MIXNODES,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// we want to treat this bucket as a set so we don't really care about what type of data is being stored.
|
||||
// I went with u8 as after serialization it takes only a single byte of space, while if a `()` was used,
|
||||
// it would have taken 4 bytes (representation of 'null')
|
||||
pub fn rewarded_mixnodes_read(
|
||||
storage: &dyn Storage,
|
||||
rewarding_interval_nonce: u32,
|
||||
) -> ReadonlyBucket<u8> {
|
||||
ReadonlyBucket::multilevel(
|
||||
storage,
|
||||
&[
|
||||
rewarding_interval_nonce.to_be_bytes().as_ref(),
|
||||
PREFIX_REWARDED_MIXNODES,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// helpers
|
||||
pub(crate) fn increase_mix_delegated_stakes(
|
||||
storage: &mut dyn Storage,
|
||||
mix_identity: IdentityKeyRef,
|
||||
scaled_reward_rate: Decimal,
|
||||
reward_blockstamp: u64,
|
||||
) -> StdResult<Uint128> {
|
||||
let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize;
|
||||
|
||||
let mut total_rewarded = Uint128::zero();
|
||||
let mut chunk_start: Option<Vec<_>> = None;
|
||||
loop {
|
||||
// get `chunk_size` of delegations
|
||||
let delegations_chunk = mix_delegations_read(storage, mix_identity)
|
||||
.range(chunk_start.as_deref(), None, Order::Ascending)
|
||||
.take(chunk_size)
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
if delegations_chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// append 0 byte to the last value to start with whatever is the next succeeding key
|
||||
chunk_start = Some(
|
||||
delegations_chunk
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(0u8))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// and for each of them increase the stake proportionally to the reward
|
||||
// if at least `MINIMUM_BLOCK_AGE_FOR_REWARDING` blocks have been created
|
||||
// since they delegated
|
||||
for (delegator_address, mut delegation) in delegations_chunk.into_iter() {
|
||||
if delegation.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= reward_blockstamp {
|
||||
let reward = delegation.amount * scaled_reward_rate;
|
||||
delegation.amount += reward;
|
||||
total_rewarded += reward;
|
||||
mix_delegations(storage, mix_identity).save(&delegator_address, &delegation)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_rewarded)
|
||||
}
|
||||
|
||||
pub(crate) fn increase_mix_delegated_stakes_v2(
|
||||
storage: &mut dyn Storage,
|
||||
bond: &MixNodeBond,
|
||||
params: &NodeRewardParams,
|
||||
) -> Result<Uint128, ContractError> {
|
||||
let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize;
|
||||
|
||||
let mut total_rewarded = Uint128::zero();
|
||||
let mut chunk_start: Option<Vec<_>> = None;
|
||||
loop {
|
||||
// get `chunk_size` of delegations
|
||||
let delegations_chunk = mix_delegations_read(storage, bond.identity())
|
||||
.range(chunk_start.as_deref(), None, Order::Ascending)
|
||||
.take(chunk_size)
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
if delegations_chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// append 0 byte to the last value to start with whatever is the next succeeding key
|
||||
chunk_start = Some(
|
||||
delegations_chunk
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(0u8))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
// and for each of them increase the stake proportionally to the reward
|
||||
// if at least `MINIMUM_BLOCK_AGE_FOR_REWARDING` blocks have been created
|
||||
// since they delegated
|
||||
for (delegator_address, mut delegation) in delegations_chunk.into_iter() {
|
||||
if delegation.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING
|
||||
<= params.reward_blockstamp()
|
||||
{
|
||||
let reward = bond.reward_delegation(delegation.amount, params);
|
||||
delegation.amount += Uint128(reward);
|
||||
total_rewarded += Uint128(reward);
|
||||
mix_delegations(storage, bond.identity()).save(&delegator_address, &delegation)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(total_rewarded)
|
||||
}
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_mixnode_bond(
|
||||
storage: &dyn Storage,
|
||||
identity: &[u8],
|
||||
) -> StdResult<cosmwasm_std::Uint128> {
|
||||
let bucket = mixnodes_read(storage);
|
||||
let node = bucket.load(identity)?;
|
||||
Ok(node.bond_amount.amount)
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_mixnode_delegation(
|
||||
storage: &dyn Storage,
|
||||
identity: &[u8],
|
||||
) -> StdResult<cosmwasm_std::Uint128> {
|
||||
let bucket = mixnodes_read(storage);
|
||||
let node = bucket.load(identity)?;
|
||||
Ok(node.total_delegation.amount)
|
||||
}
|
||||
|
||||
// Gateway-related stuff
|
||||
|
||||
pub fn gateways(storage: &mut dyn Storage) -> Bucket<GatewayBond> {
|
||||
bucket(storage, PREFIX_GATEWAYS)
|
||||
}
|
||||
|
||||
pub fn gateways_read(storage: &dyn Storage) -> ReadonlyBucket<GatewayBond> {
|
||||
bucket_read(storage, PREFIX_GATEWAYS)
|
||||
}
|
||||
|
||||
// owner address -> node identity
|
||||
pub fn gateways_owners(storage: &mut dyn Storage) -> Bucket<IdentityKey> {
|
||||
bucket(storage, PREFIX_GATEWAYS_OWNERS)
|
||||
}
|
||||
|
||||
pub fn gateways_owners_read(storage: &dyn Storage) -> ReadonlyBucket<IdentityKey> {
|
||||
bucket_read(storage, PREFIX_GATEWAYS_OWNERS)
|
||||
}
|
||||
|
||||
// delegation related
|
||||
pub fn all_mix_delegations_read<T>(storage: &dyn Storage) -> ReadonlyBucket<T>
|
||||
where
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
bucket_read(storage, PREFIX_MIX_DELEGATION)
|
||||
}
|
||||
|
||||
pub fn mix_delegations<'a>(
|
||||
storage: &'a mut dyn Storage,
|
||||
mix_identity: IdentityKeyRef,
|
||||
) -> Bucket<'a, RawDelegationData> {
|
||||
Bucket::multilevel(storage, &[PREFIX_MIX_DELEGATION, mix_identity.as_bytes()])
|
||||
}
|
||||
|
||||
pub fn mix_delegations_read<'a>(
|
||||
storage: &'a dyn Storage,
|
||||
mix_identity: IdentityKeyRef,
|
||||
) -> ReadonlyBucket<'a, RawDelegationData> {
|
||||
ReadonlyBucket::multilevel(storage, &[PREFIX_MIX_DELEGATION, mix_identity.as_bytes()])
|
||||
}
|
||||
|
||||
pub fn reverse_mix_delegations<'a>(storage: &'a mut dyn Storage, owner: &Addr) -> Bucket<'a, ()> {
|
||||
Bucket::multilevel(storage, &[PREFIX_REVERSE_MIX_DELEGATION, owner.as_bytes()])
|
||||
}
|
||||
|
||||
pub fn reverse_mix_delegations_read<'a>(
|
||||
storage: &'a dyn Storage,
|
||||
owner: &Addr,
|
||||
) -> ReadonlyBucket<'a, ()> {
|
||||
ReadonlyBucket::multilevel(storage, &[PREFIX_REVERSE_MIX_DELEGATION, owner.as_bytes()])
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_gateway_bond(
|
||||
storage: &dyn Storage,
|
||||
identity: &[u8],
|
||||
) -> StdResult<cosmwasm_std::Uint128> {
|
||||
let bucket = gateways_read(storage);
|
||||
let node = bucket.load(identity)?;
|
||||
Ok(node.bond_amount.amount)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::helpers::identity_and_owner_to_bytes;
|
||||
use crate::support::tests::helpers::{
|
||||
gateway_bond_fixture, gateway_fixture, mix_node_fixture, mixnode_bond_fixture,
|
||||
raw_delegation_fixture,
|
||||
};
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::testing::{mock_dependencies, MockStorage};
|
||||
use cosmwasm_std::{coin, Addr, Uint128};
|
||||
use mixnet_contract::{Gateway, MixNode};
|
||||
|
||||
#[test]
|
||||
fn mixnode_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = mixnode_bond_fixture();
|
||||
let bond2 = mixnode_bond_fixture();
|
||||
mixnodes(&mut storage).save(b"bond1", &bond1).unwrap();
|
||||
mixnodes(&mut storage).save(b"bond2", &bond2).unwrap();
|
||||
|
||||
let res1 = mixnodes_read(&storage).load(b"bond1").unwrap();
|
||||
let res2 = mixnodes_read(&storage).load(b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = gateway_bond_fixture();
|
||||
let bond2 = gateway_bond_fixture();
|
||||
gateways(&mut storage).save(b"bond1", &bond1).unwrap();
|
||||
gateways(&mut storage).save(b"bond2", &bond2).unwrap();
|
||||
|
||||
let res1 = gateways_read(&storage).load(b"bond1").unwrap();
|
||||
let res2 = gateways_read(&storage).load(b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_mixnode_bond() {
|
||||
let mut storage = MockStorage::new();
|
||||
let node_owner: Addr = Addr::unchecked("node-owner");
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
|
||||
// produces an error if target mixnode doesn't exist
|
||||
let res = read_mixnode_bond(&storage, node_owner.as_bytes());
|
||||
assert!(res.is_err());
|
||||
|
||||
// returns appropriate value otherwise
|
||||
let bond_value = 1000;
|
||||
|
||||
let mixnode_bond = MixNodeBond {
|
||||
bond_amount: coin(bond_value, DENOM),
|
||||
total_delegation: coin(0, DENOM),
|
||||
owner: node_owner.clone(),
|
||||
layer: Layer::One,
|
||||
block_height: 12_345,
|
||||
mix_node: MixNode {
|
||||
identity_key: node_identity.clone(),
|
||||
..mix_node_fixture()
|
||||
},
|
||||
profit_margin_percent: Some(10),
|
||||
proxy: None,
|
||||
};
|
||||
|
||||
mixnodes(&mut storage)
|
||||
.save(node_identity.as_bytes(), &mixnode_bond)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(bond_value),
|
||||
read_mixnode_bond(&storage, node_identity.as_bytes()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[test]
|
||||
fn reading_gateway_bond() {
|
||||
let mut storage = MockStorage::new();
|
||||
let node_owner: Addr = Addr::unchecked("node-owner");
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
|
||||
// produces an error if target gateway doesn't exist
|
||||
let res = read_gateway_bond(&storage, node_owner.as_bytes());
|
||||
assert!(res.is_err());
|
||||
|
||||
// returns appropriate value otherwise
|
||||
let bond_value = 1000;
|
||||
|
||||
let gateway_bond = GatewayBond {
|
||||
bond_amount: coin(bond_value, DENOM),
|
||||
owner: node_owner.clone(),
|
||||
block_height: 12_345,
|
||||
gateway: Gateway {
|
||||
identity_key: node_identity.clone(),
|
||||
..gateway_fixture()
|
||||
},
|
||||
};
|
||||
|
||||
gateways(&mut storage)
|
||||
.save(node_identity.as_bytes(), &gateway_bond)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(bond_value),
|
||||
read_gateway_bond(&storage, node_identity.as_bytes()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_mixnode_delegations_read_retrieval() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar1");
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
let raw_delegation1 = RawDelegationData::new(1u128.into(), 1000, None);
|
||||
let raw_delegation2 = RawDelegationData::new(2u128.into(), 2000, None);
|
||||
|
||||
mix_delegations(&mut deps.storage, &node_identity1)
|
||||
.save(delegation_owner1.as_bytes(), &raw_delegation1)
|
||||
.unwrap();
|
||||
mix_delegations(&mut deps.storage, &node_identity2)
|
||||
.save(delegation_owner2.as_bytes(), &raw_delegation2)
|
||||
.unwrap();
|
||||
|
||||
let res1 = all_mix_delegations_read::<RawDelegationData>(&deps.storage)
|
||||
.load(&*identity_and_owner_to_bytes(
|
||||
&node_identity1,
|
||||
&delegation_owner1,
|
||||
))
|
||||
.unwrap();
|
||||
let res2 = all_mix_delegations_read::<RawDelegationData>(&deps.storage)
|
||||
.load(&*identity_and_owner_to_bytes(
|
||||
&node_identity2,
|
||||
&delegation_owner2,
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(raw_delegation1, res1);
|
||||
assert_eq!(raw_delegation2, res2);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod increasing_mix_delegated_stakes {
|
||||
use super::*;
|
||||
use crate::queries::query_mixnode_delegations_paged;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
|
||||
#[test]
|
||||
fn when_there_are_no_delegations() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
42,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// there was no increase
|
||||
assert!(total_increase.is_zero());
|
||||
|
||||
// there are no 'new' delegations magically added
|
||||
assert!(
|
||||
query_mixnode_delegations_paged(deps.as_ref(), node_identity, None, None)
|
||||
.unwrap()
|
||||
.delegations
|
||||
.is_empty()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_is_a_single_delegation() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
let delegator_address = Addr::unchecked("bob");
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp, None),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Uint128(1), total_increase);
|
||||
|
||||
// amount is incremented, block height remains the same
|
||||
assert_eq!(
|
||||
RawDelegationData::new(1001u128.into(), 42, None),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_is_a_single_delegation_depending_on_blockstamp() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
let delegator_address = Addr::unchecked("bob");
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp, None),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING - 1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// there was no increase
|
||||
assert!(total_increase.is_zero());
|
||||
|
||||
// amount is not incremented
|
||||
assert_eq!(
|
||||
RawDelegationData::new(1000u128.into(), delegation_blockstamp, None),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// there is an increase now, that the lock period has passed
|
||||
assert_eq!(Uint128(1), total_increase);
|
||||
|
||||
// amount is incremented
|
||||
assert_eq!(
|
||||
RawDelegationData::new(1001u128.into(), delegation_blockstamp, None),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_are_multiple_delegations() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
for i in 0..100 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp, None),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(Uint128(100), total_increase);
|
||||
|
||||
for i in 0..100 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
assert_eq!(
|
||||
raw_delegation_fixture(1001),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_there_are_more_delegations_than_page_size() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let node_identity: IdentityKey = "nodeidentity".into();
|
||||
let delegation_blockstamp = 42;
|
||||
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
mix_delegations(&mut deps.storage, &node_identity)
|
||||
.save(
|
||||
delegator_address.as_bytes(),
|
||||
&RawDelegationData::new(1000u128.into(), delegation_blockstamp, None),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let total_increase = increase_mix_delegated_stakes(
|
||||
&mut deps.storage,
|
||||
node_identity.as_ref(),
|
||||
reward,
|
||||
delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(queries::DELEGATION_PAGE_MAX_LIMIT as u128 * 10),
|
||||
total_increase
|
||||
);
|
||||
|
||||
for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 {
|
||||
let delegator_address = Addr::unchecked(format!("address{}", i));
|
||||
assert_eq!(
|
||||
raw_delegation_fixture(1001),
|
||||
mix_delegations_read(&mut deps.storage, &node_identity)
|
||||
.load(delegator_address.as_bytes())
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod reverse_mix_delegations {
|
||||
use super::*;
|
||||
use crate::support::tests::helpers;
|
||||
|
||||
#[test]
|
||||
fn reverse_mix_delegation_exists() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node_identity: IdentityKey = "foo".into();
|
||||
let delegation_owner = Addr::unchecked("bar");
|
||||
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner)
|
||||
.save(node_identity.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
reverse_mix_delegations_read(deps.as_ref().storage, &delegation_owner)
|
||||
.may_load(node_identity.as_bytes())
|
||||
.unwrap()
|
||||
.is_some(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_mix_delegation_returns_none_if_delegation_doesnt_exist() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
let node_identity1: IdentityKey = "foo1".into();
|
||||
let node_identity2: IdentityKey = "foo2".into();
|
||||
let delegation_owner1 = Addr::unchecked("bar");
|
||||
let delegation_owner2 = Addr::unchecked("bar2");
|
||||
|
||||
assert!(
|
||||
reverse_mix_delegations_read(deps.as_ref().storage, &delegation_owner1)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// add delegation for a different node
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner1)
|
||||
.save(node_identity2.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
reverse_mix_delegations_read(deps.as_ref().storage, &delegation_owner1)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
// add delegation from a different owner
|
||||
reverse_mix_delegations(&mut deps.storage, &delegation_owner2)
|
||||
.save(node_identity1.as_bytes(), &())
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
reverse_mix_delegations_read(deps.as_ref().storage, &delegation_owner1)
|
||||
.may_load(node_identity1.as_bytes())
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,43 @@
|
||||
#[cfg(test)]
|
||||
pub mod helpers {
|
||||
pub mod test_helpers {
|
||||
use super::*;
|
||||
use crate::contract::query;
|
||||
use crate::contract::{instantiate, INITIAL_MIXNODE_BOND};
|
||||
use crate::transactions::{try_add_gateway, try_add_mixnode};
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::from_binary;
|
||||
use crate::contract::{
|
||||
query, DEFAULT_SYBIL_RESISTANCE_PERCENT, EPOCH_REWARD_PERCENT, INITIAL_REWARD_POOL,
|
||||
};
|
||||
use crate::gateways::transactions::try_add_gateway;
|
||||
use crate::mixnodes::bonding_transactions::try_add_mixnode;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::mixnodes::storage::StoredMixnodeBond;
|
||||
use config::defaults::{DENOM, TOTAL_SUPPLY};
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::testing::MockQuerier;
|
||||
use cosmwasm_std::testing::MockStorage;
|
||||
use cosmwasm_std::Addr;
|
||||
use cosmwasm_std::Coin;
|
||||
use cosmwasm_std::OwnedDeps;
|
||||
use cosmwasm_std::{coin, Uint128};
|
||||
use cosmwasm_std::{from_binary, DepsMut};
|
||||
use cosmwasm_std::{Addr, StdResult, Storage};
|
||||
use cosmwasm_std::{Empty, MemoryStorage};
|
||||
use mixnet_contract::mixnode::NodeRewardParams;
|
||||
use mixnet_contract::{
|
||||
Gateway, GatewayBond, InstantiateMsg, Layer, MixNode, MixNodeBond, PagedGatewayResponse,
|
||||
PagedMixnodeResponse, QueryMsg, RawDelegationData,
|
||||
Gateway, GatewayBond, IdentityKeyRef, InstantiateMsg, Layer, MixNode, MixNodeBond,
|
||||
PagedGatewayResponse, PagedMixnodeResponse, QueryMsg, RawDelegationData,
|
||||
};
|
||||
|
||||
pub fn add_mixnode(
|
||||
sender: &str,
|
||||
stake: Vec<Coin>,
|
||||
deps: &mut OwnedDeps<MockStorage, MockApi, MockQuerier>,
|
||||
) -> String {
|
||||
pub fn add_mixnode(sender: &str, stake: Vec<Coin>, deps: DepsMut) -> String {
|
||||
let info = mock_info(sender, &stake);
|
||||
let key = format!("{}mixnode", sender);
|
||||
try_add_mixnode(
|
||||
deps.as_mut(),
|
||||
deps,
|
||||
mock_env(),
|
||||
info,
|
||||
MixNode {
|
||||
identity_key: key.clone(),
|
||||
..helpers::mix_node_fixture()
|
||||
..test_helpers::mix_node_fixture()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -72,7 +74,7 @@ pub mod helpers {
|
||||
info,
|
||||
Gateway {
|
||||
identity_key: key.clone(),
|
||||
..helpers::gateway_fixture()
|
||||
..test_helpers::gateway_fixture()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -102,7 +104,7 @@ pub mod helpers {
|
||||
let env = mock_env();
|
||||
let info = mock_info("creator", &[]);
|
||||
instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
|
||||
return deps;
|
||||
deps
|
||||
}
|
||||
|
||||
pub fn mix_node_fixture() -> MixNode {
|
||||
@@ -134,7 +136,20 @@ pub mod helpers {
|
||||
12_345,
|
||||
mix_node,
|
||||
None,
|
||||
None
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn stored_mixnode_bond_fixture(owner: &str) -> mixnodes_storage::StoredMixnodeBond {
|
||||
StoredMixnodeBond::new(
|
||||
coin(50, DENOM),
|
||||
Addr::unchecked(owner),
|
||||
Layer::One,
|
||||
12_345,
|
||||
MixNode {
|
||||
identity_key: format!("id-{}", owner),
|
||||
..mix_node_fixture()
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -165,7 +180,7 @@ pub mod helpers {
|
||||
}
|
||||
|
||||
pub fn raw_delegation_fixture(amount: u128) -> RawDelegationData {
|
||||
RawDelegationData::new(Uint128(amount), 42, None)
|
||||
RawDelegationData::new(Uint128(amount), 42)
|
||||
}
|
||||
|
||||
pub fn query_contract_balance(
|
||||
@@ -189,4 +204,35 @@ pub mod helpers {
|
||||
amount: INITIAL_MIXNODE_BOND,
|
||||
}]
|
||||
}
|
||||
|
||||
// when exact values are irrelevant and what matters is the action of rewarding
|
||||
pub fn node_rewarding_params_fixture(uptime: u128) -> NodeRewardParams {
|
||||
NodeRewardParams::new(
|
||||
(INITIAL_REWARD_POOL / 100) * EPOCH_REWARD_PERCENT as u128,
|
||||
50 as u128,
|
||||
0,
|
||||
TOTAL_SUPPLY - INITIAL_REWARD_POOL,
|
||||
uptime,
|
||||
DEFAULT_SYBIL_RESISTANCE_PERCENT,
|
||||
)
|
||||
}
|
||||
|
||||
// Converts the node identity and owner of a delegation into the bytes used as
|
||||
// key in the delegation buckets. Basically a helper function.
|
||||
pub(crate) fn identity_and_owner_to_bytes(identity: &str, owner: &Addr) -> Vec<u8> {
|
||||
let mut bytes = u16::to_be_bytes(identity.len() as u16).to_vec();
|
||||
bytes.append(&mut identity.as_bytes().to_vec());
|
||||
bytes.append(&mut owner.as_bytes().to_vec());
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
pub(crate) fn read_mixnode_bond_amount(
|
||||
storage: &dyn Storage,
|
||||
identity: IdentityKeyRef,
|
||||
) -> StdResult<cosmwasm_std::Uint128> {
|
||||
let node = mixnodes_storage::mixnodes().load(storage, identity.as_bytes())?;
|
||||
Ok(node.bond_amount.amount)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Generated
-908
@@ -1,908 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "az"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d6dff4a1892b54d70af377bf7a17064192e822865791d812957f21e3108c325"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"byte-tools",
|
||||
"byteorder",
|
||||
"generic-array 0.12.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
||||
dependencies = [
|
||||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
|
||||
dependencies = [
|
||||
"byte-tools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byte-tools"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"network-defaults",
|
||||
"serde",
|
||||
"toml",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b"
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-crypto"
|
||||
version = "0.14.1"
|
||||
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"ed25519-zebra",
|
||||
"k256",
|
||||
"rand_core 0.5.1",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-derive"
|
||||
version = "0.14.1"
|
||||
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
|
||||
dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-std"
|
||||
version = "0.14.1"
|
||||
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cosmwasm-crypto",
|
||||
"cosmwasm-derive",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"thiserror",
|
||||
"uint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-storage"
|
||||
version = "0.14.1"
|
||||
source = "git+https://github.com/jstuczyn/cosmwasm?branch=0.14.1-updatedk256#308781cd5f33b0e996176c6b500793d3648c0f7b"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03"
|
||||
dependencies = [
|
||||
"generic-array 0.14.4",
|
||||
"rand_core 0.6.3",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-mac"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
|
||||
dependencies = [
|
||||
"generic-array 0.14.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"digest 0.9.0",
|
||||
"rand_core 0.5.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28e98c534e9c8a0483aa01d6f6913bc063de254311bd267c9cf535e9b70e15b2"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
||||
dependencies = [
|
||||
"generic-array 0.12.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
|
||||
dependencies = [
|
||||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372"
|
||||
dependencies = [
|
||||
"der",
|
||||
"elliptic-curve",
|
||||
"hmac",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-zebra"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"hex",
|
||||
"rand_core 0.5.1",
|
||||
"serde",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b"
|
||||
dependencies = [
|
||||
"crypto-bigint",
|
||||
"ff",
|
||||
"generic-array 0.14.4",
|
||||
"group",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.3",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fake-simd"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f"
|
||||
dependencies = [
|
||||
"rand_core 0.6.3",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixed"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d333a26ec13a023c6dff4b7584de4d323cfee2e508f5dd2bbee6669e4f7efdf"
|
||||
dependencies = [
|
||||
"az",
|
||||
"bytemuck",
|
||||
"half",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core 0.6.3",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
|
||||
|
||||
[[package]]
|
||||
name = "handlebars"
|
||||
version = "3.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"quick-error",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-literal"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
|
||||
dependencies = [
|
||||
"crypto-mac",
|
||||
"digest 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "humantime-serde"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac34a56cfd4acddb469cc7fff187ed5ac36f498ba085caf8bbc725e3ff474058"
|
||||
dependencies = [
|
||||
"humantime",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||
|
||||
[[package]]
|
||||
name = "mixnet-contract"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"az",
|
||||
"cosmwasm-std",
|
||||
"fixed",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "network-defaults"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hex-literal",
|
||||
"serde",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
|
||||
dependencies = [
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
|
||||
dependencies = [
|
||||
"maplit",
|
||||
"pest",
|
||||
"sha-1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
dependencies = [
|
||||
"getrandom 0.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271ac0c667b8229adf70f0f957697c96fafd7486ab7481e15dc5e45e3e6a4368"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ebda811090b257411540779860bc09bf321bc587f58d2c5864309d1566214e7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-json-wasm"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50eef3672ec8fa45f3457fd423ba131117786784a895548021976117c1ded449"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
|
||||
dependencies = [
|
||||
"block-buffer 0.7.3",
|
||||
"digest 0.8.1",
|
||||
"fake-simd",
|
||||
"opaque-debug 0.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32"
|
||||
dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6"
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||
|
||||
[[package]]
|
||||
name = "uint"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crunchy",
|
||||
"hex",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"matches",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
||||
|
||||
[[package]]
|
||||
name = "vesting-contracts"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"config",
|
||||
"cosmwasm-std",
|
||||
"cosmwasm-storage",
|
||||
"mixnet-contract",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619"
|
||||
@@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "vesting-contract"
|
||||
version = "0.1.0"
|
||||
authors = ["Drazen Urch <durch@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
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.
|
||||
"contract.wasm",
|
||||
"hash.txt",
|
||||
]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
# for more explicit tests, cargo test --features=backtraces
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
|
||||
[dependencies]
|
||||
mixnet-contract = { path = "../../common/mixnet-contract" }
|
||||
config = { path = "../../common/config" }
|
||||
|
||||
# this branch is identical to 0.14.1 with addition of updated k256 dependency required to help poor cargo choose correct version
|
||||
cosmwasm-std = { version = "1.0.0-beta2", features = ["iterator"]}
|
||||
cosmwasm-storage = { version = "1.0.0-beta2", features = ["iterator"]}
|
||||
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
thiserror = { version = "1.0.23" }
|
||||
@@ -1,2 +0,0 @@
|
||||
wasm:
|
||||
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
|
||||
@@ -1,11 +0,0 @@
|
||||
## Nym vesting contract
|
||||
|
||||
1. Initial vesting tokens are deposited and assigned to existing addresses via `CreatePeriodicVestingAccount`
|
||||
2. Admin account can then delegate vested and unvested tokens to mixnodes on behalf of vesting accounts
|
||||
3. Vesting accounts can withdraw vested and undelegated (spendable) coins to their addresses
|
||||
|
||||
### Vesting coin delegation flow
|
||||
|
||||

|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 699 KiB |
@@ -1,437 +0,0 @@
|
||||
use crate::errors::ContractError;
|
||||
use crate::messages::{ExecuteMsg, InitMsg, QueryMsg};
|
||||
use crate::storage::{get_account, get_account_balance, set_account_balance};
|
||||
use crate::vesting::{
|
||||
populate_vesting_periods, BondingAccount, DelegationAccount, PeriodicVestingAccount,
|
||||
VestingAccount,
|
||||
};
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{
|
||||
attr, entry_point, to_binary, Addr, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo,
|
||||
QueryResponse, Response, Timestamp, Uint128,
|
||||
};
|
||||
use mixnet_contract::{IdentityKey, MixNode};
|
||||
|
||||
pub const NUM_VESTING_PERIODS: usize = 8;
|
||||
pub const VESTING_PERIOD: u64 = 3 * 30 * 86400;
|
||||
pub const ADMIN_ADDRESS: &str = "admin";
|
||||
|
||||
// TODO: Validate vesting/vested withdraw mathematics
|
||||
// TODO: Try and get to the bottom of multilevel bucket vs vector and performance of the whole storage thing
|
||||
|
||||
/// 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: InitMsg,
|
||||
) -> 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::DelegateToMixnode { mix_identity } => {
|
||||
try_delegate_to_mixnode(mix_identity, info, env, deps)
|
||||
}
|
||||
ExecuteMsg::UndelegateFromMixnode { mix_identity } => {
|
||||
try_undelegate_from_mixnode(mix_identity, info, deps)
|
||||
}
|
||||
ExecuteMsg::CreatePeriodicVestingAccount {
|
||||
address,
|
||||
start_time,
|
||||
} => try_create_periodic_vesting_account(address, start_time, info, env, deps),
|
||||
ExecuteMsg::WithdrawVestedCoins { amount } => {
|
||||
try_withdraw_vested_coins(amount, env, info, deps)
|
||||
}
|
||||
ExecuteMsg::TrackUndelegation {
|
||||
owner,
|
||||
mix_identity,
|
||||
amount,
|
||||
} => try_track_undelegation(owner, mix_identity, amount, deps),
|
||||
ExecuteMsg::BondMixnode { mix_node } => try_bond_mixnode(mix_node, info, env, deps),
|
||||
ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps),
|
||||
ExecuteMsg::TrackUnbond { owner, amount } => try_track_unbond(owner, amount, deps),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_bond_mixnode(
|
||||
mix_node: MixNode,
|
||||
info: MessageInfo,
|
||||
env: Env,
|
||||
deps: DepsMut,
|
||||
) -> Result<Response, ContractError> {
|
||||
let owner = deps.api.addr_validate(info.sender.as_str())?;
|
||||
let bond = validate_funds(&info.funds)?;
|
||||
if let Some(account) = get_account(deps.storage, &owner) {
|
||||
account.try_bond_mixnode(mix_node, bond, &env, deps.storage)
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(
|
||||
owner.as_str().to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_unbond_mixnode(info: MessageInfo, deps: DepsMut) -> Result<Response, ContractError> {
|
||||
let owner = deps.api.addr_validate(info.sender.as_str())?;
|
||||
if let Some(account) = get_account(deps.storage, &owner) {
|
||||
account.try_unbond_mixnode()
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(
|
||||
owner.as_str().to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_track_unbond(
|
||||
owner: Addr,
|
||||
amount: Coin,
|
||||
deps: DepsMut,
|
||||
) -> Result<Response, ContractError> {
|
||||
let owner = deps.api.addr_validate(owner.as_str())?;
|
||||
if let Some(account) = get_account(deps.storage, &owner) {
|
||||
account.track_unbond(amount, deps.storage)?;
|
||||
Ok(Response::default())
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(
|
||||
owner.as_str().to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_withdraw_vested_coins(
|
||||
amount: Coin,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
deps: DepsMut,
|
||||
) -> Result<Response, ContractError> {
|
||||
let address = info.sender;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
let spendable_coins = account.spendable_coins(None, &env, deps.storage)?;
|
||||
if amount.amount < spendable_coins.amount {
|
||||
if let Some(balance) = get_account_balance(deps.storage, &address) {
|
||||
let new_balance = balance.u128().saturating_sub(amount.amount.u128());
|
||||
set_account_balance(deps.storage, &address, Uint128::new(new_balance))?;
|
||||
} else {
|
||||
return Err(ContractError::NoBalanceForAddress(
|
||||
address.as_str().to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let send_tokens = BankMsg::Send {
|
||||
to_address: address.as_str().to_string(),
|
||||
amount: vec![amount],
|
||||
};
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "whitdraw")
|
||||
.add_message(send_tokens))
|
||||
} else {
|
||||
Err(ContractError::InsufficientSpendable(
|
||||
address.as_str().to_string(),
|
||||
spendable_coins.amount.u128(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
return Err(ContractError::NoAccountForAddress(
|
||||
address.as_str().to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn try_track_undelegation(
|
||||
address: Addr,
|
||||
mix_identity: IdentityKey,
|
||||
amount: Coin,
|
||||
deps: DepsMut,
|
||||
) -> Result<Response, ContractError> {
|
||||
let adddress = deps.api.addr_validate(address.as_str())?;
|
||||
if let Some(account) = get_account(deps.storage, &adddress) {
|
||||
account.track_undelegation(mix_identity, amount, deps.storage)?;
|
||||
Ok(Response::default())
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(
|
||||
address.as_str().to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_delegate_to_mixnode(
|
||||
mix_identity: IdentityKey,
|
||||
info: MessageInfo,
|
||||
env: Env,
|
||||
deps: DepsMut,
|
||||
) -> Result<Response, ContractError> {
|
||||
let delegate_addr = info.sender;
|
||||
let amount = validate_funds(&info.funds)?;
|
||||
let address = deps.api.addr_validate(delegate_addr.as_str())?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
account.try_delegate_to_mixnode(mix_identity, amount, &env, deps.storage)
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(
|
||||
address.as_str().to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_undelegate_from_mixnode(
|
||||
mix_identity: IdentityKey,
|
||||
info: MessageInfo,
|
||||
deps: DepsMut,
|
||||
) -> Result<Response, ContractError> {
|
||||
let delegate_addr = info.sender;
|
||||
let address = deps.api.addr_validate(delegate_addr.as_str())?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
account.try_undelegate_from_mixnode(mix_identity)
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(
|
||||
address.as_str().to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_create_periodic_vesting_account(
|
||||
address: String,
|
||||
start_time: Option<u64>,
|
||||
info: MessageInfo,
|
||||
env: Env,
|
||||
deps: DepsMut,
|
||||
) -> Result<Response, ContractError> {
|
||||
if info.sender != ADMIN_ADDRESS {
|
||||
return Err(ContractError::NotAdmin(info.sender.as_str().to_string()));
|
||||
}
|
||||
let coin = validate_funds(&info.funds)?;
|
||||
let address = deps.api.addr_validate(&address)?;
|
||||
let start_time = start_time.unwrap_or_else(|| env.block.time.seconds());
|
||||
let periods = populate_vesting_periods(start_time, NUM_VESTING_PERIODS);
|
||||
PeriodicVestingAccount::new(
|
||||
address,
|
||||
coin,
|
||||
Timestamp::from_seconds(start_time),
|
||||
periods,
|
||||
deps.storage,
|
||||
)?;
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
|
||||
let query_res = match msg {
|
||||
QueryMsg::LockedCoins {
|
||||
vesting_account_address,
|
||||
block_time,
|
||||
} => to_binary(&try_get_locked_coins(
|
||||
vesting_account_address,
|
||||
block_time,
|
||||
env,
|
||||
deps,
|
||||
)?),
|
||||
QueryMsg::SpendableCoins {
|
||||
vesting_account_address,
|
||||
block_time,
|
||||
} => to_binary(&try_get_spendable_coins(
|
||||
vesting_account_address,
|
||||
block_time,
|
||||
env,
|
||||
deps,
|
||||
)?),
|
||||
QueryMsg::GetVestedCoins {
|
||||
vesting_account_address,
|
||||
block_time,
|
||||
} => to_binary(&try_get_vested_coins(
|
||||
vesting_account_address,
|
||||
block_time,
|
||||
env,
|
||||
deps,
|
||||
)?),
|
||||
QueryMsg::GetVestingCoins {
|
||||
vesting_account_address,
|
||||
block_time,
|
||||
} => to_binary(&try_get_vesting_coins(
|
||||
vesting_account_address,
|
||||
block_time,
|
||||
env,
|
||||
deps,
|
||||
)?),
|
||||
QueryMsg::GetStartTime {
|
||||
vesting_account_address,
|
||||
} => to_binary(&try_get_start_time(vesting_account_address, deps)?),
|
||||
QueryMsg::GetEndTime {
|
||||
vesting_account_address,
|
||||
} => to_binary(&try_get_end_time(vesting_account_address, deps)?),
|
||||
QueryMsg::GetOriginalVesting {
|
||||
vesting_account_address,
|
||||
} => to_binary(&try_get_original_vesting(vesting_account_address, deps)?),
|
||||
QueryMsg::GetDelegatedFree {
|
||||
block_time,
|
||||
vesting_account_address,
|
||||
} => to_binary(&try_get_delegated_free(
|
||||
block_time,
|
||||
vesting_account_address,
|
||||
env,
|
||||
deps,
|
||||
)?),
|
||||
QueryMsg::GetDelegatedVesting {
|
||||
block_time,
|
||||
vesting_account_address,
|
||||
} => to_binary(&try_get_delegated_vesting(
|
||||
block_time,
|
||||
vesting_account_address,
|
||||
env,
|
||||
deps,
|
||||
)?),
|
||||
};
|
||||
|
||||
Ok(query_res?)
|
||||
}
|
||||
|
||||
pub fn try_get_locked_coins(
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Coin, ContractError> {
|
||||
let address = deps.api.addr_validate(&vesting_account_address)?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
Ok(account.locked_coins(block_time, &env, deps.storage)?)
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(vesting_account_address))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_spendable_coins(
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Coin, ContractError> {
|
||||
let address = deps.api.addr_validate(&vesting_account_address)?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
Ok(account.spendable_coins(block_time, &env, deps.storage)?)
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(vesting_account_address))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_vested_coins(
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Coin, ContractError> {
|
||||
let address = deps.api.addr_validate(&vesting_account_address)?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
Ok(account.get_vested_coins(block_time, &env)?)
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(vesting_account_address))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_vesting_coins(
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Coin, ContractError> {
|
||||
let address = deps.api.addr_validate(&vesting_account_address)?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
Ok(account.get_vesting_coins(block_time, &env)?)
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(vesting_account_address))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_start_time(
|
||||
vesting_account_address: String,
|
||||
deps: Deps,
|
||||
) -> Result<Timestamp, ContractError> {
|
||||
let address = deps.api.addr_validate(&vesting_account_address)?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
Ok(account.get_start_time())
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(vesting_account_address))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_end_time(
|
||||
vesting_account_address: String,
|
||||
deps: Deps,
|
||||
) -> Result<Timestamp, ContractError> {
|
||||
let address = deps.api.addr_validate(&vesting_account_address)?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
Ok(account.get_end_time())
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(vesting_account_address))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_original_vesting(
|
||||
vesting_account_address: String,
|
||||
deps: Deps,
|
||||
) -> Result<Coin, ContractError> {
|
||||
let address = deps.api.addr_validate(&vesting_account_address)?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
Ok(account.get_original_vesting())
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(vesting_account_address))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_delegated_free(
|
||||
block_time: Option<Timestamp>,
|
||||
vesting_account_address: String,
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Coin, ContractError> {
|
||||
let address = deps.api.addr_validate(&vesting_account_address)?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
Ok(account.get_delegated_free(block_time, &env, deps.storage))
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(vesting_account_address))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_get_delegated_vesting(
|
||||
block_time: Option<Timestamp>,
|
||||
vesting_account_address: String,
|
||||
env: Env,
|
||||
deps: Deps,
|
||||
) -> Result<Coin, ContractError> {
|
||||
let address = deps.api.addr_validate(&vesting_account_address)?;
|
||||
if let Some(account) = get_account(deps.storage, &address) {
|
||||
Ok(account.get_delegated_vesting(block_time, &env, deps.storage))
|
||||
} else {
|
||||
Err(ContractError::NoAccountForAddress(vesting_account_address))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_funds(funds: &[Coin]) -> Result<Coin, ContractError> {
|
||||
if funds.is_empty() || funds[0].amount.is_zero() {
|
||||
return Err(ContractError::EmptyFunds);
|
||||
}
|
||||
|
||||
if funds.len() > 1 {
|
||||
return Err(ContractError::MultipleDenoms);
|
||||
}
|
||||
|
||||
if funds[0].denom != DENOM {
|
||||
return Err(ContractError::WrongDenom(
|
||||
funds[0].denom.clone(),
|
||||
DENOM.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(funds[0].clone())
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
use cosmwasm_std::StdError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum ContractError {
|
||||
#[error("{0}")]
|
||||
Std(#[from] StdError),
|
||||
#[error("Account does not exist - {0}")]
|
||||
NoAccountForAddress(String),
|
||||
#[error("Only admin can perform this action, {0} is not admin")]
|
||||
NotAdmin(String),
|
||||
#[error("Balance not found for existing account ({0}), this is a bug")]
|
||||
NoBalanceForAddress(String),
|
||||
#[error("Insufficient balance for address {0} -> {1}")]
|
||||
InsufficientBalance(String, u128),
|
||||
#[error("Insufficient spendable balance for address {0} -> {1}")]
|
||||
InsufficientSpendable(String, u128),
|
||||
#[error(
|
||||
"Only delegation owner can perform delegation actions, {0} is not the delegation owner"
|
||||
)]
|
||||
NotDelegate(String),
|
||||
#[error("Total vesting amount is inprobably low -> {0}, this is likely an error")]
|
||||
ImprobableVestingAmount(u128),
|
||||
#[error("Address {0} has already bonded a node")]
|
||||
AlreadyBonded(String),
|
||||
#[error("Recieved empty funds vector")]
|
||||
EmptyFunds,
|
||||
#[error("Recieved wrong denom: {0}, expected {1}")]
|
||||
WrongDenom(String, String),
|
||||
#[error("Recieved multiple denoms, expected 1")]
|
||||
MultipleDenoms,
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
pub mod contract;
|
||||
mod errors;
|
||||
pub mod messages;
|
||||
mod storage;
|
||||
mod support;
|
||||
mod vesting;
|
||||
@@ -1,77 +0,0 @@
|
||||
use cosmwasm_std::{Addr, Coin, Timestamp};
|
||||
use mixnet_contract::IdentityKey;
|
||||
use mixnet_contract::MixNode;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InitMsg {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecuteMsg {
|
||||
DelegateToMixnode {
|
||||
mix_identity: IdentityKey,
|
||||
},
|
||||
UndelegateFromMixnode {
|
||||
mix_identity: IdentityKey,
|
||||
},
|
||||
CreatePeriodicVestingAccount {
|
||||
address: String,
|
||||
start_time: Option<u64>,
|
||||
},
|
||||
WithdrawVestedCoins {
|
||||
amount: Coin,
|
||||
},
|
||||
TrackUndelegation {
|
||||
owner: Addr,
|
||||
mix_identity: IdentityKey,
|
||||
amount: Coin,
|
||||
},
|
||||
BondMixnode {
|
||||
mix_node: MixNode,
|
||||
},
|
||||
UnbondMixnode {},
|
||||
TrackUnbond {
|
||||
owner: Addr,
|
||||
amount: Coin,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
LockedCoins {
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
},
|
||||
SpendableCoins {
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
},
|
||||
GetVestedCoins {
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
},
|
||||
GetVestingCoins {
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
},
|
||||
GetStartTime {
|
||||
vesting_account_address: String,
|
||||
},
|
||||
GetEndTime {
|
||||
vesting_account_address: String,
|
||||
},
|
||||
GetOriginalVesting {
|
||||
vesting_account_address: String,
|
||||
},
|
||||
GetDelegatedFree {
|
||||
block_time: Option<Timestamp>,
|
||||
vesting_account_address: String,
|
||||
},
|
||||
GetDelegatedVesting {
|
||||
block_time: Option<Timestamp>,
|
||||
vesting_account_address: String,
|
||||
},
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
use cosmwasm_std::{Addr, StdResult, Storage, Uint128};
|
||||
use cosmwasm_storage::{bucket, bucket_read, Bucket, ReadonlyBucket};
|
||||
|
||||
use crate::{
|
||||
errors::ContractError,
|
||||
vesting::{BondData, DelegationData, PeriodicVestingAccount},
|
||||
};
|
||||
// storage prefixes
|
||||
// all of them must be unique and presumably not be a prefix of a different one
|
||||
// keeping them as short as possible is also desirable as they are part of each stored key
|
||||
// it's not as important for singletons, but is a nice optimisation for buckets
|
||||
|
||||
// buckets
|
||||
const PREFIX_ACCOUNTS: &[u8] = b"ac";
|
||||
const PREFIX_ACCOUNT_DELEGATIONS: &[u8] = b"ad";
|
||||
const PREFIX_ACCOUNT_BALANCE: &[u8] = b"ab";
|
||||
const PREFIX_ACCOUNT_MIXBOND: &[u8] = b"am";
|
||||
// Contract-level stuff
|
||||
|
||||
fn accounts_mut(storage: &mut dyn Storage) -> Bucket<PeriodicVestingAccount> {
|
||||
bucket(storage, PREFIX_ACCOUNTS)
|
||||
}
|
||||
|
||||
fn accounts(storage: &dyn Storage) -> ReadonlyBucket<PeriodicVestingAccount> {
|
||||
bucket_read(storage, PREFIX_ACCOUNTS)
|
||||
}
|
||||
|
||||
fn account_delegations_mut(storage: &mut dyn Storage) -> Bucket<Vec<DelegationData>> {
|
||||
bucket(storage, PREFIX_ACCOUNT_DELEGATIONS)
|
||||
}
|
||||
|
||||
fn account_delegations(storage: &dyn Storage) -> ReadonlyBucket<Vec<DelegationData>> {
|
||||
bucket_read(storage, PREFIX_ACCOUNT_DELEGATIONS)
|
||||
}
|
||||
|
||||
fn account_bond_mut(storage: &mut dyn Storage) -> Bucket<BondData> {
|
||||
bucket(storage, PREFIX_ACCOUNT_MIXBOND)
|
||||
}
|
||||
|
||||
fn account_bond(storage: &dyn Storage) -> ReadonlyBucket<BondData> {
|
||||
bucket_read(storage, PREFIX_ACCOUNT_MIXBOND)
|
||||
}
|
||||
|
||||
fn account_balance(storage: &dyn Storage) -> ReadonlyBucket<Uint128> {
|
||||
bucket_read(storage, PREFIX_ACCOUNT_BALANCE)
|
||||
}
|
||||
|
||||
fn account_balance_mut(storage: &mut dyn Storage) -> Bucket<Uint128> {
|
||||
bucket(storage, PREFIX_ACCOUNT_BALANCE)
|
||||
}
|
||||
|
||||
pub fn get_account(storage: &dyn Storage, address: &Addr) -> Option<PeriodicVestingAccount> {
|
||||
// Due to using may_load this should be safe to unwrap
|
||||
accounts(storage).may_load(address.as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
pub fn set_account(
|
||||
storage: &mut dyn Storage,
|
||||
account: PeriodicVestingAccount,
|
||||
) -> Result<(), ContractError> {
|
||||
Ok(accounts_mut(storage).save(account.address().as_bytes(), &account)?)
|
||||
}
|
||||
|
||||
pub fn get_account_delegations(
|
||||
storage: &dyn Storage,
|
||||
address: &Addr,
|
||||
) -> Option<Vec<DelegationData>> {
|
||||
// Due to using may_load this should be safe to unwrap
|
||||
account_delegations(storage)
|
||||
.may_load(address.as_bytes())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn set_account_delegations(
|
||||
storage: &mut dyn Storage,
|
||||
address: &Addr,
|
||||
delegations: Vec<DelegationData>,
|
||||
) -> StdResult<()> {
|
||||
account_delegations_mut(storage).save(address.as_bytes(), &delegations)
|
||||
}
|
||||
|
||||
pub fn get_account_bond(storage: &dyn Storage, address: &Addr) -> Option<BondData> {
|
||||
// Due to using may_load this should be safe to unwrap
|
||||
account_bond(storage).may_load(address.as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
pub fn drop_account_bond(storage: &mut dyn Storage, address: &Addr) -> StdResult<()> {
|
||||
Ok(account_bond_mut(storage).remove(address.as_bytes()))
|
||||
}
|
||||
|
||||
pub fn set_account_bond(
|
||||
storage: &mut dyn Storage,
|
||||
address: &Addr,
|
||||
bond: BondData,
|
||||
) -> StdResult<()> {
|
||||
account_bond_mut(storage).save(address.as_bytes(), &bond)
|
||||
}
|
||||
|
||||
pub fn get_account_balance(storage: &dyn Storage, address: &Addr) -> Option<Uint128> {
|
||||
// Due to using may_load this should be safe to unwrap
|
||||
account_balance(storage)
|
||||
.may_load(address.as_bytes())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn set_account_balance(
|
||||
storage: &mut dyn Storage,
|
||||
address: &Addr,
|
||||
balance: Uint128,
|
||||
) -> StdResult<()> {
|
||||
account_balance_mut(storage).save(address.as_bytes(), &balance)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod tests;
|
||||
@@ -1,47 +0,0 @@
|
||||
#[cfg(test)]
|
||||
pub mod helpers {
|
||||
use crate::contract::{instantiate, NUM_VESTING_PERIODS, VESTING_PERIOD};
|
||||
use crate::messages::InitMsg;
|
||||
use crate::storage;
|
||||
use crate::vesting::populate_vesting_periods;
|
||||
use crate::vesting::PeriodicVestingAccount;
|
||||
use crate::vesting::VestingPeriod;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::from_binary;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::testing::MockQuerier;
|
||||
use cosmwasm_std::Addr;
|
||||
use cosmwasm_std::Coin;
|
||||
use cosmwasm_std::OwnedDeps;
|
||||
use cosmwasm_std::Uint128;
|
||||
use cosmwasm_std::{Empty, Env, MemoryStorage, Storage};
|
||||
|
||||
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
let mut deps = mock_dependencies();
|
||||
let msg = InitMsg {};
|
||||
let env = mock_env();
|
||||
let info = mock_info("creator", &[]);
|
||||
instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();
|
||||
return deps;
|
||||
}
|
||||
|
||||
pub fn vesting_account_fixture(storage: &mut dyn Storage, env: &Env) -> PeriodicVestingAccount {
|
||||
let start_time = env.block.time;
|
||||
let periods = populate_vesting_periods(start_time.seconds(), NUM_VESTING_PERIODS);
|
||||
|
||||
PeriodicVestingAccount::new(
|
||||
Addr::unchecked("fixture"),
|
||||
Coin {
|
||||
amount: Uint128::new(1_000_000_000_000),
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
start_time,
|
||||
periods,
|
||||
storage,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,720 +0,0 @@
|
||||
use crate::contract::{NUM_VESTING_PERIODS, VESTING_PERIOD};
|
||||
use crate::errors::ContractError;
|
||||
use crate::storage::{
|
||||
drop_account_bond, get_account_balance, get_account_bond, get_account_delegations, set_account,
|
||||
set_account_balance, set_account_bond, set_account_delegations,
|
||||
};
|
||||
use config::defaults::{DEFAULT_MIXNET_CONTRACT_ADDRESS, DENOM};
|
||||
use cosmwasm_std::{attr, wasm_execute, Addr, Coin, Env, Response, Storage, Timestamp, Uint128};
|
||||
use mixnet_contract::IdentityKey;
|
||||
use mixnet_contract::{ExecuteMsg as MixnetExecuteMsg, MixNode};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub trait VestingAccount {
|
||||
// locked_coins returns the set of coins that are not spendable (can still be delegated tough) (i.e. locked),
|
||||
// defined as the vesting coins that are not delegated.
|
||||
//
|
||||
// To get spendable coins of a vesting account, first the total balance must
|
||||
// be retrieved and the locked tokens can be subtracted from the total balance.
|
||||
// Note, the spendable balance can be negative.
|
||||
fn locked_coins(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Coin, ContractError>;
|
||||
|
||||
// Calculates the total spendable balance that can be sent to other accounts.
|
||||
fn spendable_coins(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Coin, ContractError>;
|
||||
|
||||
fn get_vested_coins(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
) -> Result<Coin, ContractError>;
|
||||
fn get_vesting_coins(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
) -> Result<Coin, ContractError>;
|
||||
|
||||
fn get_start_time(&self) -> Timestamp;
|
||||
fn get_end_time(&self) -> Timestamp;
|
||||
|
||||
fn get_original_vesting(&self) -> Coin;
|
||||
fn get_delegated_free(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Coin;
|
||||
fn get_delegated_vesting(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Coin;
|
||||
}
|
||||
|
||||
pub trait DelegationAccount {
|
||||
fn try_delegate_to_mixnode(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
amount: Coin,
|
||||
env: &Env,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Response, ContractError>;
|
||||
|
||||
fn try_undelegate_from_mixnode(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
) -> Result<Response, ContractError>;
|
||||
|
||||
// track_delegation performs internal vesting accounting necessary when
|
||||
// delegating from a vesting account. It accepts the current block time, the
|
||||
// delegation amount and balance of all coins whose denomination exists in
|
||||
// the account's original vesting balance.
|
||||
fn track_delegation(
|
||||
&self,
|
||||
block_time: Timestamp,
|
||||
mix_identity: IdentityKey,
|
||||
delegation: Coin,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError>;
|
||||
// track_undelegation performs internal vesting accounting necessary when a
|
||||
// vesting account performs an undelegation.
|
||||
fn track_undelegation(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
amount: Coin,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError>;
|
||||
}
|
||||
|
||||
pub trait BondingAccount {
|
||||
fn try_bond_mixnode(
|
||||
&self,
|
||||
mix_node: MixNode,
|
||||
amount: Coin,
|
||||
env: &Env,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Response, ContractError>;
|
||||
|
||||
fn try_unbond_mixnode(&self) -> Result<Response, ContractError>;
|
||||
|
||||
fn track_bond(
|
||||
&self,
|
||||
block_time: Timestamp,
|
||||
bond: Coin,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError>;
|
||||
|
||||
fn track_unbond(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError>;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct VestingPeriod {
|
||||
pub start_time: u64,
|
||||
}
|
||||
|
||||
impl VestingPeriod {
|
||||
pub fn end_time(&self) -> Timestamp {
|
||||
Timestamp::from_seconds(self.start_time + VESTING_PERIOD)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct PeriodicVestingAccount {
|
||||
address: Addr,
|
||||
start_time: Timestamp,
|
||||
periods: Vec<VestingPeriod>,
|
||||
coin: Coin,
|
||||
}
|
||||
|
||||
impl PeriodicVestingAccount {
|
||||
pub fn new(
|
||||
address: Addr,
|
||||
coin: Coin,
|
||||
start_time: Timestamp,
|
||||
periods: Vec<VestingPeriod>,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Self, ContractError> {
|
||||
let amount = coin.amount;
|
||||
let account = PeriodicVestingAccount {
|
||||
address,
|
||||
start_time,
|
||||
periods,
|
||||
coin,
|
||||
};
|
||||
set_account(storage, account.clone())?;
|
||||
set_account_balance(storage, &account.address, amount)?;
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
pub fn address(&self) -> Addr {
|
||||
self.address.clone()
|
||||
}
|
||||
|
||||
pub fn tokens_per_period(&self) -> Result<u128, ContractError> {
|
||||
let amount = self.coin.amount.u128();
|
||||
if amount < NUM_VESTING_PERIODS as u128 {
|
||||
Err(ContractError::ImprobableVestingAmount(amount))
|
||||
} else {
|
||||
// Remainder tokens will be lumped into the last period.
|
||||
Ok(amount / NUM_VESTING_PERIODS as u128)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_vesting_period(&self, block_time: Timestamp) -> usize {
|
||||
// Returns the index of the next vesting period. Unless the current time is somehow in the past or vesting has not started yet.
|
||||
// In case vesting is over it will always return NUM_VESTING_PERIODS.
|
||||
let period = match self
|
||||
.periods
|
||||
.iter()
|
||||
.map(|period| period.start_time)
|
||||
.collect::<Vec<u64>>()
|
||||
.binary_search(&block_time.seconds())
|
||||
{
|
||||
Ok(u) => u,
|
||||
Err(u) => u,
|
||||
};
|
||||
|
||||
if period > 0 {
|
||||
period - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn get_delegated_with_op(
|
||||
&self,
|
||||
op: &dyn Fn(u64, u64) -> bool,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Coin {
|
||||
let block_time = block_time.unwrap_or(env.block.time);
|
||||
let period = self.get_current_vesting_period(block_time);
|
||||
if period == 0 {
|
||||
return Coin {
|
||||
amount: Uint128::new(0),
|
||||
denom: DENOM.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let end_time = if period >= NUM_VESTING_PERIODS as usize {
|
||||
u64::MAX
|
||||
} else {
|
||||
self.periods[period].end_time().seconds()
|
||||
};
|
||||
|
||||
if let Some(delegations) = get_account_delegations(storage, &self.address) {
|
||||
delegations
|
||||
.iter()
|
||||
.filter(|d| op(d.block_time.seconds(), end_time))
|
||||
.fold(
|
||||
Coin {
|
||||
amount: Uint128::new(0),
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
|mut acc, d| {
|
||||
acc.amount += d.amount;
|
||||
acc
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Coin {
|
||||
amount: Uint128::new(0),
|
||||
denom: DENOM.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_balance(&self, storage: &dyn Storage) -> Uint128 {
|
||||
get_account_balance(storage, &self.address).unwrap_or(Uint128::new(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl DelegationAccount for PeriodicVestingAccount {
|
||||
fn try_delegate_to_mixnode(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
coin: Coin,
|
||||
env: &Env,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Response, ContractError> {
|
||||
if coin.amount < self.get_balance(storage) {
|
||||
let msg = MixnetExecuteMsg::DelegateToMixnodeOnBehalf {
|
||||
mix_identity: mix_identity.clone(),
|
||||
delegate: self.address.clone(),
|
||||
};
|
||||
let delegate_to_mixnode =
|
||||
wasm_execute(DEFAULT_MIXNET_CONTRACT_ADDRESS, &msg, vec![coin.clone()])?.into();
|
||||
let attributes = vec![attr("action", "delegate to mixnode on behalf")];
|
||||
self.track_delegation(env.block.time, mix_identity, coin, storage)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "delegate to mixnode on behalf")
|
||||
.add_message(delegate_to_mixnode))
|
||||
} else {
|
||||
Err(ContractError::InsufficientBalance(
|
||||
self.address.as_str().to_string(),
|
||||
self.get_balance(storage).u128(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_undelegate_from_mixnode(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
) -> Result<Response, ContractError> {
|
||||
let msg = MixnetExecuteMsg::UndelegateFromMixnodeOnBehalf {
|
||||
mix_identity,
|
||||
delegate: self.address.clone(),
|
||||
};
|
||||
let undelegate_from_mixnode = wasm_execute(
|
||||
DEFAULT_MIXNET_CONTRACT_ADDRESS,
|
||||
&msg,
|
||||
vec![Coin {
|
||||
amount: Uint128::new(0),
|
||||
denom: DENOM.to_string(),
|
||||
}],
|
||||
)?;
|
||||
|
||||
let attributes = vec![attr("action", "undelegate to mixnode on behalf")];
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "undelegate to mixnode on behalf")
|
||||
.add_message(undelegate_from_mixnode))
|
||||
}
|
||||
|
||||
fn track_delegation(
|
||||
&self,
|
||||
block_time: Timestamp,
|
||||
mix_identity: IdentityKey,
|
||||
delegation: Coin,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError> {
|
||||
let mut delegations =
|
||||
if let Some(delegations) = get_account_delegations(storage, &self.address) {
|
||||
delegations
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
delegations.push(DelegationData {
|
||||
mix_identity,
|
||||
amount: delegation.amount,
|
||||
block_time,
|
||||
});
|
||||
|
||||
let new_balance = if let Some(balance) = get_account_balance(storage, &self.address) {
|
||||
// We've checked that delegation < balance in the caller function
|
||||
Uint128::new(balance.u128() - delegation.amount.u128())
|
||||
} else {
|
||||
return Err(ContractError::NoBalanceForAddress(
|
||||
self.address.as_str().to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
set_account_delegations(storage, &self.address, delegations)?;
|
||||
set_account_balance(storage, &self.address, new_balance)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn track_undelegation(
|
||||
&self,
|
||||
mix_identity: IdentityKey,
|
||||
amount: Coin,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError> {
|
||||
// This has to exist in storage at this point.
|
||||
let delegations = get_account_delegations(storage, &self.address)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|d| d.mix_identity != mix_identity)
|
||||
.collect();
|
||||
|
||||
let new_balance = if let Some(balance) = get_account_balance(storage, &self.address) {
|
||||
Uint128::new(balance.u128() + amount.amount.u128())
|
||||
} else {
|
||||
return Err(ContractError::NoBalanceForAddress(
|
||||
self.address.as_str().to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
set_account_balance(storage, &self.address, new_balance)?;
|
||||
|
||||
Ok(set_account_delegations(
|
||||
storage,
|
||||
&self.address,
|
||||
delegations,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl BondingAccount for PeriodicVestingAccount {
|
||||
fn try_bond_mixnode(
|
||||
&self,
|
||||
mix_node: MixNode,
|
||||
bond: Coin,
|
||||
env: &Env,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Response, ContractError> {
|
||||
if bond.amount < self.get_balance(storage) {
|
||||
let msg = MixnetExecuteMsg::BondMixnodeOnBehalf {
|
||||
mix_node,
|
||||
owner: self.address.clone(),
|
||||
};
|
||||
let bond_mixnode =
|
||||
wasm_execute(DEFAULT_MIXNET_CONTRACT_ADDRESS, &msg, vec![bond.clone()])?;
|
||||
let attributes = vec![attr("action", "bond mixnode on behalf")];
|
||||
self.track_bond(env.block.time, bond, storage)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "bond mixnode on behalf")
|
||||
.add_message(bond_mixnode))
|
||||
} else {
|
||||
Err(ContractError::InsufficientBalance(
|
||||
self.address.as_str().to_string(),
|
||||
self.get_balance(storage).u128(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn track_bond(
|
||||
&self,
|
||||
block_time: Timestamp,
|
||||
bond: Coin,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError> {
|
||||
let bond = if let Some(_bond) = get_account_bond(storage, &self.address) {
|
||||
return Err(ContractError::AlreadyBonded(
|
||||
self.address.as_str().to_string(),
|
||||
));
|
||||
} else {
|
||||
BondData {
|
||||
block_time,
|
||||
amount: bond.amount,
|
||||
}
|
||||
};
|
||||
|
||||
let new_balance = if let Some(balance) = get_account_balance(storage, &self.address) {
|
||||
// We've checked that bond.amount < balance in the caller function
|
||||
Uint128::new(balance.u128() - bond.amount.u128())
|
||||
} else {
|
||||
return Err(ContractError::NoBalanceForAddress(
|
||||
self.address.as_str().to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
set_account_balance(storage, &self.address, new_balance)?;
|
||||
Ok(set_account_bond(storage, &self.address, bond)?)
|
||||
}
|
||||
|
||||
fn try_unbond_mixnode(&self) -> Result<Response, ContractError> {
|
||||
let msg = MixnetExecuteMsg::UnbondMixnodeOnBehalf {
|
||||
owner: self.address.clone(),
|
||||
};
|
||||
let unbond_mixnode = wasm_execute(
|
||||
DEFAULT_MIXNET_CONTRACT_ADDRESS,
|
||||
&msg,
|
||||
vec![Coin {
|
||||
amount: Uint128::new(0),
|
||||
denom: DENOM.to_string(),
|
||||
}],
|
||||
)?;
|
||||
|
||||
let attributes = vec![attr("action", "unbond mixnode on behalf")];
|
||||
|
||||
Ok(Response::new()
|
||||
.add_attribute("action", "unbond mixnode on behalf")
|
||||
.add_message(unbond_mixnode))
|
||||
}
|
||||
|
||||
fn track_unbond(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError> {
|
||||
let new_balance = if let Some(balance) = get_account_balance(storage, &self.address) {
|
||||
Uint128::new(balance.u128() + amount.amount.u128())
|
||||
} else {
|
||||
return Err(ContractError::NoBalanceForAddress(
|
||||
self.address.as_str().to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
set_account_balance(storage, &self.address, new_balance)?;
|
||||
drop_account_bond(storage, &self.address)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl VestingAccount for PeriodicVestingAccount {
|
||||
fn locked_coins(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Coin, ContractError> {
|
||||
// Returns 0 in case of underflow.
|
||||
Ok(Coin {
|
||||
amount: Uint128::new(
|
||||
self.get_vesting_coins(block_time, env)?
|
||||
.amount
|
||||
.u128()
|
||||
.saturating_sub(
|
||||
self.get_delegated_vesting(block_time, env, storage)
|
||||
.amount
|
||||
.u128(),
|
||||
),
|
||||
),
|
||||
denom: DENOM.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn spendable_coins(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Coin, ContractError> {
|
||||
Ok(Coin {
|
||||
amount: Uint128::new(
|
||||
self.get_balance(storage)
|
||||
.u128()
|
||||
.saturating_sub(self.locked_coins(block_time, env, storage)?.amount.u128()),
|
||||
),
|
||||
denom: DENOM.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_vested_coins(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
) -> Result<Coin, ContractError> {
|
||||
let block_time = block_time.unwrap_or(env.block.time);
|
||||
let period = self.get_current_vesting_period(block_time);
|
||||
|
||||
let amount = match period {
|
||||
// We're in the first period, or the vesting has not started yet.
|
||||
0 => Coin {
|
||||
amount: Uint128::new(0),
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
// We always have 8 vesting periods, so periods 1-7 are special
|
||||
1..=7 => Coin {
|
||||
amount: Uint128::new(self.tokens_per_period()? * period as u128),
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
_ => Coin {
|
||||
amount: self.coin.amount,
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
};
|
||||
Ok(amount)
|
||||
}
|
||||
|
||||
fn get_vesting_coins(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
) -> Result<Coin, ContractError> {
|
||||
Ok(Coin {
|
||||
amount: Uint128::new(
|
||||
self.get_original_vesting().amount.u128()
|
||||
- self.get_vested_coins(block_time, env)?.amount.u128(),
|
||||
),
|
||||
denom: DENOM.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_start_time(&self) -> Timestamp {
|
||||
self.start_time
|
||||
}
|
||||
|
||||
fn get_end_time(&self) -> Timestamp {
|
||||
self.periods[(NUM_VESTING_PERIODS - 1) as usize].end_time()
|
||||
}
|
||||
|
||||
fn get_original_vesting(&self) -> Coin {
|
||||
self.coin.clone()
|
||||
}
|
||||
|
||||
fn get_delegated_free(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Coin {
|
||||
self.get_delegated_with_op(<, block_time, env, storage)
|
||||
}
|
||||
|
||||
fn get_delegated_vesting(
|
||||
&self,
|
||||
block_time: Option<Timestamp>,
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Coin {
|
||||
self.get_delegated_with_op(&ge, block_time, env, storage)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct DelegationData {
|
||||
mix_identity: IdentityKey,
|
||||
amount: Uint128,
|
||||
block_time: Timestamp,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct BondData {
|
||||
amount: Uint128,
|
||||
block_time: Timestamp,
|
||||
}
|
||||
|
||||
fn lt(x: u64, y: u64) -> bool {
|
||||
x < y
|
||||
}
|
||||
|
||||
fn ge(x: u64, y: u64) -> bool {
|
||||
x >= y
|
||||
}
|
||||
|
||||
pub fn populate_vesting_periods(start_time: u64, n: usize) -> Vec<VestingPeriod> {
|
||||
let mut periods = Vec::with_capacity(n as usize);
|
||||
for i in 0..n {
|
||||
let period = VestingPeriod {
|
||||
start_time: start_time + i as u64 * VESTING_PERIOD,
|
||||
};
|
||||
periods.push(period);
|
||||
}
|
||||
periods
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::contract::{NUM_VESTING_PERIODS, VESTING_PERIOD};
|
||||
use crate::storage::{get_account, get_account_balance, get_account_delegations};
|
||||
use crate::support::tests::helpers::{init_contract, vesting_account_fixture};
|
||||
use crate::vesting::{DelegationData, VestingAccount};
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
|
||||
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
|
||||
|
||||
use super::DelegationAccount;
|
||||
|
||||
#[test]
|
||||
fn test_account_creation() {
|
||||
let mut deps = init_contract();
|
||||
let env = mock_env();
|
||||
let account = vesting_account_fixture(&mut deps.storage, &env);
|
||||
let created_account = get_account(&deps.storage, &account.address);
|
||||
let created_account_test = get_account(&deps.storage, &Addr::unchecked("fixture"));
|
||||
assert_eq!(Some(&account), created_account.as_ref());
|
||||
assert_eq!(Some(&account), created_account_test.as_ref());
|
||||
assert_eq!(
|
||||
get_account_balance(&deps.storage, &account.address),
|
||||
Some(Uint128::new(1_000_000_000_000))
|
||||
);
|
||||
assert_eq!(
|
||||
account.get_balance(&deps.storage),
|
||||
Uint128::new(1_000_000_000_000)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_period_logic() {
|
||||
let mut deps = init_contract();
|
||||
let env = mock_env();
|
||||
|
||||
let account = vesting_account_fixture(&mut deps.storage, &env);
|
||||
|
||||
assert_eq!(account.periods.len(), NUM_VESTING_PERIODS as usize);
|
||||
assert_eq!(account.periods.len(), 8);
|
||||
|
||||
let current_period = account.get_current_vesting_period(Timestamp::from_seconds(0));
|
||||
assert_eq!(0, current_period);
|
||||
|
||||
let block_time = Timestamp::from_seconds(account.start_time.seconds() + VESTING_PERIOD + 1);
|
||||
let current_period = account.get_current_vesting_period(block_time);
|
||||
assert_eq!(current_period, 1);
|
||||
let vested_coins = account.get_vested_coins(Some(block_time), &env).unwrap();
|
||||
let vesting_coins = account.get_vesting_coins(Some(block_time), &env).unwrap();
|
||||
assert_eq!(
|
||||
vested_coins.amount,
|
||||
Uint128::new(account.get_original_vesting().amount.u128() / NUM_VESTING_PERIODS as u128)
|
||||
);
|
||||
assert_eq!(
|
||||
vesting_coins.amount,
|
||||
Uint128::new(
|
||||
account.get_original_vesting().amount.u128()
|
||||
- account.get_original_vesting().amount.u128() / NUM_VESTING_PERIODS as u128
|
||||
)
|
||||
);
|
||||
|
||||
let block_time =
|
||||
Timestamp::from_seconds(account.start_time.seconds() + 5 * VESTING_PERIOD + 1);
|
||||
let current_period = account.get_current_vesting_period(block_time);
|
||||
assert_eq!(current_period, 5);
|
||||
let vested_coins = account.get_vested_coins(Some(block_time), &env).unwrap();
|
||||
let vesting_coins = account.get_vesting_coins(Some(block_time), &env).unwrap();
|
||||
assert_eq!(
|
||||
vested_coins.amount,
|
||||
Uint128::new(5 * account.get_original_vesting().amount.u128() / NUM_VESTING_PERIODS as u128)
|
||||
);
|
||||
assert_eq!(
|
||||
vesting_coins.amount,
|
||||
Uint128::new(
|
||||
account.get_original_vesting().amount.u128()
|
||||
- 5 * account.get_original_vesting().amount.u128()
|
||||
/ NUM_VESTING_PERIODS as u128
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegations() {
|
||||
let mut deps = init_contract();
|
||||
let env = mock_env();
|
||||
|
||||
let account = vesting_account_fixture(&mut deps.storage, &env);
|
||||
|
||||
// Try delegating too much
|
||||
let err = account.try_delegate_to_mixnode(
|
||||
"alice".to_string(),
|
||||
Coin {
|
||||
amount: Uint128::new(1_000_000_000_001),
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
&env,
|
||||
&mut deps.storage,
|
||||
);
|
||||
assert!(err.is_err());
|
||||
|
||||
let ok = account.try_delegate_to_mixnode(
|
||||
"alice".to_string(),
|
||||
Coin {
|
||||
amount: Uint128::new(100_000_000_000),
|
||||
denom: DENOM.to_string(),
|
||||
},
|
||||
&env,
|
||||
&mut deps.storage,
|
||||
);
|
||||
assert!(ok.is_ok());
|
||||
|
||||
let delegations = get_account_delegations(&mut deps.storage, &account.address).unwrap();
|
||||
assert_eq!(
|
||||
DelegationData {
|
||||
mix_identity: "alice".to_string(),
|
||||
block_time: env.block.time,
|
||||
amount: Uint128::new(100_000_000_000)
|
||||
},
|
||||
delegations[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ export const CustomColumnHeading: React.FC<{ headingTitle: string }> = ({
|
||||
fontWeight: 'bold',
|
||||
fontSize: 14,
|
||||
padding: 0,
|
||||
// border: '1px solid red',
|
||||
// minWidth: 300,
|
||||
}}
|
||||
data-testid={headingTitle}
|
||||
>
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface UniversalTableProps {
|
||||
|
||||
function formatCellValues(val: string | number, field: string) {
|
||||
if (field === 'bond') {
|
||||
return printableCoin({ amount: val.toString(), denom: 'unpunk' });
|
||||
return printableCoin({ amount: val.toString(), denom: 'upunk' });
|
||||
}
|
||||
return val;
|
||||
}
|
||||
@@ -66,6 +66,7 @@ export const DetailTable: React.FC<{
|
||||
padding: 2,
|
||||
width: 200,
|
||||
}}
|
||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(
|
||||
eachRow[columnsData[index].field],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user